"""
Provides the graphical front-end for Piket.
"""
import collections
import itertools
import logging
import math
import os
import sys
from typing import Deque, Iterator

import qdarkstyle

# pylint: disable=E0611
from PySide2.QtWidgets import (
    QAction,
    QActionGroup,
    QApplication,
    QGridLayout,
    QInputDialog,
    QLineEdit,
    QMainWindow,
    QMessageBox,
    QPushButton,
    QSizePolicy,
    QToolBar,
    QWidget,
)
from PySide2.QtGui import QIcon
from PySide2.QtCore import QObject, QSize, Qt, Signal, Slot, QUrl
from PySide2.QtMultimedia import QSoundEffect

# pylint: enable=E0611

try:
    import dbus
except ImportError:
    dbus = None

from piket_client.sound import PLOP_PATH, UNDO_PATH
from piket_client.model import (
    Person,
    ConsumptionType,
    Consumption,
    ServerStatus,
    NetworkError,
    Settlement,
)
import piket_client.logger

LOG = logging.getLogger(__name__)


class NameButton(QPushButton):
    """ Wraps a QPushButton to provide a counter. """

    consumption_created = Signal(Consumption)

    def __init__(self, person: Person, active_id: str, *args, **kwargs) -> None:
        self.person = person
        self.active_id = active_id

        super().__init__(self.current_label, *args, **kwargs)
        self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
        self.consumption_created.connect(self.window().consumption_added)

        self.clicked.connect(self.process_click)
        self.setContextMenuPolicy(Qt.CustomContextMenu)
        self.customContextMenuRequested.connect(self.confirm_hide)

    @Slot(str)
    def new_active_id(self, new_id: str) -> None:
        """ Change the active ConsumptionType id, update the label. """
        self.active_id = new_id
        self.setText(self.current_label)

    @Slot()
    def rebuild(self) -> None:
        """ Refresh the Person object and the label. """
        self.person = self.person.reload()  # type: ignore
        self.setText(self.current_label)

    @property
    def current_count(self) -> int:
        """ Return the count of the currently active ConsumptionType for this
        Person. """
        return self.person.consumptions.get(self.active_id, 0)

    @property
    def current_label(self) -> str:
        """ Return the label to show on the button. """
        return f"{self.person.name}\n{self.current_count}"

    def process_click(self) -> None:
        """ Process a click on this button. """
        LOG.debug("Button clicked.")
        result = self.person.add_consumption(self.active_id)
        if result:
            self.window().play_plop()
            self.setText(self.current_label)
            self.consumption_created.emit(result)
        else:
            LOG.error("Failed to add consumption", extra={"person": self.person})

    def confirm_hide(self) -> None:
        LOG.debug("Button right-clicked.")
        ok = QMessageBox.warning(
            self.window(),
            "Persoon verbergen?",
            f"Wil je {self.person.name} verbergen?",
            QMessageBox.Yes,
            QMessageBox.Cancel,
        )

        if ok == QMessageBox.Yes:
            LOG.warning("Hiding person %s", self.person.name)
            self.person.set_active(False)
            self.parent().init_ui()


