import tkinter
from distutils.version import StrictVersion as Version
import sys
import os
import platform
import ctypes
from typing import Union, Tuple, Optional

from .widgets.theme import ThemeManager
from .widgets.scaling import CTkScalingBaseClass
from .widgets.appearance_mode import CTkAppearanceModeBaseClass

from customtkinter.windows.widgets.utility.utility_functions import pop_from_dict_by_set, check_kwargs_empty


class CTkToplevel(tkinter.Toplevel, CTkAppearanceModeBaseClass, CTkScalingBaseClass):
    """
    Toplevel window with dark titlebar on Windows and macOS.
    For detailed information check out the documentation.
    """

    _valid_tk_toplevel_arguments: set = {"master", "bd", "borderwidth", "class", "container", "cursor", "height",
                                         "highlightbackground", "highlightthickness", "menu", "relief",
                                         "screen", "takefocus", "use", "visual", "width"}

    _deactivate_macos_window_header_manipulation: bool = False
    _deactivate_windows_window_header_manipulation: bool = False

    def __init__(self, *args,
                 fg_color: Optional[Union[str, Tuple[str, str]]] = None,
                 **kwargs):

        self._enable_macos_dark_title_bar()

        # call init methods of super classes
        super().__init__(*args, **pop_from_dict_by_set(kwargs, self._valid_tk_toplevel_arguments))
        CTkAppearanceModeBaseClass.__init__(self)
        CTkScalingBaseClass.__init__(self, scaling_type="window")
        check_kwargs_empty(kwargs, raise_error=True)

        try:
            # Set Windows titlebar icon
            if sys.platform.startswith("win"):
                customtkinter_directory = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
                self.after(200, lambda: self.iconbitmap(os.path.join(customtkinter_directory, "assets", "icons", "CustomTkinter_icon_Windows.ico")))
        except Exception:
            pass

        self._current_width = 200  # initial window size, always without scaling
        self._current_height = 200
        self._min_width: int = 0
        self._min_height: int = 0
        self._max_width: int = 1_000_000
        self._max_height: int = 1_000_000
        self._last_resizable_args: Union[Tuple[list, dict], None] = None  # (args, kwargs)

        self._fg_color = ThemeManager.theme["CTkToplevel"]["fg_color"] if fg_color is None else self._check_color_type(fg_color)

        # set bg color of tkinter.Toplevel
        super().configure(bg=self._apply_appearance_mode(self._fg_color))

        # set title of tkinter.Toplevel
        super().title("CTkToplevel")

        # indicator variables
        self._iconbitmap_method_called = True
        self._state_before_windows_set_titlebar_color = None
        self._windows_set_titlebar_color_called = False  # indicates if windows_set_titlebar_color was called, stays True until revert_withdraw_after_windows_set_titlebar_color is called
        self._withdraw_called_after_windows_set_titlebar_color = False  # indicates if withdraw() was called after windows_set_titlebar_color
        self._iconify_called_after_windows_set_titlebar_color = False  # indicates if iconify() was called after windows_set_titlebar_color
        self._block_update_dimensions_event = False

        # save focus before calling withdraw
        self.focused_widget_before_widthdraw = None

        # set CustomTkinter titlebar icon (Windows only)
        if sys.platform.startswith("win"):
            self.after(200, self._windows_set_titlebar_icon)

        # set titlebar color (Windows only)
        if sys.platform.startswith("win"):
            self._windows_set_titlebar_color(self._get_appearance_mode())

        self.bind('<Configure>', self._update_dimensions_event)
        self.bind('<FocusIn>', self._focus_in_event)

    def destroy(self):
        self._disable_macos_dark_title_bar()

        # call destroy methods of super classes
        tkinter.Toplevel.destroy(self)
        CTkAppearanceModeBaseClass.destroy(self)
        CTkScalingBaseClass.destroy(self)

    def _focus_in_event(self, event):
        # sometimes window looses jumps back on macOS if window is selected from Mission Control, so has to be lifted again
        if sys.platform == "darwin":
            self.lift()

    def _update_dimensions_event(self, event=None):
        if not self._block_update_dimensions_event:
            detected_width = self.winfo_width()  # detect current window size
            detected_height = self.winfo_height()

            if self._current_width != self._reverse_window_scaling(detected_width) or self._current_height != self._reverse_window_scaling(detected_height):
                self._current_width = self._reverse_window_scaling(detected_width)  # adjust current size according to new size given by event
                self._current_height = self._reverse_window_scaling(detected_height)  # _current_width and _current_height are independent of the scale

    def _set_scaling(self, new_widget_scaling, new_window_scaling):
        super()._set_scaling(new_widget_scaling, new_window_scaling)

        # Force new dimensions on window by using min, max, and geometry. Without min, max it won't work.
        super().minsize(self._apply_window_scaling(self._current_width), self._apply_window_scaling(self._current_height))
        super().maxsize(self._apply_window_scaling(self._current_width), self._apply_window_scaling(self._current_height))

        super().geometry(f"{self._apply_window_scaling(self._current_width)}x{self._apply_window_scaling(self._current_height)}")

        # set new scaled min and max with delay (delay prevents weird bug where window dimensions snap to unscaled dimensions when mouse releases window)
        self.after(1000, self._set_scaled_min_max)  # Why 1000ms delay? Experience! (Everything tested on Windows 11)

    def block_update_dimensions_event(self):
        self._block_update_dimensions_event = False

    def unblock_update_dimensions_event(self):
        self._block_update_dimensions_event = False

    def _set_scaled_min_max(self):
        if self._min_width is not None or self._min_height is not None:
            super().minsize(self._apply_window_scaling(self._min_width), self._apply_window_scaling(self._min_height))
        if self._max_width is not None or self._max_height is not None:
            super().maxsize(self._apply_window_scaling(self._max_width), self._apply_window_scaling(self._max_height))

    def geometry(self, geometry_string: str = None):
        if geometry_string is not None:
            super().geometry(self._apply_geometry_scaling(geometry_string))

            # update width and height attributes
            width, height, x, y = self._parse_geometry_string(geometry_string)
            if width is not None and height is not None:
                self._current_width = max(self._min_width, min(width, self._max_width))  # bound value between min and max
                self._current_height = max(self._min_height, min(height, self._max_height))
        else:
            return self._reverse_geometry_scaling(super().geometry())

    def withdraw(self):
        if self._windows_set_titlebar_color_called:
            self._withdraw_called_after_windows_set_titlebar_color = True
        super().withdraw()

    def iconify(self):
        if self._windows_set_titlebar_color_called:
            self._iconify_called_after_windows_set_titlebar_color = True
        super().iconify()

    def resizable(self, width: bool = None, height: bool = None):
        current_resizable_values = super().resizable(width, height)
        self._last_resizable_args = ([], {"width": width, "height": height})

        if sys.platform.startswith("win"):
            self.after(10, lambda: self._windows_set_titlebar_color(self._get_appearance_mode()))

        return current_resizable_values

    def minsize(self, width=None, height=None):
        self._min_width = width
        self._min_height = height
        if self._current_width < width:
            self._current_width = width
        if self._current_height < height:
            self._current_height = height
        super().minsize(self._apply_window_scaling(self._min_width), self._apply_window_scaling(self._min_height))

    def maxsize(self, width=None, height=None):
        self._max_width = width
        self._max_height = height
        if self._current_width > width:
            self._current_width = width
        if self._current_height > height:
            self._current_height = height
        super().maxsize(self._apply_window_scaling(self._max_width), self._apply_window_scaling(self._max_height))

    def configure(self, **kwargs):
        if "fg_color" in kwargs:
            self._fg_color = self._check_color_type(kwargs.pop("fg_color"))
            super().configure(bg=self._apply_appearance_mode(self._fg_color))

            for child in self.winfo_children():
                try:
                    child.configure(bg_color=self._fg_color)
                except Exception:
                    pass

        super().configure(**pop_from_dict_by_set(kwargs, self._valid_tk_toplevel_arguments))
        check_kwargs_empty(kwargs)

    def cget(self, attribute_name: str) -> any:
        if attribute_name == "fg_color":
            return self._fg_color
        else:
            return super().cget(attribute_name)

    def wm_iconbitmap(self, bitmap=None, default=None):
        self._iconbitmap_method_called = True
        super().wm_iconbitmap(bitmap, default)

    def _windows_set_titlebar_icon(self):
        try:
            # if not the user already called iconbitmap method, set icon
            if not self._iconbitmap_method_called:
                customtkinter_directory = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
                self.iconbitmap(os.path.join(customtkinter_directory, "assets", "icons", "CustomTkinter_icon_Windows.ico"))
        except Exception:
            pass

    @classmethod
    def _enable_macos_dark_title_bar(cls):
        if sys.platform == "darwin" and not cls._deactivate_macos_window_header_manipulation:  # macOS
            if Version(platform.python_version()) < Version("3.10"):
                if Version(tkinter.Tcl().call("info", "patchlevel")) >= Version("8.6.9"):  # Tcl/Tk >= 8.6.9
                    os.system("defaults write -g NSRequiresAquaSystemAppearance -bool No")

    @classmethod
    def _disable_macos_dark_title_bar(cls):
        if sys.platform == "darwin" and not cls._deactivate_macos_window_header_manipulation:  # macOS
            if Version(platform.python_version()) < Version("3.10"):
                if Version(tkinter.Tcl().call("info", "patchlevel")) >= Version("8.6.9"):  # Tcl/Tk >= 8.6.9
                    os.system("defaults delete -g NSRequiresAquaSystemAppearance")
                    # This command reverts the dark-mode setting for all programs.

    def _windows_set_titlebar_color(self, color_mode: str):
        """
        Set the titlebar color of the window to light or dark theme on Microsoft Windows.

        Credits for this function:
        https://stackoverflow.com/questions/23836000/can-i-change-the-title-bar-in-tkinter/70724666#70724666

        MORE INFO:
        https://docs.microsoft.com/en-us/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute
        """

        if sys.platform.startswith("win") and not self._deactivate_windows_window_header_manipulation:

            self._state_before_windows_set_titlebar_color = self.state()
            self.focused_widget_before_widthdraw = self.focus_get()
            super().withdraw()  # hide window so that it can be redrawn after the titlebar change so that the color change is visible
            super().update()

            if color_mode.lower() == "dark":
                value = 1
            elif color_mode.lower() == "light":
                value = 0
            else:
                return

            try:
                hwnd = ctypes.windll.user32.GetParent(self.winfo_id())
                DWMWA_USE_IMMERSIVE_DARK_MODE = 20
                DWMWA_USE_IMMERSIVE_DARK_MODE_BEFORE_20H1 = 19

                # try with DWMWA_USE_IMMERSIVE_DARK_MODE
                if ctypes.windll.dwmapi.DwmSetWindowAttribute(hwnd, DWMWA_USE_IMMERSIVE_DARK_MODE,
                                                              ctypes.byref(ctypes.c_int(value)),
                                                              ctypes.sizeof(ctypes.c_int(value))) != 0:
                    # try with DWMWA_USE_IMMERSIVE_DARK_MODE_BEFORE_20h1
                    ctypes.windll.dwmapi.DwmSetWindowAttribute(hwnd, DWMWA_USE_IMMERSIVE_DARK_MODE_BEFORE_20H1,
                                                               ctypes.byref(ctypes.c_int(value)),
                                                               ctypes.sizeof(ctypes.c_int(value)))

            except Exception as err:
                print(err)

            self._windows_set_titlebar_color_called = True
            self.after(5, self._revert_withdraw_after_windows_set_titlebar_color)

            if self.focused_widget_before_widthdraw is not None:
                self.after(10, self.focused_widget_before_widthdraw.focus)
                self.focused_widget_before_widthdraw = None

    def _revert_withdraw_after_windows_set_titlebar_color(self):
        """ if in a short time (5ms) after """
        if self._windows_set_titlebar_color_called:

            if self._withdraw_called_after_windows_set_titlebar_color:
                pass  # leave it withdrawed
            elif self._iconify_called_after_windows_set_titlebar_color:
                super().iconify()
            else:
                if self._state_before_windows_set_titlebar_color == "normal":
                    self.deiconify()
                elif self._state_before_windows_set_titlebar_color == "iconic":
                    self.iconify()
                elif self._state_before_windows_set_titlebar_color == "zoomed":
                    self.state("zoomed")
                else:
                    self.state(self._state_before_windows_set_titlebar_color)  # other states

            self._windows_set_titlebar_color_called = False
            self._withdraw_called_after_windows_set_titlebar_color = False
            self._iconify_called_after_windows_set_titlebar_color = False

    def _set_appearance_mode(self, mode_string):
        super()._set_appearance_mode(mode_string)

        if sys.platform.startswith("win"):
            self._windows_set_titlebar_color(mode_string)

        super().configure(bg=self._apply_appearance_mode(self._fg_color))
