from PIL import Image
from collections import deque
import subprocess
from multiprocessing import Process, Queue
import threading
import sys
import logging
import queue

from .constants import USER_APP_NAME, APP_NAME
from .conffile import confdir
from .clients import clientManager
from .utils import get_resource
from .log_utils import CustomFormatter, root_logger
from .i18n import _

log = logging.getLogger("gui_mgr")

# From https://stackoverflow.com/questions/6631299/
# This is for opening the config directory.


def _show_file_darwin(path: str):
    subprocess.Popen(["open", path])


def _show_file_xdg(path: str):
    subprocess.Popen(["xdg-open", path])


def _show_file_win32(path: str):
    subprocess.Popen(["explorer", path])


_show_file_func = {
    "darwin": _show_file_darwin,
    "linux": _show_file_xdg,
    "openbsd": _show_file_xdg,
    "win32": _show_file_win32,
    "cygwin": _show_file_win32,
}

try:
    show_file = None
    for platform, func in _show_file_func.items():
        if sys.platform.startswith(platform):
            show_file = func

    def open_config():
        show_file(confdir(APP_NAME))

except KeyError:
    open_config = None
    log.warning("Platform does not support opening folders.")

# Setup a log handler for log items.
log_cache = deque([], 1000)


class GUILogHandler(logging.Handler):
    def __init__(self):
        self.callback = None
        super().__init__()

    def emit(self, record):
        log_entry = self.format(record)
        log_cache.append(log_entry)

        if self.callback:
            try:
                self.callback(log_entry)
            except Exception:
                pass


guiHandler = GUILogHandler()
guiHandler.setFormatter(CustomFormatter())
root_logger.addHandler(guiHandler)

# Why am I using another process for the GUI windows?
# Because both pystray and tkinter must run
# in the main thread of their respective process.


class LoggerWindow(threading.Thread):
    def __init__(self):
        self.dead = False
        self.queue = None
        self.r_queue = None
        self.process = None
        threading.Thread.__init__(self)

    def run(self):
        self.queue = Queue()
        self.r_queue = Queue()
        self.process = LoggerWindowProcess(self.queue, self.r_queue)

        def handle(message):
            self.handle("append", message)

        self.process.start()
        handle("\n".join(log_cache))
        guiHandler.callback = handle
        while True:
            action, param = self.r_queue.get()
            if action == "die":
                self._die()
                break

    def handle(self, action: str, params=None):
        self.queue.put((action, params))

    def stop(self):
        self.r_queue.put(("die", None))

    def _die(self):
        guiHandler.callback = None
        self.handle("die")
        self.process.terminate()
        self.dead = True


class LoggerWindowProcess(Process):
    def __init__(self, queue: Queue, r_queue: Queue):
        self.queue = queue
        self.r_queue = r_queue
        self.tk = None
        self.root = None
        self.text = None
        Process.__init__(self)

    def update(self):
        try:
            self.text.config(state=self.tk.NORMAL)
            while True:
                action, param = self.queue.get_nowait()
                if action == "append":
                    self.text.config(state=self.tk.NORMAL)
                    self.text.insert(self.tk.END, "\n")
                    self.text.insert(self.tk.END, param)
                    self.text.config(state=self.tk.DISABLED)
                    self.text.see(self.tk.END)
                elif action == "die":
                    self.root.destroy()
                    self.root.quit()
                    return
        except queue.Empty:
            pass
        self.text.after(100, self.update)

    def run(self):
        import tkinter as tk

        self.tk = tk
        root = tk.Tk()
        self.root = root
        root.title(_("Application Log"))
        try:
            icon_img = tk.PhotoImage(file=get_resource("logo.png"))
            root.iconphoto(True, icon_img)
        except Exception:
            pass
        text = tk.Text(root)
        text.pack(side=tk.LEFT, fill=tk.BOTH, expand=tk.YES)
        text.config(wrap=tk.WORD)
        self.text = text
        yscroll = tk.Scrollbar(command=text.yview)
        text["yscrollcommand"] = yscroll.set
        yscroll.pack(side=tk.RIGHT, fill=tk.Y)
        text.config(state=tk.DISABLED)
        self.update()
        root.mainloop()
        self.r_queue.put(("die", None))


