import tkinter as tk
from tkinter import messagebox, ttk


class BoundedNumericalEntry(tk.Entry):
    def __init__(
        self,
        master=None,
        min_value=None,
        max_value=None,
        variable=None,
        vartype=float,
        width=7,
        allow_inf=False,
        **kwargs,
    ):
        if variable is None:
            if vartype == float:
                self.var = tk.DoubleVar()
            elif vartype == int:
                self.var = tk.IntVar()
            else:
                self.var = tk.StringVar()
        else:
            self.var = variable

        self.fake_var = tk.StringVar(value=self.var.get())
        self.vartype = vartype
        self.old_value = self.var.get()
        self.allow_inf = allow_inf

        self.min_value, self.max_value = min_value, max_value
        self.get, self.set = self.fake_var.get, self.fake_var.set

        self.validate_command = master.register(self._check_bounds)
        tk.Entry.__init__(
            self,
            master,
            textvariable=self.fake_var,
            validate="focus",
            width=width,
            vcmd=(self.validate_command, "%P", "%d"),
            **kwargs,
        )

    def _check_bounds(self, instr, action_type):
        if self.allow_inf and instr == "INF":
            self.fake_var.set("INF")
            return True

        if action_type == "-1":
            try:
                new_value = self.vartype(instr)
            except ValueError:
                pass
            else:
                if (
                    self.min_value is None or new_value >= self.min_value
                ) and (self.max_value is None or new_value <= self.max_value):
                    if new_value != self.old_value:
                        self.old_value = self.vartype(self.fake_var.get())
                        self.delete(0, tk.END)
                        self.insert(0, str(self.old_value))
                        self.var.set(self.old_value)
                    return True
        self.delete(0, tk.END)
        self.insert(0, str(self.old_value))
        mn = "-inf" if self.min_value is None else str(self.min_value)
        mx = "+inf" if self.max_value is None else str(self.max_value)
        messagebox.showwarning(
            "Incorrect value in input field",
            f"Value for {self._name} should be in "
            f"[{mn}; {mx}] and of type {self.vartype.__name__}",
        )

        return False


class FocusHorizontalScale(tk.Scale):
    def __init__(
        self,
        *args,
        highlightthickness=0,
        sliderrelief=tk.GROOVE,
        resolution=0.01,
        sliderlength=20,
        length=200,
        **kwargs,
    ):
        tk.Scale.__init__(
            self,
            *args,
            orient=tk.HORIZONTAL,
            highlightthickness=highlightthickness,
            sliderrelief=sliderrelief,
            resolution=resolution,
            sliderlength=sliderlength,
            length=length,
            **kwargs,
        )
        self.bind("<1>", lambda event: self.focus_set())


class FocusCheckButton(tk.Checkbutton):
    def __init__(self, *args, highlightthickness=0, **kwargs):
        tk.Checkbutton.__init__(
            self, *args, highlightthickness=highlightthickness, **kwargs
        )
        self.bind("<1>", lambda event: self.focus_set())


class FocusButton(tk.Button):
    def __init__(self, *args, highlightthickness=0, **kwargs):
        tk.Button.__init__(
            self, *args, highlightthickness=highlightthickness, **kwargs
        )
        self.bind("<1>", lambda event: self.focus_set())


class FocusLabelFrame(ttk.LabelFrame):
    def __init__(
        self,
        *args,
        highlightthickness=0,
        relief=tk.RIDGE,
        borderwidth=2,
        **kwargs,
    ):
        tk.LabelFrame.__init__(
            self,
            *args,
            highlightthickness=highlightthickness,
            relief=relief,
            borderwidth=borderwidth,
            **kwargs,
        )
        self.bind("<1>", lambda event: self.focus_set())

    def set_frame_state(self, state):
        def set_widget_state(widget, state):
            if widget.winfo_children is not None:
                for w in widget.winfo_children():
                    w.configure(state=state)

        set_widget_state(self, state)
