#!/usr/bin python3
""" The Menu Bars for faceswap GUI """

import locale
import logging
import os
import sys
import tkinter as tk
from tkinter import ttk
import webbrowser

from importlib import import_module
from subprocess import Popen, PIPE, STDOUT

from lib.multithreading import MultiThread
from lib.serializer import get_serializer
import update_deps

from .popup_configure import popup_config
from .custom_widgets import Tooltip
from .utils import get_config, get_images

_RESOURCES = [("faceswap.dev - Guides and Forum", "https://www.faceswap.dev"),
              ("Patreon - Support this project", "https://www.patreon.com/faceswap"),
              ("Discord - The FaceSwap Discord server", "https://discord.gg/VasFUAy"),
              ("Github - Our Source Code", "https://github.com/deepfakes/faceswap")]

_CONFIG_FILES = []
_CONFIGS = dict()
_WORKING_DIR = os.path.dirname(os.path.realpath(sys.argv[0]))


logger = logging.getLogger(__name__)  # pylint: disable=invalid-name


class MainMenuBar(tk.Menu):  # pylint:disable=too-many-ancestors
    """ GUI Main Menu Bar """
    def __init__(self, master=None):
        logger.debug("Initializing %s", self.__class__.__name__)
        super().__init__(master)
        self.root = master

        self.file_menu = FileMenu(self)
        self.settings_menu = SettingsMenu(self)
        self.help_menu = HelpMenu(self)

        self.add_cascade(label="File", menu=self.file_menu, underline=0)
        self.add_cascade(label="Settings", menu=self.settings_menu, underline=0)
        self.add_cascade(label="Help", menu=self.help_menu, underline=0)
        logger.debug("Initialized %s", self.__class__.__name__)


class SettingsMenu(tk.Menu):  # pylint:disable=too-many-ancestors
    """ Settings menu items and functions """
    def __init__(self, parent):
        logger.debug("Initializing %s", self.__class__.__name__)
        super().__init__(parent, tearoff=0)
        self.root = parent.root
        self.configs = self.scan_for_plugin_configs()
        self.build()
        logger.debug("Initialized %s", self.__class__.__name__)

    def scan_for_plugin_configs(self):
        """ Scan for config.ini file locations """
        global _CONFIGS, _CONFIG_FILES  # pylint:disable=global-statement
        root_path = os.path.abspath(os.path.dirname(sys.argv[0]))
        plugins_path = os.path.join(root_path, "plugins")
        logger.debug("Scanning path: '%s'", plugins_path)
        configs = dict()
        for dirpath, _, filenames in os.walk(plugins_path):
            if "_config.py" in filenames:
                plugin_type = os.path.split(dirpath)[-1]
                config = self.load_config(plugin_type)
                configs[plugin_type] = config
        logger.debug("Configs loaded: %s", sorted(list(configs.keys())))
        keys = list(configs.keys())
        for key in ("extract", "train", "convert"):
            if key in keys:
                _CONFIG_FILES.append(keys.pop(keys.index(key)))
        _CONFIG_FILES.extend([key for key in sorted(keys)])
        _CONFIGS = configs
        return configs

    @staticmethod
    def load_config(plugin_type):
        """ Load the config to generate config file if it doesn't exist and get filename """
        # Load config to generate default if doesn't exist
        mod = ".".join(("plugins", plugin_type, "_config"))
        module = import_module(mod)
        config = module.Config(None)
        logger.debug("Found '%s' config at '%s'", plugin_type, config.configfile)
        return config

    def build(self):
        """ Add the settings menu to the menu bar """
        # pylint: disable=cell-var-from-loop
        logger.debug("Building settings menu")
        for name in _CONFIG_FILES:
            label = "Configure {} Plugins...".format(name.title())
            config = self.configs[name]
            self.add_command(
                label=label,
                underline=10,
                command=lambda conf=(name, config), root=self.root: popup_config(conf, root))
        self.add_separator()
        conf = get_config().user_config
        self.add_command(
            label="GUI Settings...",
            underline=10,
            command=lambda conf=("GUI", conf), root=self.root: popup_config(conf, root))
        logger.debug("Built settings menu")