class PreferencesWindow(threading.Thread):
    def __init__(self):
        self.dead = False
        self.dead_trigger = threading.Event()
        self.queue = None
        self.r_queue = None
        self.process = None
        threading.Thread.__init__(self)

    def run(self):
        self.queue = Queue()
        self.r_queue = Queue()
        self.process = PreferencesWindowProcess(self.queue, self.r_queue)
        self.process.start()
        self.handle("upd", clientManager.credentials)
        while True:
            action, param = self.r_queue.get()
            if action == "die":
                self._die()
                break
            elif action == "add":
                try:
                    is_logged_in = clientManager.login(*param)
                    if is_logged_in:
                        self.handle("upd", clientManager.credentials)
                    else:
                        self.handle("error")
                except Exception:
                    log.error("Error while adding server.", exc_info=True)
                    self.handle("error")
            elif action == "remove":
                clientManager.remove_client(param)
                self.handle("upd", clientManager.credentials)

    def handle(self, action: str, params=None):
        self.queue.put((action, params))

    def stop(self):
        self.r_queue.put(("die", None))

    def block_until_close(self):
        self.dead_trigger.wait()

    def _die(self):
        self.dead_trigger.set()
        self.handle("die")
        self.process.terminate()
        self.dead = True


class PreferencesWindowProcess(Process):
    def __init__(self, queue: Queue, r_queue: Queue):
        self.queue = queue
        self.r_queue = r_queue
        self.servers = None
        self.server_ids = None
        self.tk = None
        self.messagebox = None
        self.root = None
        self.serverList = None
        self.current_uuid = None
        self.servername = None
        self.username = None
        self.password = None
        self.add_button = None
        self.remove_button = None
        Process.__init__(self)

    def update(self):
        try:
            while True:
                action, param = self.queue.get_nowait()
                if action == "upd":
                    self.update_servers(param)
                    self.add_button.config(state=self.tk.NORMAL)
                    self.remove_button.config(state=self.tk.NORMAL)
                elif action == "error":
                    self.messagebox.showerror(
                        _("Add Server"),
                        _(
                            "Could not add server.\nPlease check your connection information."
                        ),
                    )
                    self.add_button.config(state=self.tk.NORMAL)
                elif action == "die":
                    self.root.destroy()
                    self.root.quit()
                    return
        except queue.Empty:
            pass
        self.root.after(100, self.update)

    def update_servers(self, server_list):
        self.servers = server_list
        self.server_ids = [x["uuid"] for x in self.servers]
        self.serverList.set(
            [
                "{0} ({1}, {2})".format(
                    server["Name"],
                    server["username"],
                    _("Ok") if server["connected"] else _("Fail"),
                )
                for server in self.servers
            ]
        )

    def run(self):
        import tkinter as tk
        from tkinter import ttk, messagebox

        self.tk = tk
        self.messagebox = messagebox
        root = tk.Tk()
        root.title(_("Server Configuration"))
        try:
            icon_img = tk.PhotoImage(file=get_resource("logo.png"))
            root.iconphoto(True, icon_img)
        except Exception:
            pass
        self.root = root

        self.servers = {}
        self.server_ids = []
        self.serverList = tk.StringVar(value=[])
        self.current_uuid = None

        def server_select(_x):
            idxs = serverlist.curselection()
            if len(idxs) == 1:
                self.current_uuid = self.server_ids[idxs[0]]

        c = ttk.Frame(root, padding=(5, 5, 12, 0))
        c.grid(column=0, row=0, sticky=(tk.N, tk.W, tk.E, tk.S))
        root.grid_columnconfigure(0, weight=1)
        root.grid_rowconfigure(0, weight=1)

        serverlist = tk.Listbox(c, listvariable=self.serverList, height=10, width=40)
        serverlist.grid(column=0, row=0, rowspan=6, sticky=(tk.N, tk.S, tk.E, tk.W))
        c.grid_columnconfigure(0, weight=1)
        c.grid_rowconfigure(4, weight=1)

        servername_label = ttk.Label(c, text=_("Server:"))
        servername_label.grid(column=1, row=0, sticky=tk.E)
        self.servername = tk.StringVar()
        servername_box = ttk.Entry(c, textvariable=self.servername)
        servername_box.grid(column=2, row=0)
        username_label = ttk.Label(c, text=_("Username:"))
        username_label.grid(column=1, row=1, sticky=tk.E)
        self.username = tk.StringVar()
        username_box = ttk.Entry(c, textvariable=self.username)
        username_box.grid(column=2, row=1)
        password_label = ttk.Label(c, text=_("Password:"))
        password_label.grid(column=1, row=2, sticky=tk.E)
        self.password = tk.StringVar()
        password_box = ttk.Entry(c, textvariable=self.password, show="*")
        password_box.grid(column=2, row=2)

        def add_server():
            self.add_button.config(state=tk.DISABLED)
            self.r_queue.put(
                (
                    "add",
                    (self.servername.get(), self.username.get(), self.password.get()),
                )
            )

        def remove_server():
            self.remove_button.config(state=tk.DISABLED)
            self.r_queue.put(("remove", self.current_uuid))

        def close():
            self.r_queue.put(("die", None))

        self.add_button = ttk.Button(c, text=_("Add Server"), command=add_server)
        self.add_button.grid(column=2, row=3, pady=5, sticky=tk.E)
        self.remove_button = ttk.Button(
            c, text=_("Remove Server"), command=remove_server
        )
        self.remove_button.grid(column=1, row=4, padx=5, pady=10, sticky=(tk.E, tk.S))
        close_button = ttk.Button(c, text=_("Close"), command=close)
        close_button.grid(column=2, row=4, pady=10, sticky=(tk.E, tk.S))

        serverlist.bind("<<ListboxSelect>>", server_select)
        self.update()
        root.mainloop()
        self.r_queue.put(("die", None))


