import logging
import tkinter as tk
from enum import Enum
from functools import partial
from tkinter import ttk
from typing import TYPE_CHECKING, Callable, List, Optional

from PIL.ImageTk import PhotoImage

from core.gui.dialogs.colorpicker import ColorPickerDialog
from core.gui.dialogs.runtool import RunToolDialog
from core.gui.graph import tags
from core.gui.graph.enums import GraphMode
from core.gui.graph.shapeutils import ShapeType, is_marker
from core.gui.images import ImageEnum
from core.gui.nodeutils import NodeDraw, NodeUtils
from core.gui.observers import ObserversMenu
from core.gui.task import ProgressTask
from core.gui.themes import Styles
from core.gui.tooltip import Tooltip

if TYPE_CHECKING:
    from core.gui.app import Application

TOOLBAR_SIZE: int = 32
PICKER_SIZE: int = 24


class NodeTypeEnum(Enum):
    NODE = 0
    NETWORK = 1
    OTHER = 2


def enable_buttons(frame: ttk.Frame, enabled: bool) -> None:
    state = tk.NORMAL if enabled else tk.DISABLED
    for child in frame.winfo_children():
        child.configure(state=state)


class PickerFrame(ttk.Frame):
    def __init__(self, app: "Application", button: ttk.Button) -> None:
        super().__init__(app)
        self.app: "Application" = app
        self.button: ttk.Button = button

    def create_node_button(self, node_draw: NodeDraw, func: Callable) -> None:
        self.create_button(
            node_draw.label, func, node_draw.image_enum, node_draw.image_file
        )

    def create_button(
        self,
        label: str,
        func: Callable,
        image_enum: ImageEnum = None,
        image_file: str = None,
    ) -> None:
        if image_enum:
            bar_image = self.app.get_icon(image_enum, TOOLBAR_SIZE)
            image = self.app.get_icon(image_enum, PICKER_SIZE)
        else:
            bar_image = self.app.get_custom_icon(image_file, TOOLBAR_SIZE)
            image = self.app.get_custom_icon(image_file, PICKER_SIZE)
        button = ttk.Button(
            self, image=image, text=label, compound=tk.TOP, style=Styles.picker_button
        )
        button.image = image
        button.bind("<ButtonRelease-1>", lambda e: func(bar_image))
        button.grid(pady=1)

    def show(self) -> None:
        self.button.after(0, self._show)

    def _show(self) -> None:
        x = self.button.winfo_width() + 1
        y = self.button.winfo_rooty() - self.app.winfo_rooty() - 1
        self.place(x=x, y=y)
        self.app.bind_all("<ButtonRelease-1>", lambda e: self.destroy())
        self.wait_visibility()
        self.grab_set()
        self.wait_window()
        self.app.unbind_all("<ButtonRelease-1>")


class ButtonBar(ttk.Frame):
    def __init__(self, master: tk.Widget, app: "Application") -> None:
        super().__init__(master)
        self.app: "Application" = app
        self.radio_buttons: List[ttk.Button] = []

    def create_button(
        self, image_enum: ImageEnum, func: Callable, tooltip: str, radio: bool = False
    ) -> ttk.Button:
        image = self.app.get_icon(image_enum, TOOLBAR_SIZE)
        button = ttk.Button(self, image=image, command=func)
        button.image = image
        button.grid(sticky=tk.EW)
        Tooltip(button, tooltip)
        if radio:
            self.radio_buttons.append(button)
        return button

    def select_radio(self, selected: ttk.Button) -> None:
        for button in self.radio_buttons:
            button.state(["!pressed"])
        selected.state(["pressed"])