class FileMenu(tk.Menu):  # pylint:disable=too-many-ancestors
    """ File menu items and functions """
    def __init__(self, parent):
        logger.debug("Initializing %s", self.__class__.__name__)
        super().__init__(parent, tearoff=0)
        self.root = parent.root
        self._config = get_config()
        self.recent_menu = tk.Menu(self, tearoff=0, postcommand=self.refresh_recent_menu)
        self.build()
        logger.debug("Initialized %s", self.__class__.__name__)

    def build(self):
        """ Add the file menu to the menu bar """
        logger.debug("Building File menu")
        self.add_command(label="New Project...",
                         underline=0,
                         accelerator="Ctrl+N",
                         command=self._config.project.new)
        self.root.bind_all("<Control-n>", self._config.project.new)
        self.add_command(label="Open Project...",
                         underline=0,
                         accelerator="Ctrl+O",
                         command=self._config.project.load)
        self.root.bind_all("<Control-o>", self._config.project.load)
        self.add_command(label="Save Project",
                         underline=0,
                         accelerator="Ctrl+S",
                         command=lambda: self._config.project.save(save_as=False))
        self.root.bind_all("<Control-s>", lambda e: self._config.project.save(e, save_as=False))
        self.add_command(label="Save Project as...",
                         underline=13,
                         accelerator="Ctrl+Alt+S",
                         command=lambda: self._config.project.save(save_as=True))
        self.root.bind_all("<Control-Alt-s>", lambda e: self._config.project.save(e, save_as=True))
        self.add_command(label="Reload Project from Disk",
                         underline=0,
                         accelerator="F5",
                         command=self._config.project.reload)
        self.root.bind_all("<F5>", self._config.project.reload)
        self.add_command(label="Close Project",
                         underline=0,
                         accelerator="Ctrl+W",
                         command=self._config.project.close)
        self.root.bind_all("<Control-w>", self._config.project.close)
        self.add_separator()
        self.add_command(label="Open Task...",
                         underline=5,
                         accelerator="Ctrl+Alt+T",
                         command=lambda: self._config.tasks.load(current_tab=False))
        self.root.bind_all("<Control-Alt-t>",
                           lambda e: self._config.tasks.load(e, current_tab=False))
        self.add_separator()
        self.add_cascade(label="Open recent", underline=6, menu=self.recent_menu)
        self.add_separator()
        self.add_command(label="Quit",
                         underline=0,
                         accelerator="Alt+F4",
                         command=self.root.close_app)
        self.root.bind_all("<Alt-F4>", self.root.close_app)
        logger.debug("Built File menu")

    def build_recent_menu(self):
        """ Load recent files into menu bar """
        logger.debug("Building Recent Files menu")
        serializer = get_serializer("json")
        menu_file = os.path.join(self._config.pathcache, ".recent.json")
        if not os.path.isfile(menu_file) or os.path.getsize(menu_file) == 0:
            self.clear_recent_files(serializer, menu_file)
        recent_files = serializer.load(menu_file)
        logger.debug("Loaded recent files: %s", recent_files)
        removed_files = []
        for recent_item in recent_files:
            filename, command = recent_item
            if not os.path.isfile(filename):
                logger.debug("File does not exist. Flagging for removal: '%s'", filename)
                removed_files.append(recent_item)
                continue
            # Legacy project files didn't have a command stored
            command = command if command else "project"
            logger.debug("processing: ('%s', %s)", filename, command)
            if command.lower() == "project":
                load_func = self._config.project.load
                lbl = command
                kwargs = dict(filename=filename)
            else:
                load_func = self._config.tasks.load
                lbl = "{} Task".format(command)
                kwargs = dict(filename=filename, current_tab=False)
            self.recent_menu.add_command(
                label="{} ({})".format(filename, lbl.title()),
                command=lambda kw=kwargs, fn=load_func: fn(**kw))
        if removed_files:
            for recent_item in removed_files:
                logger.debug("Removing from recent files: `%s`", recent_item[0])
                recent_files.remove(recent_item)
            serializer.save(menu_file, recent_files)
        self.recent_menu.add_separator()
        self.recent_menu.add_command(
            label="Clear recent files",
            underline=0,
            command=lambda srl=serializer, mnu=menu_file: self.clear_recent_files(srl, mnu))

        logger.debug("Built Recent Files menu")

    @staticmethod
    def clear_recent_files(serializer, menu_file):
        """ Creates or clears recent file list """
        logger.debug("clearing recent files list: '%s'", menu_file)
        serializer.save(menu_file, list())

    def refresh_recent_menu(self):
        """ Refresh recent menu on save/load of files """
        self.recent_menu.delete(0, "end")
        self.build_recent_menu()


