"""
Archipelago launcher for bundled app.

* if run with APBP as argument, launch corresponding client.
* if run with executable as argument, run it passing argv[2:] as arguments
* if run without arguments, open launcher GUI

Scroll down to components= to add components to the launcher as well as setup.py
"""


import argparse
from os.path import isfile
import sys
from typing import Iterable, Sequence, Callable, Union, Optional
import subprocess
import itertools
from Utils import is_frozen, user_path, local_path, init_logging
from shutil import which
import shlex
from enum import Enum, auto
import logging


is_linux = sys.platform.startswith('linux')
is_macos = sys.platform == 'darwin'
is_windows = sys.platform in ("win32", "cygwin", "msys")


def open_host_yaml():
    file = user_path('host.yaml')
    if is_linux:
        exe = which('sensible-editor') or which('gedit') or \
              which('xdg-open') or which('gnome-open') or which('kde-open')
        subprocess.Popen([exe, file])
    elif is_macos:
        exe = which("open")
        subprocess.Popen([exe, file])
    else:
        import webbrowser
        webbrowser.open(file)


def open_patch():
    try:
        import tkinter
        import tkinter.filedialog
    except Exception as e:
        logging.error("Could not load tkinter, which is likely not installed. "
                      "This attempt was made because Launcher.open_patch was used.")
        raise e
    else:
        root = tkinter.Tk()
        root.withdraw()
        suffixes = []
        for c in components:
            if isfile(get_exe(c)[-1]):
                suffixes += c.file_identifier.suffixes if c.type == Type.CLIENT and \
                                                          isinstance(c.file_identifier, SuffixIdentifier) else []
        filename = tkinter.filedialog.askopenfilename(filetypes=(('Patches', ' '.join(suffixes)),))
        file, _, component = identify(filename)
        if file and component:
            launch([*get_exe(component), file], component.cli)


def browse_files():
    file = user_path()
    if is_linux:
        exe = which('xdg-open') or which('gnome-open') or which('kde-open')
        subprocess.Popen([exe, file])
    elif is_macos:
        exe = which("open")
        subprocess.Popen([exe, file])
    else:
        import webbrowser
        webbrowser.open(file)


class Type(Enum):
    TOOL = auto()
    FUNC = auto()  # not a real component
    CLIENT = auto()
    ADJUSTER = auto()


class SuffixIdentifier:
    suffixes: Iterable[str]

    def __init__(self, *args: str):
        self.suffixes = args

    def __call__(self, path: str):
        if isinstance(path, str):
            for suffix in self.suffixes:
                if path.endswith(suffix):
                    return True
        return False


class Component:
    display_name: str
    type: Optional[Type]
    script_name: Optional[str]
    frozen_name: Optional[str]
    icon: str  # just the name, no suffix
    cli: bool
    func: Optional[Callable]
    file_identifier: Optional[Callable[[str], bool]]

    def __init__(self, display_name: str, script_name: Optional[str] = None, frozen_name: Optional[str] = None,
                 cli: bool = False, icon: str = 'icon', component_type: Type = None, func: Optional[Callable] = None,
                 file_identifier: Optional[Callable[[str], bool]] = None):
        self.display_name = display_name
        self.script_name = script_name
        self.frozen_name = frozen_name or f'Archipelago{script_name}' if script_name else None
        self.icon = icon
        self.cli = cli
        self.type = component_type or \
            None if not display_name else \
            Type.FUNC if func else \
            Type.CLIENT if 'Client' in display_name else \
            Type.ADJUSTER if 'Adjuster' in display_name else Type.TOOL
        self.func = func
        self.file_identifier = file_identifier

    def handles_file(self, path: str):
        return self.file_identifier(path) if self.file_identifier else False


components: Iterable[Component] = (
    # Launcher
    Component('', 'Launcher'),
    # Core
    Component('Host', 'MultiServer', 'ArchipelagoServer', cli=True,
              file_identifier=SuffixIdentifier('.archipelago', '.zip')),
    Component('Generate', 'Generate', cli=True),
    Component('Text Client', 'CommonClient', 'ArchipelagoTextClient'),
    # SNI
    Component('SNI Client', 'SNIClient',
              file_identifier=SuffixIdentifier('.apz3', '.apm3', '.apsoe', '.aplttp', '.apsm', '.apsmz3')),
    Component('LttP Adjuster', 'LttPAdjuster'),
    # Factorio
    Component('Factorio Client', 'FactorioClient'),
    # Minecraft
    Component('Minecraft Client', 'MinecraftClient', icon='mcicon', cli=True,
              file_identifier=SuffixIdentifier('.apmc')),
    # Ocarina of Time
    Component('OoT Client', 'OoTClient',
              file_identifier=SuffixIdentifier('.apz5')),
    Component('OoT Adjuster', 'OoTAdjuster'),
    # FF1
    Component('FF1 Client', 'FF1Client'),
    # ChecksFinder
    Component('ChecksFinder Client', 'ChecksFinderClient'),
    # Functions
    Component('Open host.yaml', func=open_host_yaml),
    Component('Open Patch', func=open_patch),
    Component('Browse Files', func=browse_files),
)
icon_paths = {
    'icon': local_path('data', 'icon.ico' if is_windows else 'icon.png'),
    'mcicon': local_path('data', 'mcicon.ico')
}