# Q: OK. So you put Tkinter in it's own process.
#    Now why is Pystray in another process too?!
# A: Because if I don't, MPV and GNOME Appindicator
#    try to access the same resources and cause the
#    entire application to segfault.
#
# I suppose this means I can put the Tkinter GUI back
# into the main process. This is true, but then the
# two need to be merged, which is non-trivial.


class UserInterface(threading.Thread):
    def __init__(self):
        self.dead = False
        self.open_player_menu = lambda: None
        self.icon_stop = lambda: None
        self.log_window = None
        self.preferences_window = None
        self.stop_callback = None
        self.gui_ready = None
        self.r_queue = None
        self.process = None

        threading.Thread.__init__(self)

    def run(self):
        self.r_queue = Queue()
        self.process = STrayProcess(self.r_queue)
        self.process.start()

        while True:
            action, param = self.r_queue.get()
            if hasattr(self, action):
                try:
                    getattr(self, action)()
                except Exception:
                    log.error("Error handling tray action: %s", action, exc_info=True)
            elif action == "die":
                self._die()
                if self.stop_callback:
                    self.stop_callback()
                break

    def stop(self):
        self.r_queue.put(("die", None))

    def _die(self):
        self.process.terminate()
        self.dead = True

        if self.log_window and not self.log_window.dead:
            self.log_window.stop()
        if self.preferences_window and not self.preferences_window.dead:
            self.preferences_window.stop()

    def login_servers(self):
        is_logged_in = clientManager.try_connect()
        if not is_logged_in:
            self.show_preferences()
            self.preferences_window.block_until_close()

    def show_console(self):
        if self.log_window is None or self.log_window.dead:
            self.log_window = LoggerWindow()
            self.log_window.start()

    def show_preferences(self):
        if self.preferences_window is None or self.preferences_window.dead:
            self.preferences_window = PreferencesWindow()
            self.preferences_window.start()

    def ready(self):
        if self.gui_ready:
            self.gui_ready.set()

    @staticmethod
    def open_config_brs():
        if open_config:
            open_config()
        else:
            log.error("Config opening is not available.")


class STrayProcess(Process):
    def __init__(self, r_queue: Queue):
        self.r_queue = r_queue
        self.icon_stop = None
        Process.__init__(self)

    def run(self):
        import os
        import sys

        # Force X11 backend for GTK to fix Wayland startup issues. GDK_BACKEND
        # and WAYLAND_DISPLAY only mean anything to GTK on Linux/BSD; on
        # Windows and macOS pystray uses native APIs, so leave the env alone.
        if sys.platform.startswith("linux") or sys.platform.startswith("freebsd"):
            os.environ.pop("WAYLAND_DISPLAY", None)
            os.environ["GDK_BACKEND"] = "x11"

        try:
            from pystray import Icon, MenuItem, Menu
        except Exception as e:
            log.error(f"Failed to import pystray: {e}")
            self.r_queue.put(("die", None))
            return

        def get_wrapper(command):
            def wrapper():
                self.r_queue.put((command, None))

            return wrapper

        def die():
            # We don't call self.icon_stop() because it crashes on Linux now...
            if sys.platform == "linux":
                # This kills the status icon uncleanly.
                self.r_queue.put(("die", None))
            else:
                self.icon_stop()

        menu_items = [
            MenuItem(_("Configure Servers"), get_wrapper("show_preferences")),
            MenuItem(_("Show Console"), get_wrapper("show_console")),
            MenuItem(_("Application Menu"), get_wrapper("open_player_menu")),
            MenuItem(_("Open Config Folder"), get_wrapper("open_config_brs")),
            MenuItem(_("Quit"), die),
        ]

        icon = Icon(APP_NAME, title=USER_APP_NAME, menu=Menu(*menu_items))
        icon.icon = Image.open(get_resource("systray.png"))
        self.icon_stop = icon.stop

        def setup(icon: Icon):
            icon.visible = True
            self.r_queue.put(("ready", None))

        try:
            icon.run(setup=setup)
        except Exception:
            pass
        self.r_queue.put(("die", None))


user_interface = UserInterface()