class HelpMenu(tk.Menu):  # pylint:disable=too-many-ancestors
    """ Help menu items and functions """
    def __init__(self, parent):
        logger.debug("Initializing %s", self.__class__.__name__)
        super().__init__(parent, tearoff=0)
        self.root = parent.root
        self.recources_menu = tk.Menu(self, tearoff=0)
        self._branches_menu = tk.Menu(self, tearoff=0)
        self.build()
        logger.debug("Initialized %s", self.__class__.__name__)

    def build(self):
        """ Build the help menu """
        logger.debug("Building Help menu")

        self.add_command(label="Check for updates...",
                         underline=0,
                         command=lambda action="check": self.in_thread(action))
        self.add_command(label="Update Faceswap...",
                         underline=0,
                         command=lambda action="update": self.in_thread(action))
        if self._build_branches_menu():
            self.add_cascade(label="Switch Branch", underline=7, menu=self._branches_menu)
        self.add_separator()
        self._build_recources_menu()
        self.add_cascade(label="Resources", underline=0, menu=self.recources_menu)
        self.add_separator()
        self.add_command(label="Output System Information",
                         underline=0,
                         command=lambda action="output_sysinfo": self.in_thread(action))
        logger.debug("Built help menu")

    def _build_branches_menu(self):
        """ Build branch selection menu.

        Queries git for available branches and builds a menu based on output.

        Returns
        -------
        bool
            ``True`` if menu was successfully built otherwise ``False``
        """
        stdout = self._get_branches()
        if stdout is None:
            return False

        branches = self._filter_branches(stdout)
        if not branches:
            return False

        for branch in branches:
            self._branches_menu.add_command(
                label=branch,
                command=lambda b=branch: self._switch_branch(b))
        return True

    @staticmethod
    def _get_branches():
        """ Get the available github branches

        Returns
        -------
        str
            The list of branches available. If no branches were found or there was an
            error then `None` is returned
        """
        gitcmd = "git branch -a"
        cmd = Popen(gitcmd, shell=True, stdout=PIPE, stderr=STDOUT, cwd=_WORKING_DIR)
        stdout, _ = cmd.communicate()
        retcode = cmd.poll()
        if retcode != 0:
            logger.debug("Unable to list git branches. return code: %s, message: %s",
                         retcode, stdout.decode().strip().replace("\n", " - "))
            return None
        return stdout.decode(locale.getpreferredencoding())

    @staticmethod
    def _filter_branches(stdout):
        """ Filter the branches, remove duplicates and the current branch and return a sorted
        list.

        Parameters
        ----------
        stdout: str
            The output from the git branch query converted to a string

        Returns
        -------
        list
            Unique list of available branches sorted in alphabetical order
        """
        current = None
        branches = set()
        for line in stdout.splitlines():
            branch = line[line.rfind("/") + 1:] if "/" in line else line.strip()
            if branch.startswith("*"):
                branch = branch.replace("*", "").strip()
                current = branch
                continue
            branches.add(branch)
        logger.debug("Found branches: %s", branches)
        if current in branches:
            logger.debug("Removing current branch from output: %s", current)
            branches.remove(current)

        branches = sorted(list(branches), key=str.casefold)
        logger.debug("Final branches: %s", branches)
        return branches

    @staticmethod
    def _switch_branch(branch):
        """ Change the currently checked out branch, and return a notification.

        Parameters
        ----------
        str
            The branch to switch to
        """
        logger.info("Switching branch to '%s'...", branch)
        gitcmd = "git checkout {}".format(branch)
        cmd = Popen(gitcmd, shell=True, stdout=PIPE, stderr=STDOUT, cwd=_WORKING_DIR)
        stdout, _ = cmd.communicate()
        retcode = cmd.poll()
        if retcode != 0:
            logger.error("Unable to switch branch. return code: %s, message: %s",
                         retcode, stdout.decode().strip().replace("\n", " - "))
            return
        logger.info("Succesfully switched to '%s'. You may want to check for updates to make sure "
                    "that you have the latest code.", branch)
        logger.info("Please restart Faceswap to complete the switch.")

    def _build_recources_menu(self):
        """ Build resources menu """
        # pylint: disable=cell-var-from-loop
        logger.debug("Building Resources Files menu")
        for resource in _RESOURCES:
            self.recources_menu.add_command(
                label=resource[0],
                command=lambda link=resource[1]: webbrowser.open_new(link))
        logger.debug("Built resources menu")

    def in_thread(self, action):
        """ Perform selected action inside a thread """
        logger.debug("Performing help action: %s", action)
        thread = MultiThread(getattr(self, action), thread_count=1)
        thread.start()
        logger.debug("Performed help action: %s", action)

    @staticmethod
    def clear_console():
        """ Clear the console window """
        get_config().tk_vars["consoleclear"].set(True)

    def output_sysinfo(self):
        """ Output system information to console """
        logger.debug("Obtaining system information")
        self.root.config(cursor="watch")
        self.clear_console()
        try:
            from lib.sysinfo import sysinfo
            info = sysinfo
        except Exception as err:  # pylint:disable=broad-except
            info = "Error obtaining system info: {}".format(str(err))
        self.clear_console()
        logger.debug("Obtained system information: %s", info)
        print(info)
        self.root.config(cursor="")

    def check(self):
        """ Check for updates and clone repository """
        logger.debug("Checking for updates...")
        self.root.config(cursor="watch")
        encoding = locale.getpreferredencoding()
        logger.debug("Encoding: %s", encoding)
        self.check_for_updates(encoding, check=True)
        self.root.config(cursor="")

    def update(self):
        """ Check for updates and clone repository """
        logger.debug("Updating Faceswap...")
        self.root.config(cursor="watch")
        encoding = locale.getpreferredencoding()
        logger.debug("Encoding: %s", encoding)
        success = False
        if self.check_for_updates(encoding):
            success = self.do_update(encoding)
        update_deps.main(logger=logger)
        if success:
            logger.info("Please restart Faceswap to complete the update.")
        self.root.config(cursor="")

    @staticmethod
    def check_for_updates(encoding, check=False):
        """ Check whether an update is required """
        # Do the check
        logger.info("Checking for updates...")
        update = False
        msg = ""
        gitcmd = "git remote update && git status -uno"
        cmd = Popen(gitcmd, shell=True, stdout=PIPE, stderr=STDOUT, cwd=_WORKING_DIR)
        stdout, _ = cmd.communicate()
        retcode = cmd.poll()
        if retcode != 0:
            msg = ("Git is not installed or you are not running a cloned repo. "
                   "Unable to check for updates")
        else:
            chk = stdout.decode(encoding).splitlines()
            for line in chk:
                if line.lower().startswith("your branch is ahead"):
                    msg = "Your branch is ahead of the remote repo. Not updating"
                    break
                if line.lower().startswith("your branch is up to date"):
                    msg = "Faceswap is up to date."
                    break
                if line.lower().startswith("your branch is behind"):
                    msg = "There are updates available"
                    update = True
                    break
                if "have diverged" in line.lower():
                    msg = "Your branch has diverged from the remote repo. Not updating"
                    break
        if not update or check:
            logger.info(msg)
        logger.debug("Checked for update. Update required: %s", update)
        return update

    @staticmethod
    def do_update(encoding):
        """ Update Faceswap """
        logger.info("A new version is available. Updating...")
        gitcmd = "git pull"
        cmd = Popen(gitcmd, shell=True, stdout=PIPE, stderr=STDOUT, bufsize=1, cwd=_WORKING_DIR)
        while True:
            output = cmd.stdout.readline().decode(encoding)
            if output == "" and cmd.poll() is not None:
                break
            if output:
                logger.debug("'%s' output: '%s'", gitcmd, output.strip())
                print(output.strip())
        retcode = cmd.poll()
        logger.debug("'%s' returncode: %s", gitcmd, retcode)
        if retcode != 0:
            logger.info("An error occurred during update. return code: %s", retcode)
            retval = False
        else:
            retval = True
        return retval


