from PyQt5.QtWidgets import QMainWindow, QApplication, QMessageBox, QFileDialog,\
                            QBoxLayout, QShortcut, QDialog, QCompleter
from PyQt5.QtGui import QKeySequence
from PyQt5.QtCore import QStringListModel, Qt

import deen.constants
from deen.gui.widgets.log import DeenLogger, DeenStatusConsole
from deen.gui.widgets.ui_deenmainwindow import Ui_MainWindow
from deen.gui.encoder import DeenEncoderWidget
from deen.gui.widgets.ui_deenfuzzysearch import Ui_DeenFuzzySearchWidget
from deen import logger

LOGGER = logger.DEEN_LOG.getChild('gui.widgets.core')


class FuzzySearchUi(QDialog):
    def __init__(self, parent):
        super(FuzzySearchUi, self).__init__(parent)
        self.ui = Ui_DeenFuzzySearchWidget()
        self.ui.setupUi(self)
        self.parent = parent


class DeenGui(QMainWindow):
    """The main window class that is the core of
    the Deen GUI. If is basically just the main
    window with a central element that includes
    one or more DeenEncoderWidget."""
    def __init__(self, parent=None, plugins=None, fullscreen=False):
        super(DeenGui, self).__init__(parent)
        self.ui = Ui_MainWindow()
        self.ui.setupUi(self)
        self.plugins = plugins
        self.widgets = []
        self.ui.actionLoad_from_file.triggered.connect(self.load_from_file)
        self.ui.actionQuit.triggered.connect(QApplication.quit)
        self.ui.actionAbout.triggered.connect(self.show_about)
        self.ui.actionStatus_console.triggered.connect(self.show_status_console)
        self.ui.actionTop_to_bottom.triggered.connect(self.set_widget_direction_toptobottom)
        self.ui.actionLeft_to_right.triggered.connect(self.set_widget_direction_lefttoright)
        # Set default direction
        self.set_widget_direction_toptobottom()
        self.ui.actionCopy_to_clipboard.triggered.connect(self.copy_content_to_clipboard)
        self.ui.actionSave_content_to_file.triggered.connect(self.save_widget_content_to_file)
        self.ui.actionSearch.triggered.connect(self.toggle_search_box_visibility)
        self.widgets.append(DeenEncoderWidget(self))
        for widget in self.widgets:
            self.ui.encoder_widget_layout.addWidget(widget)
        self.load_from_file_dialog = QFileDialog(self)
        self.setWindowTitle('deen')
        self.log = DeenLogger(self)
        self.widgets[0].set_field_focus()
        # Add action fuzzy search
        self.fuzzy_search_ui = FuzzySearchUi(self)
        self.fuzzy_search_action_shortcut = QShortcut(QKeySequence(Qt.CTRL | Qt.Key_R), self)
        self.fuzzy_search_action_shortcut.activated.connect(self.fuzzy_search_action)
        self.clear_current_widget_shortcut = QShortcut(QKeySequence(Qt.CTRL | Qt.Key_Q), self)
        self.clear_current_widget_shortcut.activated.connect(self.clear_current_widget)
        self.hide_search_box_shortcut = QShortcut(QKeySequence(Qt.CTRL | Qt.Key_F), self)
        self.hide_search_box_shortcut.activated.connect(self.toggle_search_box_visibility)
        self.next_encoder_widget_shortcut = QShortcut(QKeySequence(Qt.ALT | Qt.Key_Right), self)
        self.next_encoder_widget_shortcut.activated.connect(self.toggle_next_encoder_focus)
        self.prev_encoder_widget_shortcut = QShortcut(QKeySequence(Qt.ALT | Qt.Key_Left), self)
        self.prev_encoder_widget_shortcut.activated.connect(self.toggle_prev_encoder_focus)
        if fullscreen:
            self.showMaximized()
        self.show()

    def fuzzy_search_action(self):
        """Open a dialog for quick access to actions
        with fuzzy search."""
        focussed_widget = QApplication.focusWidget()
        self.fuzzy_search_ui.ui.fuzzy_search_field.setFocus()
        def get_data(model):
            plugins = [x[1].name for x in self.plugins.available_plugins]
            for p in self.plugins.codecs + \
                     self.plugins.compressions +\
                     self.plugins.assemblies:
                plugins.append('-' + p[1].name)
                plugins.extend(['-' + x for x in p[1].aliases])
            for p in self.plugins.available_plugins:
                plugins.extend(p[1].aliases)
            model.setStringList(plugins)
        completer = QCompleter()
        self.fuzzy_search_ui.ui.fuzzy_search_field.setCompleter(completer)
        model = QStringListModel()
        completer.setModel(model)
        get_data(model)
        if self.fuzzy_search_ui.exec_() == 0:
            return
        search_data = self.fuzzy_search_ui.ui.fuzzy_search_field.text()
        parent_encoder = self.get_parent_encoder(focussed_widget)
        if parent_encoder:
            parent_encoder.action_fuzzy(search_data)
        else:
            LOGGER.error('Unable to find parent encoder for ' + str(focussed_widget))

    def toggle_search_box_visibility(self):
        """Toggle the search box visibility
        via a shortcut for the current encoder
        widget."""
        focussed_widget = QApplication.focusWidget()
        parent_encoder = self.get_parent_encoder(focussed_widget)
        if parent_encoder:
            parent_encoder.toggle_search_box_visibility()
        else:
            LOGGER.error('Unable to find parent encoder for ' + str(focussed_widget))

    def get_parent_encoder(self, widget):
        """A wrapper function that returns the
        parent encoder widget for a given widget
        retrieved via QApplication.focusWidget().
        Can be used on signal receivers to
        reference the current encoder widget."""
        while not isinstance(widget, DeenEncoderWidget):
            # Builin clases may implement
            # parent() to retrieve the
            # parent object.
            if not widget:
                break
            if callable(widget.parent):
                widget = widget.parent()
            else:
                widget = widget.parent
            if isinstance(widget, DeenGui):
                return False
        return widget

    def toggle_next_encoder_focus(self):
        """Focus the next encoder widget."""
        focussed_widget = QApplication.focusWidget()
        parent_encoder = self.get_parent_encoder(focussed_widget)
        if parent_encoder:
            if parent_encoder.has_next():
                parent_encoder.next.field.setFocus()
                self.ui.DeenMainWindow.ensureWidgetVisible(parent_encoder.next)
        else:
            LOGGER.error('Unable to find parent encoder for ' + str(focussed_widget))

    def toggle_prev_encoder_focus(self):
        """Focus the previous encoder widget."""
        focussed_widget = QApplication.focusWidget()
        parent_encoder = self.get_parent_encoder(focussed_widget)
        if parent_encoder:
            if parent_encoder.has_previous():
                parent_encoder.previous.field.setFocus()
                self.ui.DeenMainWindow.ensureWidgetVisible(parent_encoder.previous)
        else:
            LOGGER.error('Unable to find parent encoder for ' + str(focussed_widget))

    def clear_current_widget(self):
        """Clear and remove the current encoder widget."""
        focussed_widget = QApplication.focusWidget()
        if not hasattr(focussed_widget, 'parent') or \
                not focussed_widget.parent:
            LOGGER.warning('NO parent for widget found: ' + str(focussed_widget))
            return
        if callable(focussed_widget.parent):
            widget = focussed_widget.parent()
        else:
            widget = focussed_widget.parent
        if isinstance(widget, DeenEncoderWidget):
            widget.clear_content()

    def set_root_content(self, data):
        if data:
            if isinstance(data, (str, bytes)):
                data = bytearray(data)
            self.widgets[0].content = data

    def set_widget_direction_toptobottom(self):
        self.ui.encoder_widget_layout.setDirection(QBoxLayout.TopToBottom)

    def set_widget_direction_lefttoright(self):
        self.ui.encoder_widget_layout.setDirection(QBoxLayout.LeftToRight)

    def show_about(self):
        about = QMessageBox(self)
        about.setWindowTitle('About')
        about.setText(deen.constants.about_text)
        about.resize(100, 75)
        about.show()

    def show_status_console(self):
        status = DeenStatusConsole(self)
        status.setWindowTitle('Status Console')
        status.resize(600, 400)
        status.console.show()
        status.show()

    def show_error_msg(self, error_msg, parent=None):
        """Generic message box for displaying any
        kind of error message from GUI elements."""
        widget = parent or self
        dialog = QMessageBox(widget)
        dialog.setIcon(QMessageBox.Critical)
        dialog.setWindowTitle('Error')
        dialog.setText(error_msg)
        dialog.show()

    def load_from_file(self, file_name=None):
        if file_name:
            name = file_name
        else:
            name = self.load_from_file_dialog.getOpenFileName(
                        self.load_from_file_dialog, 'Load from File')
        if not name or not name[0]:
            return
        if isinstance(name, tuple):
            name = name[0]
        with open(name, 'rb') as file:
            content = file.read()
        if content:
            self.widgets[0].clear_content()
            self.widgets[0].content = bytearray(content)
            try:
                content = content.decode('utf8')
            except UnicodeDecodeError:
                content = content.decode('utf8', errors='replace')
                LOGGER.warning('Failed to decode file content')
            self.widgets[0].text_field.setPlainText(content)
        self.widgets[0].hex_field.setHidden(True)
        self.widgets[0].text_field.setHidden(False)

    def save_widget_content_to_file(self, file_name=None):
        """Save the content of the current widget
        to a file."""
        focussed_widget = QApplication.focusWidget()
        parent_encoder = self.get_parent_encoder(focussed_widget)
        if not parent_encoder._content:
            return
        fd = QFileDialog(parent_encoder)
        if file_name:
            name = file_name
        else:
            name = fd.getSaveFileName(fd, 'Save File')
        if not name or not name[0]:
            return
        if isinstance(name, tuple):
            name = name[0]
        with open(name, 'wb') as file:
            current_plugin = parent_encoder.plugin
            file.write(parent_encoder._content)

    def copy_content_to_clipboard(self):
        focussed_widget = QApplication.focusWidget()
        parent_encoder = self.get_parent_encoder(focussed_widget)
        if not parent_encoder._content:
            return
        try:
            content = parent_encoder._content.decode('utf8')
        except UnicodeDecodeError as e:
            parent_encoder.log.error('Cannot copy non-ASCII content to clipboard')
            parent_encoder.log.debug(e, exc_info=True)
            return
        clipboard = QApplication.clipboard()
        clipboard.setText(content)