import tkinter
from typing import Union, Tuple, Optional, Callable, Any

from .core_rendering import CTkCanvas
from .ctk_scrollbar import CTkScrollbar
from .theme import ThemeManager
from .core_rendering import DrawEngine
from .core_widget_classes import CTkBaseClass
from .font import CTkFont
from .utility import pop_from_dict_by_set, check_kwargs_empty


class CTkTextbox(CTkBaseClass):
    """
    Textbox with x and y scrollbars, rounded corners, and all text features of tkinter.Text widget.
    Scrollbars only appear when they are needed. Text is wrapped on line end by default,
    set wrap='none' to disable automatic line wrapping.
    For detailed information check out the documentation.

    Detailed methods and parameters of the underlaying tkinter.Text widget can be found here:
    https://anzeljg.github.io/rin2/book2/2405/docs/tkinter/text.html
    (most of them are implemented here too)
    """

    _scrollbar_update_time = 200  # interval in ms, to check if scrollbars are needed

    # attributes that are passed to and managed by the tkinter textbox only:
    _valid_tk_text_attributes = {"autoseparators", "cursor", "exportselection",
                                 "insertborderwidth", "insertofftime", "insertontime", "insertwidth",
                                 "maxundo", "padx", "pady", "selectborderwidth", "spacing1",
                                 "spacing2", "spacing3", "state", "tabs", "takefocus", "undo", "wrap",
                                 "xscrollcommand", "yscrollcommand"}

    def __init__(self,
                 master: any,
                 width: int = 200,
                 height: int = 200,
                 corner_radius: Optional[int] = None,
                 border_width: Optional[int] = None,
                 border_spacing: int = 3,

                 bg_color: Union[str, Tuple[str, str]] = "transparent",
                 fg_color: Optional[Union[str, Tuple[str, str]]] = None,
                 border_color: Optional[Union[str, Tuple[str, str]]] = None,
                 text_color: Optional[Union[str, str]] = None,
                 scrollbar_button_color: Optional[Union[str, Tuple[str, str]]] = None,
                 scrollbar_button_hover_color:  Optional[Union[str, Tuple[str, str]]] = None,

                 font: Optional[Union[tuple, CTkFont]] = None,
                 activate_scrollbars: bool = True,
                 **kwargs):

        # transfer basic functionality (_bg_color, size, __appearance_mode, scaling) to CTkBaseClass
        super().__init__(master=master, bg_color=bg_color, width=width, height=height)

        # color
        self._fg_color = ThemeManager.theme["CTkTextbox"]["fg_color"] if fg_color is None else self._check_color_type(fg_color, transparency=True)
        self._border_color = ThemeManager.theme["CTkTextbox"]["border_color"] if border_color is None else self._check_color_type(border_color)
        self._text_color = ThemeManager.theme["CTkTextbox"]["text_color"] if text_color is None else self._check_color_type(text_color)
        self._scrollbar_button_color = ThemeManager.theme["CTkTextbox"]["scrollbar_button_color"] if scrollbar_button_color is None else self._check_color_type(scrollbar_button_color)
        self._scrollbar_button_hover_color = ThemeManager.theme["CTkTextbox"]["scrollbar_button_hover_color"] if scrollbar_button_hover_color is None else self._check_color_type(scrollbar_button_hover_color)

        # shape
        self._corner_radius = ThemeManager.theme["CTkTextbox"]["corner_radius"] if corner_radius is None else corner_radius
        self._border_width = ThemeManager.theme["CTkTextbox"]["border_width"] if border_width is None else border_width
        self._border_spacing = border_spacing

        # font
        self._font = CTkFont() if font is None else self._check_font_type(font)
        if isinstance(self._font, CTkFont):
            self._font.add_size_configure_callback(self._update_font)

        self._canvas = CTkCanvas(master=self,
                                 highlightthickness=0,
                                 width=self._apply_widget_scaling(self._desired_width),
                                 height=self._apply_widget_scaling(self._desired_height))
        self._canvas.grid(row=0, column=0, rowspan=2, columnspan=2, sticky="nsew")
        self._canvas.configure(bg=self._apply_appearance_mode(self._bg_color))
        self._draw_engine = DrawEngine(self._canvas)

        self._textbox = tkinter.Text(self,
                                     fg=self._apply_appearance_mode(self._text_color),
                                     width=0,
                                     height=0,
                                     font=self._apply_font_scaling(self._font),
                                     highlightthickness=0,
                                     relief="flat",
                                     insertbackground=self._apply_appearance_mode(self._text_color),
                                     **pop_from_dict_by_set(kwargs, self._valid_tk_text_attributes))

        check_kwargs_empty(kwargs, raise_error=True)

        # scrollbars
        self._scrollbars_activated = activate_scrollbars
        self._hide_x_scrollbar = True
        self._hide_y_scrollbar = True

        self._y_scrollbar = CTkScrollbar(self,
                                         width=8,
                                         height=0,
                                         border_spacing=0,
                                         fg_color=self._fg_color,
                                         button_color=self._scrollbar_button_color,
                                         button_hover_color=self._scrollbar_button_hover_color,
                                         orientation="vertical",
                                         command=self._textbox.yview)
        self._textbox.configure(yscrollcommand=self._y_scrollbar.set)

        self._x_scrollbar = CTkScrollbar(self,
                                         height=8,
                                         width=0,
                                         border_spacing=0,
                                         fg_color=self._fg_color,
                                         button_color=self._scrollbar_button_color,
                                         button_hover_color=self._scrollbar_button_hover_color,
                                         orientation="horizontal",
                                         command=self._textbox.xview)
        self._textbox.configure(xscrollcommand=self._x_scrollbar.set)

        self._create_grid_for_text_and_scrollbars(re_grid_textbox=True, re_grid_x_scrollbar=True, re_grid_y_scrollbar=True)

        self.after(50, self._check_if_scrollbars_needed, None, True)
        self._draw()

    def _create_grid_for_text_and_scrollbars(self, re_grid_textbox=False, re_grid_x_scrollbar=False, re_grid_y_scrollbar=False):

        # configure 2x2 grid
        self.grid_rowconfigure(0, weight=1)
        self.grid_rowconfigure(1, weight=0, minsize=self._apply_widget_scaling(max(self._corner_radius, self._border_width + self._border_spacing)))
        self.grid_columnconfigure(0, weight=1)
        self.grid_columnconfigure(1, weight=0, minsize=self._apply_widget_scaling(max(self._corner_radius, self._border_width + self._border_spacing)))

        if re_grid_textbox:
            self._textbox.grid(row=0, column=0, rowspan=1, columnspan=1, sticky="nsew",
                               padx=(self._apply_widget_scaling(max(self._corner_radius, self._border_width + self._border_spacing)), 0),
                               pady=(self._apply_widget_scaling(max(self._corner_radius, self._border_width + self._border_spacing)), 0))

        if re_grid_x_scrollbar:
            if not self._hide_x_scrollbar and self._scrollbars_activated:
                self._x_scrollbar.grid(row=1, column=0, rowspan=1, columnspan=1, sticky="ewn",
                                       pady=(3, self._border_spacing + self._border_width),
                                       padx=(max(self._corner_radius, self._border_width + self._border_spacing), 0))  # scrollbar grid method without scaling
            else:
                self._x_scrollbar.grid_forget()

        if re_grid_y_scrollbar:
            if not self._hide_y_scrollbar and self._scrollbars_activated:
                self._y_scrollbar.grid(row=0, column=1, rowspan=1, columnspan=1, sticky="nsw",
                                       padx=(3, self._border_spacing + self._border_width),
                                       pady=(max(self._corner_radius, self._border_width + self._border_spacing), 0))  # scrollbar grid method without scaling
            else:
                self._y_scrollbar.grid_forget()

    def _check_if_scrollbars_needed(self, event=None, continue_loop: bool = False):
        """ Method hides or places the scrollbars if they are needed on key release event of tkinter.text widget """

        if self._scrollbars_activated:
            if self._textbox.xview() != (0.0, 1.0) and not self._x_scrollbar.winfo_ismapped():  # x scrollbar needed
                self._hide_x_scrollbar = False
                self._create_grid_for_text_and_scrollbars(re_grid_x_scrollbar=True)
            elif self._textbox.xview() == (0.0, 1.0) and self._x_scrollbar.winfo_ismapped():  # x scrollbar not needed
                self._hide_x_scrollbar = True
                self._create_grid_for_text_and_scrollbars(re_grid_x_scrollbar=True)

            if self._textbox.yview() != (0.0, 1.0) and not self._y_scrollbar.winfo_ismapped():  # y scrollbar needed
                self._hide_y_scrollbar = False
                self._create_grid_for_text_and_scrollbars(re_grid_y_scrollbar=True)
            elif self._textbox.yview() == (0.0, 1.0) and self._y_scrollbar.winfo_ismapped():  # y scrollbar not needed
                self._hide_y_scrollbar = True
                self._create_grid_for_text_and_scrollbars(re_grid_y_scrollbar=True)
        else:
            self._hide_x_scrollbar = False
            self._hide_x_scrollbar = False
            self._create_grid_for_text_and_scrollbars(re_grid_y_scrollbar=True)

        if self._textbox.winfo_exists() and continue_loop is True:
            self.after(self._scrollbar_update_time, lambda: self._check_if_scrollbars_needed(continue_loop=True))

    def _set_scaling(self, *args, **kwargs):
        super()._set_scaling(*args, **kwargs)

        self._textbox.configure(font=self._apply_font_scaling(self._font))
        self._canvas.configure(width=self._apply_widget_scaling(self._desired_width),
                               height=self._apply_widget_scaling(self._desired_height))
        self._create_grid_for_text_and_scrollbars(re_grid_textbox=True, re_grid_x_scrollbar=True, re_grid_y_scrollbar=True)
        self._draw(no_color_updates=True)

    def _set_dimensions(self, width=None, height=None):
        super()._set_dimensions(width, height)

        self._canvas.configure(width=self._apply_widget_scaling(self._desired_width),
                               height=self._apply_widget_scaling(self._desired_height))
        self._draw()

    def _update_font(self):
        """ pass font to tkinter widgets with applied font scaling and update grid with workaround """
        self._textbox.configure(font=self._apply_font_scaling(self._font))

        # Workaround to force grid to be resized when text changes size.
        # Otherwise grid will lag and only resizes if other mouse action occurs.
        self._canvas.grid_forget()
        self._canvas.grid(row=0, column=0, rowspan=2, columnspan=2, sticky="nsew")

    def destroy(self):
        if isinstance(self._font, CTkFont):
            self._font.remove_size_configure_callback(self._update_font)

        super().destroy()

    def _draw(self, no_color_updates=False):
        super()._draw(no_color_updates)

        if not self._canvas.winfo_exists():
            return

        requires_recoloring = self._draw_engine.draw_rounded_rect_with_border(self._apply_widget_scaling(self._current_width),
                                                                              self._apply_widget_scaling(self._current_height),
                                                                              self._apply_widget_scaling(self._corner_radius),
                                                                              self._apply_widget_scaling(self._border_width))

        if no_color_updates is False or requires_recoloring:
            if self._fg_color == "transparent":
                self._canvas.itemconfig("inner_parts",
                                        fill=self._apply_appearance_mode(self._bg_color),
                                        outline=self._apply_appearance_mode(self._bg_color))
                self._textbox.configure(fg=self._apply_appearance_mode(self._text_color),
                                        bg=self._apply_appearance_mode(self._bg_color),
                                        insertbackground=self._apply_appearance_mode(self._text_color))
                self._x_scrollbar.configure(fg_color=self._bg_color, button_color=self._scrollbar_button_color,
                                            button_hover_color=self._scrollbar_button_hover_color)
                self._y_scrollbar.configure(fg_color=self._bg_color, button_color=self._scrollbar_button_color,
                                            button_hover_color=self._scrollbar_button_hover_color)
            else:
                self._canvas.itemconfig("inner_parts",
                                        fill=self._apply_appearance_mode(self._fg_color),
                                        outline=self._apply_appearance_mode(self._fg_color))
                self._textbox.configure(fg=self._apply_appearance_mode(self._text_color),
                                        bg=self._apply_appearance_mode(self._fg_color),
                                        insertbackground=self._apply_appearance_mode(self._text_color))
                self._x_scrollbar.configure(fg_color=self._fg_color, button_color=self._scrollbar_button_color,
                                            button_hover_color=self._scrollbar_button_hover_color)
                self._y_scrollbar.configure(fg_color=self._fg_color, button_color=self._scrollbar_button_color,
                                            button_hover_color=self._scrollbar_button_hover_color)

            self._canvas.itemconfig("border_parts",
                                    fill=self._apply_appearance_mode(self._border_color),
                                    outline=self._apply_appearance_mode(self._border_color))
            self._canvas.configure(bg=self._apply_appearance_mode(self._bg_color))

        self._canvas.tag_lower("inner_parts")
        self._canvas.tag_lower("border_parts")

    def configure(self, require_redraw=False, **kwargs):
        if "fg_color" in kwargs:
            self._fg_color = self._check_color_type(kwargs.pop("fg_color"), transparency=True)
            require_redraw = True

            # check if CTk widgets are children of the frame and change their _bg_color to new frame fg_color
            for child in self.winfo_children():
                if isinstance(child, CTkBaseClass) and hasattr(child, "_fg_color"):
                    child.configure(bg_color=self._fg_color)

        if "border_color" in kwargs:
            self._border_color = self._check_color_type(kwargs.pop("border_color"))
            require_redraw = True

        if "text_color" in kwargs:
            self._text_color = self._check_color_type(kwargs.pop("text_color"))
            require_redraw = True

        if "scrollbar_button_color" in kwargs:
            self._scrollbar_button_color = self._check_color_type(kwargs.pop("scrollbar_button_color"))
            self._x_scrollbar.configure(button_color=self._scrollbar_button_color)
            self._y_scrollbar.configure(button_color=self._scrollbar_button_color)

        if "scrollbar_button_hover_color" in kwargs:
            self._scrollbar_button_hover_color = self._check_color_type(kwargs.pop("scrollbar_button_hover_color"))
            self._x_scrollbar.configure(button_hover_color=self._scrollbar_button_hover_color)
            self._y_scrollbar.configure(button_hover_color=self._scrollbar_button_hover_color)

        if "corner_radius" in kwargs:
            self._corner_radius = kwargs.pop("corner_radius")
            self._create_grid_for_text_and_scrollbars(re_grid_textbox=True, re_grid_x_scrollbar=True, re_grid_y_scrollbar=True)
            require_redraw = True

        if "border_width" in kwargs:
            self._border_width = kwargs.pop("border_width")
            self._create_grid_for_text_and_scrollbars(re_grid_textbox=True, re_grid_x_scrollbar=True, re_grid_y_scrollbar=True)
            require_redraw = True

        if "border_spacing" in kwargs:
            self._border_spacing = kwargs.pop("border_spacing")
            self._create_grid_for_text_and_scrollbars(re_grid_textbox=True, re_grid_x_scrollbar=True, re_grid_y_scrollbar=True)
            require_redraw = True

        if "font" in kwargs:
            if isinstance(self._font, CTkFont):
                self._font.remove_size_configure_callback(self._update_font)
            self._font = self._check_font_type(kwargs.pop("font"))
            if isinstance(self._font, CTkFont):
                self._font.add_size_configure_callback(self._update_font)

            self._update_font()

        self._textbox.configure(**pop_from_dict_by_set(kwargs, self._valid_tk_text_attributes))
        super().configure(require_redraw=require_redraw, **kwargs)

    def cget(self, attribute_name: str) -> any:
        if attribute_name == "corner_radius":
            return self._corner_radius
        elif attribute_name == "border_width":
            return self._border_width
        elif attribute_name == "border_spacing":
            return self._border_spacing

        elif attribute_name == "fg_color":
            return self._fg_color
        elif attribute_name == "border_color":
            return self._border_color
        elif attribute_name == "text_color":
            return self._text_color

        elif attribute_name == "font":
            return self._font

        else:
            return super().cget(attribute_name)

    def bind(self, sequence: str = None, command: Callable = None, add: Union[str, bool] = True):
        """ called on the tkinter.Canvas """
        if not (add == "+" or add is True):
            raise ValueError("'add' argument can only be '+' or True to preserve internal callbacks")
        self._textbox.bind(sequence, command, add=True)

    def unbind(self, sequence: str = None, funcid: str = None):
        """ called on the tkinter.Label and tkinter.Canvas """
        if funcid is not None:
            raise ValueError("'funcid' argument can only be None, because there is a bug in" +
                             " tkinter and its not clear whether the internal callbacks will be unbinded or not")
        self._textbox.unbind(sequence, None)

    def focus(self):
        return self._textbox.focus()

    def focus_set(self):
        return self._textbox.focus_set()

    def focus_force(self):
        return self._textbox.focus_force()

    def insert(self, index, text, tags=None):
        return self._textbox.insert(index, text, tags)

    def get(self, index1, index2=None):
        return self._textbox.get(index1, index2)

    def bbox(self, index):
        return self._textbox.bbox(index)

    def compare(self, index, op, index2):
        return self._textbox.compare(index, op, index2)

    def delete(self, index1, index2=None):
        return self._textbox.delete(index1, index2)

    def dlineinfo(self, index):
        return self._textbox.dlineinfo(index)

    def edit_modified(self, arg=None):
        return self._textbox.edit_modified(arg)

    def edit_redo(self):
        self._check_if_scrollbars_needed()
        return self._textbox.edit_redo()

    def edit_reset(self):
        return self._textbox.edit_reset()

    def edit_separator(self):
        return self._textbox.edit_separator()

    def edit_undo(self):
        self._check_if_scrollbars_needed()
        return self._textbox.edit_undo()

    def image_create(self, index, **kwargs):
        raise AttributeError("embedding images is forbidden, because would be incompatible with scaling")

    def image_cget(self, index, option):
        raise AttributeError("embedding images is forbidden, because would be incompatible with scaling")

    def image_configure(self, index):
        raise AttributeError("embedding images is forbidden, because would be incompatible with scaling")

    def image_names(self):
        raise AttributeError("embedding images is forbidden, because would be incompatible with scaling")

    def index(self, i):
        return self._textbox.index(i)

    def mark_gravity(self, mark, gravity=None):
        return self._textbox.mark_gravity(mark, gravity)

    def mark_names(self):
        return self._textbox.mark_names()

    def mark_next(self, index):
        return self._textbox.mark_next(index)

    def mark_previous(self, index):
        return self._textbox.mark_previous(index)

    def mark_set(self, mark, index):
        return self._textbox.mark_set(mark, index)

    def mark_unset(self, mark):
        return self._textbox.mark_unset(mark)

    def scan_dragto(self, x, y):
        return self._textbox.scan_dragto(x, y)

    def scan_mark(self, x, y):
        return self._textbox.scan_mark(x, y)

    def search(self, pattern, index, *args, **kwargs):
        return self._textbox.search(pattern, index, *args, **kwargs)

    def see(self, index):
        return self._textbox.see(index)

    def tag_add(self, tagName, index1, index2=None):
        return self._textbox.tag_add(tagName, index1, index2)

    def tag_bind(self, tagName, sequence, func, add=None):
        return self._textbox.tag_bind(tagName, sequence, func, add)

    def tag_cget(self, tagName, option):
        return self._textbox.tag_cget(tagName, option)

    def tag_config(self, tagName, **kwargs):
        if "font" in kwargs:
            raise AttributeError("'font' option forbidden, because would be incompatible with scaling")
        return self._textbox.tag_config(tagName, **kwargs)

    def tag_delete(self, *tagName):
        return self._textbox.tag_delete(*tagName)

    def tag_lower(self, tagName, belowThis=None):
        return self._textbox.tag_lower(tagName, belowThis)

    def tag_names(self, index=None):
        return self._textbox.tag_names(index)

    def tag_nextrange(self, tagName, index1, index2=None):
        return self._textbox.tag_nextrange(tagName, index1, index2)

    def tag_prevrange(self, tagName, index1, index2=None):
        return self._textbox.tag_prevrange(tagName, index1, index2)

    def tag_raise(self, tagName, aboveThis=None):
        return self._textbox.tag_raise(tagName, aboveThis)

    def tag_ranges(self, tagName):
        return self._textbox.tag_ranges(tagName)

    def tag_remove(self, tagName, index1, index2=None):
        return self._textbox.tag_remove(tagName, index1, index2)

    def tag_unbind(self, tagName, sequence, funcid=None):
        return self._textbox.tag_unbind(tagName, sequence, funcid)

    def window_cget(self, index, option):
        raise AttributeError("embedding widgets is forbidden, would probably cause all kinds of problems ;)")

    def window_configure(self, index, option):
        raise AttributeError("embedding widgets is forbidden, would probably cause all kinds of problems ;)")

    def window_create(self, index, **kwargs):
        raise AttributeError("embedding widgets is forbidden, would probably cause all kinds of problems ;)")

    def window_names(self):
        raise AttributeError("embedding widgets is forbidden, would probably cause all kinds of problems ;)")

    def xview(self, *args):
        return self._textbox.xview(*args)

    def xview_moveto(self, fraction):
        return self._textbox.xview_moveto(fraction)

    def xview_scroll(self, n, what):
        return self._textbox.xview_scroll(n, what)

    def yview(self, *args):
        return self._textbox.yview(*args)

    def yview_moveto(self, fraction):
        return self._textbox.yview_moveto(fraction)

    def yview_scroll(self, n, what):
        return self._textbox.yview_scroll(n, what)