class TaskBar(ttk.Frame):  # pylint: disable=too-many-ancestors
    """ Task bar buttons """
    def __init__(self, parent):
        super().__init__(parent)
        self._config = get_config()
        self.pack(side=tk.TOP, anchor=tk.W, fill=tk.X, expand=False)
        self._btn_frame = ttk.Frame(self)
        self._btn_frame.pack(side=tk.TOP, pady=2, anchor=tk.W, fill=tk.X, expand=False)

        self._project_btns()
        self._group_separator()
        self._task_btns()
        self._group_separator()
        self._settings_btns()
        self._section_separator()

    def _project_btns(self):
        frame = ttk.Frame(self._btn_frame)
        frame.pack(side=tk.LEFT, anchor=tk.W, expand=False, padx=2)

        for btntype in ("new", "load", "save", "save_as", "reload"):
            logger.debug("Adding button: '%s'", btntype)

            loader, kwargs = self._loader_and_kwargs(btntype)
            cmd = getattr(self._config.project, loader)
            btn = ttk.Button(frame,
                             image=get_images().icons[btntype],
                             command=lambda fn=cmd, kw=kwargs: fn(**kw))
            btn.pack(side=tk.LEFT, anchor=tk.W)
            hlp = self.set_help(btntype)
            Tooltip(btn, text=hlp, wraplength=200)

    def _task_btns(self):
        frame = ttk.Frame(self._btn_frame)
        frame.pack(side=tk.LEFT, anchor=tk.W, expand=False, padx=2)

        for loadtype in ("load", "save", "save_as", "clear", "reload"):
            btntype = "{}2".format(loadtype)
            logger.debug("Adding button: '%s'", btntype)

            loader, kwargs = self._loader_and_kwargs(loadtype)
            if loadtype == "load":
                kwargs["current_tab"] = True
            cmd = getattr(self._config.tasks, loader)
            btn = ttk.Button(
                frame,
                image=get_images().icons[btntype],
                command=lambda fn=cmd, kw=kwargs: fn(**kw))
            btn.pack(side=tk.LEFT, anchor=tk.W)
            hlp = self.set_help(btntype)
            Tooltip(btn, text=hlp, wraplength=200)

    @staticmethod
    def _loader_and_kwargs(btntype):
        if btntype == "save":
            loader = btntype
            kwargs = dict(save_as=False)
        elif btntype == "save_as":
            loader = "save"
            kwargs = dict(save_as=True)
        else:
            loader = btntype
            kwargs = dict()
        logger.debug("btntype: %s, loader: %s, kwargs: %s", btntype, loader, kwargs)
        return loader, kwargs

    def _settings_btns(self):
        # pylint: disable=cell-var-from-loop
        frame = ttk.Frame(self._btn_frame)
        frame.pack(side=tk.LEFT, anchor=tk.W, expand=False, padx=2)
        root = get_config().root
        for name in _CONFIG_FILES:
            config = _CONFIGS[name]
            btntype = "settings_{}".format(name)
            btntype = btntype if btntype in get_images().icons else "settings"
            logger.debug("Adding button: '%s'", btntype)
            btn = ttk.Button(
                frame,
                image=get_images().icons[btntype],
                command=lambda conf=(name, config), root=root: popup_config(conf, root))
            btn.pack(side=tk.LEFT, anchor=tk.W)
            hlp = "Configure {} settings...".format(name.title())
            Tooltip(btn, text=hlp, wraplength=200)

    @staticmethod
    def set_help(btntype):
        """ Set the helptext for option buttons """
        logger.debug("Setting help")
        hlp = ""
        task = "currently selected Task" if btntype[-1] == "2" else "Project"
        if btntype.startswith("reload"):
            hlp = "Reload {} from disk".format(task)
        if btntype == "new":
            hlp = "Create a new {}...".format(task)
        if btntype.startswith("clear"):
            hlp = "Reset {} to default".format(task)
        elif btntype.startswith("save") and "_" not in btntype:
            hlp = "Save {}".format(task)
        elif btntype.startswith("save_as"):
            hlp = "Save {} as...".format(task)
        elif btntype.startswith("load"):
            msg = task
            if msg.endswith("Task"):
                msg += " from a task or project file"
            hlp = "Load {}...".format(msg)
        return hlp

    def _group_separator(self):
        separator = ttk.Separator(self._btn_frame, orient="vertical")
        separator.pack(padx=(2, 1), fill=tk.Y, side=tk.LEFT)

    def _section_separator(self):
        frame = ttk.Frame(self)
        frame.pack(side=tk.BOTTOM, fill=tk.X)
        separator = ttk.Separator(frame, orient="horizontal")
        separator.pack(fill=tk.X, side=tk.LEFT, expand=True)