import logging
import math
import tkinter as tk
from tkinter import PhotoImage, font, ttk
from tkinter.ttk import Progressbar

import grpc

from core.gui import appconfig, themes
from core.gui.coreclient import CoreClient
from core.gui.dialogs.error import ErrorDialog
from core.gui.graph.graph import CanvasGraph
from core.gui.images import ImageEnum, Images
from core.gui.menubar import Menubar
from core.gui.nodeutils import NodeUtils
from core.gui.statusbar import StatusBar
from core.gui.toolbar import Toolbar

WIDTH = 1000
HEIGHT = 800


class Application(ttk.Frame):
    def __init__(self, proxy: bool) -> None:
        super().__init__()
        # load node icons
        NodeUtils.setup()

        # widgets
        self.menubar = None
        self.toolbar = None
        self.right_frame = None
        self.canvas = None
        self.statusbar = None
        self.progress = None

        # fonts
        self.fonts_size = None
        self.icon_text_font = None
        self.edge_font = None

        # setup
        self.guiconfig = appconfig.read()
        self.app_scale = self.guiconfig.scale
        self.setup_scaling()
        self.style = ttk.Style()
        self.setup_theme()
        self.core = CoreClient(self, proxy)
        self.setup_app()
        self.draw()
        self.core.setup()

    def setup_scaling(self) -> None:
        self.fonts_size = {name: font.nametofont(name)["size"] for name in font.names()}
        text_scale = self.app_scale if self.app_scale < 1 else math.sqrt(self.app_scale)
        themes.scale_fonts(self.fonts_size, self.app_scale)
        self.icon_text_font = font.Font(family="TkIconFont", size=int(12 * text_scale))
        self.edge_font = font.Font(
            family="TkDefaultFont", size=int(8 * text_scale), weight=font.BOLD
        )

    def setup_theme(self) -> None:
        themes.load(self.style)
        self.master.bind_class("Menu", "<<ThemeChanged>>", themes.theme_change_menu)
        self.master.bind("<<ThemeChanged>>", themes.theme_change)
        self.style.theme_use(self.guiconfig.preferences.theme)

    def setup_app(self) -> None:
        self.master.title("CORE")
        self.center()
        self.master.protocol("WM_DELETE_WINDOW", self.on_closing)
        image = Images.get(ImageEnum.CORE, 16)
        self.master.tk.call("wm", "iconphoto", self.master._w, image)
        self.master.option_add("*tearOff", tk.FALSE)
        self.setup_file_dialogs()

    def setup_file_dialogs(self) -> None:
        """
        Hack code that needs to initialize a bad dialog so that we can apply,
        global settings for dialogs to not show hidden files by default and display
        the hidden file toggle.

        :return: nothing
        """
        try:
            self.master.tk.call("tk_getOpenFile", "-foobar")
        except tk.TclError:
            pass
        self.master.tk.call("set", "::tk::dialog::file::showHiddenBtn", "1")
        self.master.tk.call("set", "::tk::dialog::file::showHiddenVar", "0")

    def center(self) -> None:
        screen_width = self.master.winfo_screenwidth()
        screen_height = self.master.winfo_screenheight()
        x = int((screen_width / 2) - (WIDTH * self.app_scale / 2))
        y = int((screen_height / 2) - (HEIGHT * self.app_scale / 2))
        self.master.geometry(
            f"{int(WIDTH * self.app_scale)}x{int(HEIGHT * self.app_scale)}+{x}+{y}"
        )

    def draw(self) -> None:
        self.master.rowconfigure(0, weight=1)
        self.master.columnconfigure(0, weight=1)
        self.rowconfigure(0, weight=1)
        self.columnconfigure(1, weight=1)
        self.grid(sticky="nsew")
        self.toolbar = Toolbar(self)
        self.toolbar.grid(sticky="ns")
        self.right_frame = ttk.Frame(self)
        self.right_frame.columnconfigure(0, weight=1)
        self.right_frame.rowconfigure(0, weight=1)
        self.right_frame.grid(row=0, column=1, sticky="nsew")
        self.draw_canvas()
        self.draw_status()
        self.progress = Progressbar(self.right_frame, mode="indeterminate")
        self.menubar = Menubar(self)
        self.master.config(menu=self.menubar)

    def draw_canvas(self) -> None:
        canvas_frame = ttk.Frame(self.right_frame)
        canvas_frame.rowconfigure(0, weight=1)
        canvas_frame.columnconfigure(0, weight=1)
        canvas_frame.grid(sticky="nsew", pady=1)
        self.canvas = CanvasGraph(canvas_frame, self, self.core)
        self.canvas.grid(sticky="nsew")
        scroll_y = ttk.Scrollbar(canvas_frame, command=self.canvas.yview)
        scroll_y.grid(row=0, column=1, sticky="ns")
        scroll_x = ttk.Scrollbar(
            canvas_frame, orient=tk.HORIZONTAL, command=self.canvas.xview
        )
        scroll_x.grid(row=1, column=0, sticky="ew")
        self.canvas.configure(xscrollcommand=scroll_x.set)
        self.canvas.configure(yscrollcommand=scroll_y.set)

    def draw_status(self) -> None:
        self.statusbar = StatusBar(self.right_frame, self)
        self.statusbar.grid(sticky="ew")

    def show_grpc_exception(self, title: str, e: grpc.RpcError) -> None:
        logging.exception("app grpc exception", exc_info=e)
        message = e.details()
        self.show_error(title, message)

    def show_exception(self, title: str, e: Exception) -> None:
        logging.exception("app exception", exc_info=e)
        self.show_error(title, str(e))

    def show_error(self, title: str, message: str) -> None:
        self.after(0, lambda: ErrorDialog(self, title, message).show())

    def on_closing(self) -> None:
        if self.toolbar.picker:
            self.toolbar.picker.destroy()
        self.menubar.prompt_save_running_session(True)

    def save_config(self) -> None:
        appconfig.save(self.guiconfig)

    def joined_session_update(self) -> None:
        if self.core.is_runtime():
            self.toolbar.set_runtime()
        else:
            self.toolbar.set_design()

    def get_icon(self, image_enum: ImageEnum, width: int) -> PhotoImage:
        return Images.get(image_enum, int(width * self.app_scale))

    def get_custom_icon(self, image_file: str, width: int) -> PhotoImage:
        return Images.get_custom(image_file, int(width * self.app_scale))

    def close(self) -> None:
        self.master.destroy()