#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""
   @project: HSPyLib
   @package: hspylib.main.hspylib.modules.qt.promotions
      @file: hlistwidget.py
   @created: Tue, 4 May 2021
    @author: <B>H</B>ugo <B>S</B>aporetti <B>J</B>unior"
      @site: https://github.com/yorevs/hspylib
   @license: MIT - Please refer to <https://opensource.org/licenses/MIT>

   Copyright 2022, HSPyLib team
"""
from typing import List, Optional, Union

from PyQt5.QtCore import pyqtSignal, QModelIndex, Qt
from PyQt5.QtGui import QCursor, QKeyEvent
from PyQt5.QtWidgets import QListWidget, QListWidgetItem, QMenu, QWidget

from hspylib.core.preconditions import check_argument, check_not_none, check_state


class HListWidget(QListWidget):
    """TODO"""

    keyPressed = pyqtSignal(int)

    @staticmethod
    def unset_flag(flags: int, flag: int) -> int:
        """TODO"""
        return flags ^ flag if flags & flag == flag else flags

    def __init__(self, parent: Optional[QWidget] = None):
        super().__init__(parent)
        self._editable = False
        self._selectable = True
        self._items = []
        self._custom_menu_actions = []
        self._context_menu_enable = True
        self.setContextMenuPolicy(Qt.CustomContextMenu)
        self.customContextMenuRequested.connect(self._context_menu)
        self.itemChanged.connect(self.item_changed)

    def keyPressEvent(self, event: QKeyEvent) -> None:
        """Handles and forwards the key press event in the list. """
        super().keyPressEvent(event)
        self.keyPressed.emit(event.key())

    def item_changed(self, item: QListWidgetItem):
        """Handles itemChanged event to avoid adding duplicates to the list. """
        existing = self.findItems(item.text(), Qt.MatchFixedString)
        if len(existing) > 1:
            self.del_item(item)

    def addItem(self, item: QListWidgetItem) -> None:
        """Adds the specified element to this list regardless if it is present or not. """
        prev = self.item(self.count() - 1)
        if prev:
            item.setFlags(prev.flags())
        super().addItem(item)
        self._items.append(item)

    def is_empty(self) -> bool:
        """Returns true if this list contains no elements. """
        return len(self._items) == 0

    def set_item(self, item: Union[str, QListWidgetItem], flags: Union[Qt.ItemFlags, Qt.ItemFlag] = None) -> None:
        """Adds the specified element to this list if it is not already present."""
        if not self.findItems(item, Qt.MatchFixedString):
            if isinstance(item, str):
                w_item = QListWidgetItem(item)
                w_item.setFlags(flags or Qt.ItemIsSelectable | Qt.ItemIsEditable | Qt.ItemIsEnabled)
                self.addItem(w_item)
            else:
                item.setFlags(flags or Qt.ItemIsSelectable | Qt.ItemIsEditable | Qt.ItemIsEnabled)
                self.addItem(item)

    def remove_item_at_index(self, row: int) -> Optional[QListWidgetItem]:
        """Removes the element at the specified row from this list if it is present. """
        item = self.takeItem(row)
        if item:
            self._items.remove(self._items[row])
        return item

    def del_item(self, item_or_index: Union[int, QModelIndex, QListWidgetItem]) -> Optional[QListWidgetItem]:
        """Removes the specified element from this list if it is present."""
        item = None
        if isinstance(item_or_index, int):
            if 0 <= item_or_index < len(self._items):
                item = self.remove_item_at_index(item_or_index)
        elif isinstance(item_or_index, QListWidgetItem):
            index = self.indexFromItem(item_or_index)
            if index:
                item = self.remove_item_at_index(index.row())
        elif isinstance(item_or_index, QModelIndex):
            item = self.remove_item_at_index(item_or_index.row())

        return item

    def clear(self) -> None:
        """Removes all of the elements from this list."""
        super().clear()
        del self._items[:]

    def size(self) -> int:
        """Returns the number of elements in this list. """
        return self.count()

    def index_of(self, item: str) -> int:
        """Returns the index of the first occurrence of the specified element in this list,
        or -1 if this list does not contain the element."""
        return next((obj for obj in self._items if obj == item), -1)

    def as_list(self) -> List[str]:
        """Returns a list containing all items in this list as a string."""
        return list(map(QListWidgetItem.text, self._items))

    def current_text(self) -> str:
        """Returns the current item's text."""
        return self.currentItem().text()

    def set_context_menu_enable(self, enabled: bool = True) -> None:
        """Whether context menu is enabled or not"""
        self._context_menu_enable = enabled

    def set_editable(self, editable: bool = True) -> None:
        """Set editable property."""
        for item in self._items:
            flags = int(item.flags())
            if editable:
                item.setFlags(flags | Qt.ItemIsEditable)
            else:
                item.setFlags(self.unset_flag(flags, int(Qt.ItemIsEditable)))
        self._editable = editable

    def set_selectable(self, selectable: bool = True) -> None:
        """Set selectable property."""
        for item in self._items:
            flags = int(item.flags())
            if selectable:
                item.setFlags(flags | Qt.ItemIsSelectable)
            else:
                item.setFlags(self.unset_flag(flags, int(Qt.ItemIsSelectable)))
        self._selectable = selectable

    def _context_menu(self) -> None:
        """Display the custom context menu."""
        if self._context_menu_enable:
            ctx_menu = QMenu(self)
            if self._editable:
                ctx_menu.addAction('Add Item', lambda: self.set_item("<new_item>"))
                if not self.is_empty():
                    ctx_menu.addAction('Delete Item', lambda: self.del_item(self.currentIndex().row()))
                    ctx_menu.addSeparator()
                    ctx_menu.addAction('Clear list', self.clear)

            for act in self._custom_menu_actions:
                check_not_none(act)
                check_state(len(act) == 3, f'Invalid custom menu action: {act}')
                check_argument(callable(act[1]), 'The action must be callable')
                if act[2]:
                    ctx_menu.addSeparator()
                ctx_menu.addAction(act[0], act[1])

            ctx_menu.exec_(QCursor.pos())