def identify(path: Union[None, str]):
    if path is None:
        return None, None, None
    for component in components:
        if component.handles_file(path):
            return path, component.script_name, component
    return (None, None, None) if '/' in path or '\\' in path else (None, path, None)


def get_exe(component: Union[str, Component]) -> Optional[Sequence[str]]:
    if isinstance(component, str):
        name = component
        component = None
        if name.startswith('Archipelago'):
            name = name[11:]
        if name.endswith('.exe'):
            name = name[:-4]
        if name.endswith('.py'):
            name = name[:-3]
        if not name:
            return None
        for c in components:
            if c.script_name == name or c.frozen_name == f'Archipelago{name}':
                component = c
                break
        if not component:
            return None
    if is_frozen():
        suffix = '.exe' if is_windows else ''
        return [local_path(f'{component.frozen_name}{suffix}')]
    else:
        return [sys.executable, local_path(f'{component.script_name}.py')]


def launch(exe, in_terminal=False):
    if in_terminal:
        if is_windows:
            subprocess.Popen(['start', *exe], shell=True)
            return
        elif is_linux:
            terminal = which('x-terminal-emulator') or which('gnome-terminal') or which('xterm')
            if terminal:
                subprocess.Popen([terminal, '-e', shlex.join(exe)])
                return
        elif is_macos:
            terminal = [which('open'), '-W', '-a', 'Terminal.app']
            subprocess.Popen([*terminal, *exe])
            return
    subprocess.Popen(exe)


def run_gui():
    if not sys.stdout:
        from kvui import App, ContainerLayout, GridLayout, Button, Label  # this kills stdout
    else:
        from kivy.app import App
        from kivy.uix.button import Button
        from kivy.uix.floatlayout import FloatLayout as ContainerLayout
        from kivy.uix.gridlayout import GridLayout
        from kivy.uix.label import Label

    class Launcher(App):
        base_title: str = "Archipelago Launcher"
        container: ContainerLayout
        grid: GridLayout

        _tools = {c.display_name: c for c in components if c.type == Type.TOOL and isfile(get_exe(c)[-1])}
        _clients = {c.display_name: c for c in components if c.type == Type.CLIENT and isfile(get_exe(c)[-1])}
        _adjusters = {c.display_name: c for c in components if c.type == Type.ADJUSTER and isfile(get_exe(c)[-1])}
        _funcs = {c.display_name: c for c in components if c.type == Type.FUNC}

        def __init__(self, ctx=None):
            self.title = self.base_title
            self.ctx = ctx
            self.icon = r"data/icon.png"
            super().__init__()

        def build(self):
            self.container = ContainerLayout()
            self.grid = GridLayout(cols=2)
            self.container.add_widget(self.grid)

            button_layout = self.grid  # make buttons fill the window
            for (tool, client) in itertools.zip_longest(itertools.chain(
                    self._tools.items(), self._funcs.items(), self._adjusters.items()), self._clients.items()):
                # column 1
                if tool:
                    button = Button(text=tool[0])
                    button.component = tool[1]
                    button.bind(on_release=self.component_action)
                    button_layout.add_widget(button)
                else:
                    button_layout.add_widget(Label())
                # column 2
                if client:
                    button = Button(text=client[0])
                    button.component = client[1]
                    button.bind(on_press=self.component_action)
                    button_layout.add_widget(button)
                else:
                    button_layout.add_widget(Label())

            return self.container

        @staticmethod
        def component_action(button):
            if button.component.type == Type.FUNC:
                button.component.func()
            else:
                launch(get_exe(button.component), button.component.cli)

    Launcher().run()


def main(args: Optional[Union[argparse.Namespace, dict]] = None):
    if isinstance(args, argparse.Namespace):
        args = {k: v for k, v in args._get_kwargs()}
    elif not args:
        args = {}

    if "Patch|Game|Component" in args:
        file, component, _ = identify(args["Patch|Game|Component"])
        if file:
            args['file'] = file
        if component:
            args['component'] = component

    if 'file' in args:
        subprocess.run([*get_exe(args['component']), args['file'], *args['args']])
    elif 'component' in args:
        subprocess.run([*get_exe(args['component']), *args['args']])
    else:
        run_gui()


if __name__ == '__main__':
    init_logging('Launcher')
    parser = argparse.ArgumentParser(description='Archipelago Launcher')
    parser.add_argument('Patch|Game|Component', type=str, nargs='?',
                        help="Pass either a patch file, a generated game or the name of a component to run.")
    parser.add_argument('args', nargs="*", help="Arguments to pass to component.")
    main(parser.parse_args())