class MarkerFrame(ttk.Frame):
    PAD: int = 3

    def __init__(self, master: tk.BaseWidget, app: "Application") -> None:
        super().__init__(master, padding=self.PAD)
        self.app: "Application" = app
        self.color: str = "#000000"
        self.size: tk.DoubleVar = tk.DoubleVar()
        self.color_frame: Optional[tk.Frame] = None
        self.draw()

    def draw(self) -> None:
        self.columnconfigure(0, weight=1)

        image = self.app.get_icon(ImageEnum.DELETE, 16)
        button = ttk.Button(self, image=image, width=2, command=self.click_clear)
        button.image = image
        button.grid(sticky=tk.EW, pady=self.PAD)
        Tooltip(button, "Delete Marker")

        sizes = [1, 3, 8, 10]
        self.size.set(sizes[0])
        sizes = ttk.Combobox(
            self, state="readonly", textvariable=self.size, value=sizes, width=2
        )
        sizes.grid(sticky=tk.EW, pady=self.PAD)
        Tooltip(sizes, "Marker Size")

        frame_size = TOOLBAR_SIZE
        self.color_frame = tk.Frame(
            self, background=self.color, height=frame_size, width=frame_size
        )
        self.color_frame.grid(sticky=tk.EW)
        self.color_frame.bind("<Button-1>", self.click_color)
        Tooltip(self.color_frame, "Marker Color")

    def click_clear(self) -> None:
        canvas = self.app.manager.current()
        canvas.delete(tags.MARKER)

    def click_color(self, _event: tk.Event) -> None:
        dialog = ColorPickerDialog(self.app, self.app, self.color)
        self.color = dialog.askcolor()
        self.color_frame.config(background=self.color)