class NameButtons(QWidget):
    """ Main widget responsible for capturing presses and registering them.
    """

    new_id_set = Signal(str)

    def __init__(self, consumption_type_id: str, *args, **kwargs) -> None:
        super().__init__(*args, **kwargs)

        self.layout = None
        self.layout = QGridLayout()
        self.setLayout(self.layout)
        self.active_consumption_type_id = consumption_type_id
        self.init_ui()

    @Slot(str)
    def consumption_type_changed(self, new_id: str):
        """ Process a change of the consumption type and propagate to the
        contained buttons. """
        LOG.debug("Consumption type updated in NameButtons.", extra={"new_id": new_id})
        self.active_consumption_type_id = new_id
        self.new_id_set.emit(new_id)

    def init_ui(self) -> None:
        """ Initialize UI: build GridLayout, retrieve People and build a button
        for each. """

        LOG.debug("Initializing NameButtons.")

        ps = Person.get_all(True)
        assert not isinstance(ps, NetworkError)
        num_columns = math.ceil(math.sqrt(len(ps)))

        if self.layout:
            LOG.debug("Removing %s widgets for rebuild", self.layout.count())
            for index in range(self.layout.count()):
                item = self.layout.itemAt(0)
                LOG.debug("Removing item %s: %s", index, item)
                if item:
                    w = item.widget()
                    LOG.debug("Person %s", w.person)
                    self.layout.removeItem(item)
                    w.deleteLater()

        for index, person in enumerate(ps):
            button = NameButton(person, self.active_consumption_type_id, self)
            self.new_id_set.connect(button.new_active_id)
            self.layout.addWidget(button, index // num_columns, index % num_columns)


class PiketMainWindow(QMainWindow):
    """ QMainWindow subclass responsible for showing the main application
    window. """

    consumption_type_changed = Signal(str)

    plop_loop: Iterator[QSoundEffect]
    undo_loop: Iterator[QSoundEffect]

    def __init__(self) -> None:
        LOG.debug("Initializing PiketMainWindow.")
        super().__init__()

        self.main_widget = None
        self.dark_theme = True
        self.toolbar = None
        self.osk = None
        self.undo_action = None
        self.undo_queue: Deque[Consumption] = collections.deque([], 15)
        self.init_ui()

    def init_ui(self) -> None:
        """ Initialize the UI: construct main widget and toolbar. """

        # Connect to dbus, get handle to virtual keyboard
        if dbus:
            try:
                session_bus = dbus.SessionBus()
                self.osk = session_bus.get_object(
                    "org.onboard.Onboard", "/org/onboard/Onboard/Keyboard"
                )
            except dbus.exceptions.DBusException as exception:
                # Onboard not present or dbus broken
                self.osk = None
                LOG.error("Could not connect to Onboard:")
                LOG.exception(exception)
        else:
            LOG.warning("Onboard disabled due to missing dbus.")

        # Go full screen
        self.setWindowState(Qt.WindowActive | Qt.WindowFullScreen)

        font_metrics = self.fontMetrics()
        icon_size = font_metrics.height() * 1.45

        # Initialize toolbar
        self.toolbar = QToolBar()
        assert self.toolbar is not None
        self.toolbar.setToolButtonStyle(Qt.ToolButtonTextUnderIcon)
        self.toolbar.setIconSize(QSize(icon_size, icon_size))

        # Left
        self.toolbar.addAction(
            self.load_icon("add_person.svg"), "+ Naam", self.add_person
        )
        self.undo_action = self.toolbar.addAction(
            self.load_icon("undo.svg"), "Oeps", self.do_undo
        )
        self.undo_action.setDisabled(True)

        self.toolbar.addAction(
            self.load_icon("quit.svg"), "Afsluiten", self.confirm_quit
        )

        self.toolbar.addWidget(self.create_spacer())

        # Right
        self.toolbar.addAction(
            self.load_icon("add_consumption_type.svg"),
            "Nieuw",
            self.add_consumption_type,
        )
        self.toolbar.setContextMenuPolicy(Qt.PreventContextMenu)
        self.toolbar.setFloatable(False)
        self.toolbar.setMovable(False)
        self.ct_ag: QActionGroup = QActionGroup(self.toolbar)
        self.ct_ag.setExclusive(True)

        cts = ConsumptionType.get_all()
        if not cts:
            self.show_keyboard()
            name, ok = QInputDialog.getItem(
                self,
                "Consumptietype toevoegen",
                (
                    "Dit lijkt de eerste keer te zijn dat Piket start. Wat wil je "
                    "tellen? Je kunt later meer typen toevoegen."
                ),
                ["Bier", "Wijn", "Cola"],
                current=0,
                editable=True,
            )
            self.hide_keyboard()

            if ok and name:
                c_type = ConsumptionType(name=name)
                c_type = c_type.create()

                cts.append(c_type)

            else:
                QMessageBox.critical(
                    self,
                    "Kan niet doorgaan",
                    (
                        "Je drukte op 'Annuleren' of voerde geen naam in, dus ik"
                        "sluit af."
                    ),
                )
                sys.exit()

        for ct in cts:
            action = QAction(
                self.load_icon(ct.icon or "beer_bottle.svg"), ct.name, self.ct_ag
            )
            action.setCheckable(True)
            action.setData(str(ct.consumption_type_id))

        self.ct_ag.actions()[0].setChecked(True)
        [self.toolbar.addAction(a) for a in self.ct_ag.actions()]
        self.ct_ag.triggered.connect(self.consumption_type_change)

        self.addToolBar(self.toolbar)

        # Load sounds
        plops = [QSoundEffect(self) for _ in range(7)]
        for qse in plops:
            qse.setSource(QUrl.fromLocalFile(str(PLOP_PATH)))
        self.plop_loop = itertools.cycle(plops)

        undos = [QSoundEffect(self) for _ in range(5)]
        for qse in undos:
            qse.setSource(QUrl.fromLocalFile(str(UNDO_PATH)))
        self.undo_loop = itertools.cycle(undos)

        # Initialize main widget
        self.main_widget = NameButtons(self.ct_ag.actions()[0].data(), self)
        self.consumption_type_changed.connect(self.main_widget.consumption_type_changed)
        self.setCentralWidget(self.main_widget)

    @Slot(QAction)
    def consumption_type_change(self, action: QAction):
        self.consumption_type_changed.emit(action.data())

    def show_keyboard(self) -> None:
        """ Show the virtual keyboard, if possible. """
        if self.osk:
            self.osk.Show()

    def hide_keyboard(self) -> None:
        """ Hide the virtual keyboard, if possible. """
        if self.osk:
            self.osk.Hide()

    def add_person(self) -> None:
        """ Ask for a new Person and register it, then rebuild the central
        widget. """
        inactive_persons = Person.get_all(False)
        assert not isinstance(inactive_persons, NetworkError)

        inactive_persons.sort(key=lambda p: p.name)
        inactive_names = [p.name for p in inactive_persons]

        self.show_keyboard()
        name, ok = QInputDialog.getItem(
            self,
            "Persoon toevoegen",
            "Voer de naam van de nieuwe persoon in, of kies uit de lijst.",
            inactive_names,
            0,
            True,
        )
        self.hide_keyboard()
        if ok and name:

            if name in inactive_names:
                person = inactive_persons[inactive_names.index(name)]
                person.set_active(True)

            else:
                person = Person(full_name=name, display_name=None,)
                person.create()

            assert self.main_widget is not None
            self.main_widget.init_ui()

    def add_consumption_type(self) -> None:
        self.show_keyboard()
        name, ok = QInputDialog.getItem(
            self, "Lijst toevoegen", "Wat wil je strepen?", ["Wijn", "Radler"]
        )
        self.hide_keyboard()

        if ok and name:
            ct = ConsumptionType(name=name).create()
            assert not isinstance(ct, NetworkError)

            action = QAction(
                self.load_icon(ct.icon or "beer_bottle.svg"), ct.name, self.ct_ag
            )
            action.setCheckable(True)
            action.setData(str(ct.consumption_type_id))

            assert self.toolbar is not None
            self.toolbar.addAction(action)

    def confirm_quit(self) -> None:
        """ Ask for confirmation that the user wishes to quit, then do so. """
        ok = QMessageBox.warning(
            self,
            "Wil je echt afsluiten?",
            "Bevestig dat je wilt afsluiten.",
            QMessageBox.Yes,
            QMessageBox.Cancel,
        )

        if ok == QMessageBox.Yes:
            LOG.warning("Shutdown by user.")
            QApplication.instance().quit()

    def do_undo(self) -> None:
        """ Undo the last marked consumption. """
        next(self.undo_loop).play()

        to_undo = self.undo_queue.pop()
        LOG.warning("Undoing consumption %s", to_undo)

        result = to_undo.reverse()

        if not result or not result.reversed:
            LOG.error("Reversed consumption %s but was not reversed!", to_undo)
            self.undo_queue.append(to_undo)

        elif not self.undo_queue:
            assert self.undo_action is not None
            self.undo_action.setDisabled(True)

        assert self.main_widget is not None
        self.main_widget.init_ui()

    @Slot(Consumption)
    def consumption_added(self, consumption):
        """ Mark an added consumption in the queue. """

        self.undo_queue.append(consumption)
        self.undo_action.setDisabled(False)

    @staticmethod
    def create_spacer() -> QWidget:
        """ Return an empty QWidget that automatically expands. """
        spacer = QWidget()
        spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
        return spacer

    icons_dir = os.path.join(os.path.dirname(__file__), "icons")

    def load_icon(self, filename: str) -> QIcon:
        """ Return a QtIcon loaded from the given `filename` in the icons
        directory. """

        if self.dark_theme:
            filename = "white_" + filename

        icon = QIcon(os.path.join(self.icons_dir, filename))
        return icon

    def play_plop(self) -> None:
        next(self.plop_loop).play()


def main() -> None:
    """ Main entry point of GUI client. """
    LOG.info("Loading piket_client")
    app = QApplication(sys.argv)

    # Set dark theme
    app.setStyleSheet(qdarkstyle.load_stylesheet_pyside2())

    # Enlarge font size
    font = app.font()
    size = font.pointSize()
    font.setPointSize(size * 1.5)
    app.setFont(font)

    # Test connectivity
    server_running = ServerStatus.is_server_running()

    if isinstance(server_running, NetworkError):
        LOG.critical("Could not connect to server, error %s", server_running.value)
        QMessageBox.critical(
            None,
            "Help er is iets kapot",
            "Kan niet starten omdat de server niet reageert, stuur een foto van "
            "dit naar Maarten: " + repr(server_running.value),
        )
        return

    # Load main window
    main_window = PiketMainWindow()

    # Test unsettled consumptions
    status = ServerStatus.unsettled_consumptions()
    assert not isinstance(status, NetworkError)

    unsettled = status.amount

    if unsettled > 0:
        assert status.first_timestamp is not None

        first = status.first_timestamp
        first_date = first.strftime("%c")
        ok = QMessageBox.information(
            None,
            "Onafgesloten lijst",
            f"Wil je verdergaan met een lijst met {unsettled} onafgesloten "
            f"consumpties sinds {first_date}?",
            QMessageBox.Yes,
            QMessageBox.No,
        )
        if ok == QMessageBox.No:
            main_window.show_keyboard()
            name, ok = QInputDialog.getText(
                None,
                "Lijst afsluiten",
                "Voer een naam in voor de lijst of druk op OK. Laat de datum staan.",
                QLineEdit.Normal,
                f"{first.strftime('%Y-%m-%d')}",
            )
            main_window.hide_keyboard()

            if name and ok:
                settlement = Settlement.create(name)
                info = [
                    f'{item["count"]} {item["name"]}'
                    for item in settlement.consumption_summary.values()
                ]
                info2 = ", ".join(info)
                QMessageBox.information(
                    None, "Lijst afgesloten", f"VO! Op deze lijst stonden: {info2}"
                )

                main_window = PiketMainWindow()

    main_window.show()

    # Let's go
    LOG.info("Starting QT event loop.")
    app.exec_()


if __name__ == "__main__":
    main()