import os
import json
import logging
from collections import defaultdict

from .vendor.Qt import QtWidgets, QtCore
from . import action

log = logging.getLogger(__name__)


class ScriptsMenu(QtWidgets.QMenu):
    """A Qt menu that displays a list of searchable actions"""

    updated = QtCore.Signal(QtWidgets.QMenu)

    def __init__(self, *args, **kwargs):
        """Initialize Scripts menu

        Args:
            title (str): the name of the root menu which will be created
        
            parent (QtWidgets.QObject) : the QObject to parent the menu to
        
        Returns:
            None

        """
        QtWidgets.QMenu.__init__(self, *args, **kwargs)

        self.searchbar = None
        self.update_action = None

        self._script_actions = []
        self._callbacks = defaultdict(list)

        # Automatically add it to the parent menu
        parent = kwargs.get("parent", None)
        if parent:
            parent.addMenu(self)

        objectname = kwargs.get("objectName", "scripts")
        title = kwargs.get("title", "Scripts")
        self.setObjectName(objectname)
        self.setTitle(title)

        # add default items in the menu
        self.create_default_items()

    def on_update(self):
        self.updated.emit(self)

    @property
    def registered_callbacks(self):
        return self._callbacks.copy()

    def create_default_items(self):
        """Add a search bar to the top of the menu given"""

        # create widget and link function
        searchbar = QtWidgets.QLineEdit()
        searchbar.setFixedWidth(120)
        searchbar.setPlaceholderText("Search ...")
        searchbar.textChanged.connect(self._update_search)
        self.searchbar = searchbar

        # create widget holder
        searchbar_action = QtWidgets.QWidgetAction(self)

        # add widget to widget holder
        searchbar_action.setDefaultWidget(self.searchbar)
        searchbar_action.setObjectName("Searchbar")

        # add update button and link function
        update_action = QtWidgets.QAction(self)
        update_action.setObjectName("Update Scripts")
        update_action.setText("Update Scripts")
        update_action.setVisible(False)
        update_action.triggered.connect(self.on_update)
        self.update_action = update_action

        # add action to menu
        self.addAction(searchbar_action)
        self.addAction(update_action)

        # add separator object
        separator = self.addSeparator()
        separator.setObjectName("separator")

    def add_menu(self, title, parent=None):
        """Create a sub menu for a parent widget

        Args:
            parent(QtWidgets.QWidget): the object to parent the menu to

            title(str): the title of the menu
        
        Returns:
             QtWidget.QMenu instance
        """

        if not parent:
            parent = self

        menu = QtWidgets.QMenu(parent, title)
        menu.setTitle(title)
        menu.setObjectName(title)
        menu.setTearOffEnabled(True)
        parent.addMenu(menu)

        return menu

    def add_script(self, parent, title, command, sourcetype, icon=None,
                   tags=None, label=None, tooltip=None):
        """Create an action item which runs a script when clicked

        Args:
            parent (QtWidget.QWidget): The widget to parent the item to

            title (str): The text which will be displayed in the menu

            command (str): The command which needs to be run when the item is
                           clicked.

            sourcetype (str): The type of command, the way the command is
                              processed is based on the source type.

            icon (str): The file path of an icon to display with the menu item

            tags (list, tuple): Keywords which describe the action

            label (str): A short description of the script which will be displayed
                         when hovering over the menu item

            tooltip (str): A tip for the user about the usage fo the tool

        Returns:
            QtWidget.QAction instance

        """

        assert tags is None or isinstance(tags, (list, tuple))
        # Ensure tags is a list
        tags = list() if tags is None else list(tags)
        tags.append(title.lower())

        assert icon is None or isinstance(icon, str), (
            "Invalid data type for icon, supported : None, string")

        # create new action
        script_action = action.Action(parent)
        script_action.setText(title)
        script_action.setObjectName(title)
        script_action.tags = tags

        # link action to root for callback library
        script_action.root = self

        # Set up the command
        script_action.sourcetype = sourcetype
        script_action.command = command

        try:
            script_action.process_command()
        except RuntimeError as e:
            raise RuntimeError("Script action can't be "
                               "processed: {}".format(e))

        if icon:
            iconfile = os.path.expandvars(icon)
            script_action.iconfile = iconfile
            script_action_icon = QtWidgets.QIcon(iconfile)
            script_action.setIcon(script_action_icon)

        if label:
            script_action.label = label

        if tooltip:
            script_action.setStatusTip(tooltip)

        script_action.triggered.connect(script_action.run_command)
        parent.addAction(script_action)

        # Add to our searchable actions
        self._script_actions.append(script_action)

        return script_action

    def build_from_configuration(self, parent, configuration):
        """Process the configurations and store the configuration

        This creates all submenus from a configuration.json file.

        When the configuration holds the key `main` all scripts under `main` will
        be added to the main menu first before adding the rest

        Args:
            parent (ScriptsMenu): script menu instance
            configuration (list): A ScriptsMenu configuration list

        Returns:
            None

        """

        for item in configuration:
            assert isinstance(item, dict), "Configuration is wrong!"

            # skip items which have no `type` key
            item_type = item.get('type', None)
            if not item_type:
                log.warning("Missing 'type' from configuration item")
                continue

            # add separator
            # Special behavior for separators
            if item_type == "separator":
                parent.addSeparator()

            # add submenu
            # items should hold a collection of submenu items (dict)
            elif item_type == "menu":
                assert "items" in item, "Menu is missing 'items' key"
                menu = self.add_menu(parent=parent, title=item["title"])
                self.build_from_configuration(menu, item["items"])

            # add script
            elif item_type == "action":
                # filter out `type` from the item dict
                config = {key: value for key, value in
                          item.items() if key != "type"}

                self.add_script(parent=parent, **config)

    def set_update_visible(self, state):
        self.update_action.setVisible(state)

    def clear_menu(self):
        """Clear all menu items which are not default

        Returns:
            None

        """

        # TODO: Set up a more robust implementation for this
        # Delete all except the first three actions
        for _action in self.actions()[3:]:
            self.removeAction(_action)

    def register_callback(self, modifiers, callback):
        self._callbacks[modifiers].append(callback)

    def _update_search(self, search):
        """Hide all the samples which do not match the user's import
        
        Returns:
            None

        """

        if not search:
            for action in self._script_actions:
                action.setVisible(True)
        else:
            for action in self._script_actions:
                if not action.has_tag(search.lower()):
                    action.setVisible(False)

        # Set visibility for all submenus
        for action in self.actions():
            if not action.menu():
                continue

            menu = action.menu()
            visible = any(action.isVisible() for action in menu.actions())
            action.setVisible(visible)


def load_configuration(path):
    """Load the configuration from a file

    Read out the JSON file which will dictate the structure of the scripts menu

    Args:
        path (str): file path of the .JSON file

    Returns:
        dict

    """

    if not os.path.isfile(path):
        raise AttributeError("Given configuration is not "
                             "a file!\n'{}'".format(path))

    extension = os.path.splitext(path)[-1]
    if extension != ".json":
        raise AttributeError("Given configuration file has unsupported "
                             "file type, provide a .json file")

    # retrieve and store config
    with open(path, "r") as f:
        configuration = json.load(f)

    return configuration


def application(configuration, parent):
    import sys
    app = QtWidgets.QApplication(sys.argv)

    scriptsmenu = ScriptsMenu(configuration, parent)
    scriptsmenu.show()

    sys.exit(app.exec_())