class Toolbar(ttk.Frame):
    """
    Core toolbar class
    """

    def __init__(self, app: "Application") -> None:
        """
        Create a CoreToolbar instance
        """
        super().__init__(app)
        self.app: "Application" = app

        # design buttons
        self.play_button: Optional[ttk.Button] = None
        self.select_button: Optional[ttk.Button] = None
        self.link_button: Optional[ttk.Button] = None
        self.node_button: Optional[ttk.Button] = None
        self.network_button: Optional[ttk.Button] = None
        self.annotation_button: Optional[ttk.Button] = None

        # runtime buttons
        self.runtime_select_button: Optional[ttk.Button] = None
        self.stop_button: Optional[ttk.Button] = None
        self.runtime_marker_button: Optional[ttk.Button] = None
        self.run_command_button: Optional[ttk.Button] = None

        # frames
        self.design_frame: Optional[ButtonBar] = None
        self.runtime_frame: Optional[ButtonBar] = None
        self.marker_frame: Optional[MarkerFrame] = None
        self.picker: Optional[PickerFrame] = None

        # observers
        self.observers_menu: Optional[ObserversMenu] = None

        # these variables help keep track of what images being drawn so that scaling
        # is possible since PhotoImage does not have resize method
        self.current_node: NodeDraw = NodeUtils.NODES[0]
        self.current_network: NodeDraw = NodeUtils.NETWORK_NODES[0]
        self.current_annotation: ShapeType = ShapeType.MARKER
        self.annotation_enum: ImageEnum = ImageEnum.MARKER

        # draw components
        self.draw()

    def draw(self) -> None:
        self.columnconfigure(0, weight=1)
        self.rowconfigure(0, weight=1)
        self.draw_design_frame()
        self.draw_runtime_frame()
        self.design_frame.tkraise()
        self.marker_frame = MarkerFrame(self, self.app)

    def draw_design_frame(self) -> None:
        self.design_frame = ButtonBar(self, self.app)
        self.design_frame.grid(row=0, column=0, sticky=tk.NSEW)
        self.design_frame.columnconfigure(0, weight=1)
        self.play_button = self.design_frame.create_button(
            ImageEnum.START, self.click_start, "Start Session"
        )
        self.select_button = self.design_frame.create_button(
            ImageEnum.SELECT, self.click_selection, "Selection Tool", radio=True
        )
        self.link_button = self.design_frame.create_button(
            ImageEnum.LINK, self.click_link, "Link Tool", radio=True
        )
        self.node_button = self.design_frame.create_button(
            self.current_node.image_enum,
            self.draw_node_picker,
            "Container Nodes",
            radio=True,
        )
        self.network_button = self.design_frame.create_button(
            self.current_network.image_enum,
            self.draw_network_picker,
            "Link Layer Nodes",
            radio=True,
        )
        self.annotation_button = self.design_frame.create_button(
            self.annotation_enum,
            self.draw_annotation_picker,
            "Annotation Tools",
            radio=True,
        )

    def draw_runtime_frame(self) -> None:
        self.runtime_frame = ButtonBar(self, self.app)
        self.runtime_frame.grid(row=0, column=0, sticky=tk.NSEW)
        self.runtime_frame.columnconfigure(0, weight=1)
        self.stop_button = self.runtime_frame.create_button(
            ImageEnum.STOP, self.click_stop, "Stop Session"
        )
        self.runtime_select_button = self.runtime_frame.create_button(
            ImageEnum.SELECT, self.click_runtime_selection, "Selection Tool", radio=True
        )
        self.create_observe_button()
        self.runtime_marker_button = self.runtime_frame.create_button(
            ImageEnum.MARKER, self.click_marker_button, "Marker Tool", radio=True
        )
        self.run_command_button = self.runtime_frame.create_button(
            ImageEnum.RUN, self.click_run_button, "Run Tool"
        )

    def draw_node_picker(self) -> None:
        self.hide_marker()
        self.app.manager.mode = GraphMode.NODE
        self.app.manager.node_draw = self.current_node
        self.design_frame.select_radio(self.node_button)
        self.picker = PickerFrame(self.app, self.node_button)
        # draw default nodes
        for node_draw in NodeUtils.NODES:
            func = partial(
                self.update_button, self.node_button, node_draw, NodeTypeEnum.NODE
            )
            self.picker.create_node_button(node_draw, func)
        # draw custom nodes
        for name in sorted(self.app.core.custom_nodes):
            node_draw = self.app.core.custom_nodes[name]
            func = partial(
                self.update_button, self.node_button, node_draw, NodeTypeEnum.NODE
            )
            self.picker.create_node_button(node_draw, func)
        self.picker.show()

    def click_selection(self) -> None:
        self.design_frame.select_radio(self.select_button)
        self.app.manager.mode = GraphMode.SELECT
        self.hide_marker()

    def click_runtime_selection(self) -> None:
        self.runtime_frame.select_radio(self.runtime_select_button)
        self.app.manager.mode = GraphMode.SELECT
        self.hide_marker()

    def click_start(self) -> None:
        """
        Start session handler redraw buttons, send node and link messages to grpc
        server.
        """
        self.app.menubar.set_state(is_runtime=True)
        self.app.manager.mode = GraphMode.SELECT
        enable_buttons(self.design_frame, enabled=False)
        task = ProgressTask(
            self.app, "Start", self.app.core.start_session, self.start_callback
        )
        task.start()

    def start_callback(self, result: bool, exceptions: List[str]) -> None:
        if result:
            self.set_runtime()
            self.app.core.set_metadata()
            self.app.core.show_mobility_players()
        else:
            enable_buttons(self.design_frame, enabled=True)
            if exceptions:
                message = "\n".join(exceptions)
                self.app.show_error("Start Session Error", message)

    def set_runtime(self) -> None:
        enable_buttons(self.runtime_frame, enabled=True)
        self.runtime_frame.tkraise()
        self.click_runtime_selection()
        self.hide_marker()

    def set_design(self) -> None:
        enable_buttons(self.design_frame, enabled=True)
        self.design_frame.tkraise()
        self.click_selection()
        self.hide_marker()

    def click_link(self) -> None:
        self.design_frame.select_radio(self.link_button)
        self.app.manager.mode = GraphMode.EDGE
        self.hide_marker()

    def update_button(
        self,
        button: ttk.Button,
        node_draw: NodeDraw,
        type_enum: NodeTypeEnum,
        image: PhotoImage,
    ) -> None:
        logging.debug("update button(%s): %s", button, node_draw)
        button.configure(image=image)
        button.image = image
        self.app.manager.node_draw = node_draw
        if type_enum == NodeTypeEnum.NODE:
            self.current_node = node_draw
        elif type_enum == NodeTypeEnum.NETWORK:
            self.current_network = node_draw

    def draw_network_picker(self) -> None:
        """
        Draw the options for link-layer button.
        """
        self.hide_marker()
        self.app.manager.mode = GraphMode.NODE
        self.app.manager.node_draw = self.current_network
        self.design_frame.select_radio(self.network_button)
        self.picker = PickerFrame(self.app, self.network_button)
        for node_draw in NodeUtils.NETWORK_NODES:
            func = partial(
                self.update_button, self.network_button, node_draw, NodeTypeEnum.NETWORK
            )
            self.picker.create_node_button(node_draw, func)
        self.picker.show()

    def draw_annotation_picker(self) -> None:
        """
        Draw the options for marker button.
        """
        self.design_frame.select_radio(self.annotation_button)
        self.app.manager.mode = GraphMode.ANNOTATION
        self.app.manager.annotation_type = self.current_annotation
        if is_marker(self.current_annotation):
            self.show_marker()
        self.picker = PickerFrame(self.app, self.annotation_button)
        nodes = [
            (ImageEnum.MARKER, ShapeType.MARKER),
            (ImageEnum.OVAL, ShapeType.OVAL),
            (ImageEnum.RECTANGLE, ShapeType.RECTANGLE),
            (ImageEnum.TEXT, ShapeType.TEXT),
        ]
        for image_enum, shape_type in nodes:
            label = shape_type.value
            func = partial(self.update_annotation, shape_type, image_enum)
            self.picker.create_button(label, func, image_enum)
        self.picker.show()

    def create_observe_button(self) -> None:
        image = self.app.get_icon(ImageEnum.OBSERVE, TOOLBAR_SIZE)
        menu_button = ttk.Menubutton(
            self.runtime_frame, image=image, direction=tk.RIGHT
        )
        menu_button.image = image
        menu_button.grid(sticky=tk.EW)
        self.observers_menu = ObserversMenu(menu_button, self.app)
        menu_button["menu"] = self.observers_menu

    def click_stop(self) -> None:
        """
        redraw buttons on the toolbar, send node and link messages to grpc server
        """
        logging.info("clicked stop button")
        self.app.menubar.set_state(is_runtime=False)
        self.app.core.close_mobility_players()
        enable_buttons(self.runtime_frame, enabled=False)
        task = ProgressTask(
            self.app, "Stop", self.app.core.stop_session, self.stop_callback
        )
        task.start()

    def stop_callback(self, result: bool) -> None:
        self.set_design()
        self.app.manager.stopped_session()

    def update_annotation(
        self, shape_type: ShapeType, image_enum: ImageEnum, image: PhotoImage
    ) -> None:
        logging.debug("clicked annotation")
        self.annotation_button.configure(image=image)
        self.annotation_button.image = image
        self.app.manager.annotation_type = shape_type
        self.current_annotation = shape_type
        self.annotation_enum = image_enum
        if is_marker(shape_type):
            self.show_marker()
        else:
            self.hide_marker()

    def hide_marker(self) -> None:
        self.marker_frame.grid_forget()

    def show_marker(self) -> None:
        self.marker_frame.grid()

    def click_run_button(self) -> None:
        logging.debug("Click on RUN button")
        dialog = RunToolDialog(self.app)
        dialog.show()

    def click_marker_button(self) -> None:
        self.runtime_frame.select_radio(self.runtime_marker_button)
        self.app.manager.mode = GraphMode.ANNOTATION
        self.app.manager.annotation_type = ShapeType.MARKER
        self.show_marker()

    def scale_button(
        self, button: ttk.Button, image_enum: ImageEnum = None, image_file: str = None
    ) -> None:
        image = None
        if image_enum:
            image = self.app.get_icon(image_enum, TOOLBAR_SIZE)
        elif image_file:
            image = self.app.get_custom_icon(image_file, TOOLBAR_SIZE)
        if image:
            button.config(image=image)
            button.image = image

    def scale(self) -> None:
        self.scale_button(self.play_button, ImageEnum.START)
        self.scale_button(self.select_button, ImageEnum.SELECT)
        self.scale_button(self.link_button, ImageEnum.LINK)
        if self.current_node.image_enum:
            self.scale_button(self.node_button, self.current_node.image_enum)
        else:
            self.scale_button(self.node_button, image_file=self.current_node.image_file)
        self.scale_button(self.network_button, self.current_network.image_enum)
        self.scale_button(self.annotation_button, self.annotation_enum)
        self.scale_button(self.runtime_select_button, ImageEnum.SELECT)
        self.scale_button(self.stop_button, ImageEnum.STOP)
        self.scale_button(self.runtime_marker_button, ImageEnum.MARKER)
        self.scale_button(self.run_command_button, ImageEnum.RUN)