import datetime as dt
import uuid

from .bootstrap_HTML import *
from js import document, FileReader, btoa, Uint8Array   # type: ignore
from pyodide import create_proxy  # type: ignore
import io
import base64


class InputLabel(HTML.Label, BootstrapContainer):
    _default_class_name: str = "form-label"


class InputLabelCheckbox(InputLabel):
    _default_class_name: str = "form-check-label"


class InputHelp(BootstrapContainer):
    _default_class_name: str = "form-text"


class InputFormControl(HTML.Input, BootstrapContainer):
    _default_class_name: str = "form-control"

    @property
    def value(self) -> str:
        return self._parse_value()

    @value.setter
    def value(self, value: str):
        self._set_value(value)


class InputFormControlColor(InputFormControl):
    _default_class_name: str = "form-control form-control-color"


class InputFormControlNumber(InputFormControl):
    _default_class_name: str = "form-control"

    @property
    def min(self):
        return self.element.getAttribute("min")

    @min.setter
    def min(self, value):
        self.element.setAttribute("min", str(value))

    @property
    def max(self):
        return self.element.getAttribute("max")

    @max.setter
    def max(self, value):
        self.element.setAttribute("max", str(value))


class InputFormControlRange(InputFormControl):
    _default_class_name: str = "form-range"


class InputFormControlSelect(InputFormControl):
    _default_class_name: str = "form-select"
    _tag_type: str = "select"


class InputFormControlCheckbox(InputFormControl):
    _default_class_name: str = "form-check-input"


class InputElement(BootstrapContainer):

    _default_input_type = "text"
    _default_input_class = InputFormControl
    _default_label_class = InputLabel

    def __init__(self,
                 label_text: str = None,
                 help_text: str = None,
                 floating_label: bool = False,
                 placeholder: str = None,
                 input_type=None,
                 id=None,
                 parent=None):
        super().__init__(parent=parent, id=id)

        if floating_label and placeholder is None:
            placeholder = " "  # create a dummy placeholder

        self._input = self.__class__._default_input_class(
            parent=None,
            id=self.id+"_input",
            type=input_type if input_type is not None else self._default_input_type,
            placeholder=placeholder
        )

        self._label = self.__class__._default_label_class(
            parent=None, id=self.id+"_label", inner_html=label_text)

        # if floating_label is True, add the input first, then the label

        if floating_label:
            self.append_child(self._input)
            self.append_child(self._label)

        else:
            self.append_child(self._label)
            self.append_child(self._input)

        self._help_text = None
        if help_text is not None:
            self._help_text = InputHelp(
                parent=self, id=self.id+"_help_text", inner_html=help_text)

        self.floating_label = floating_label

    @property
    def is_small(self) -> bool:
        return self._input.has_class("form-control-sm")

    @is_small.setter
    def is_small(self, value: bool):
        self._input.set_class_name("form-control-sm", value)

    @property
    def is_large(self) -> bool:
        return self._input.has_class("form-control-lg")

    @is_large.setter
    def is_large(self, value: bool):
        self._input.set_class_name("form-control-lg", value)

    @property
    def readonly(self) -> bool:
        return self._input.has_attribute("readonly")

    @readonly.setter
    def readonly(self, value: bool):
        self._input.set_attribute("readonly", value, is_boolean_attribute=True)

    @property
    def value(self):
        return self._input.value

    @value.setter
    def value(self, value):
        self._input.value = value

    @property
    def onchange(self):
        return self._input.onchange

    @onchange.setter
    def onchange(self, value):
        self._input.onchange = value

    @property
    def floating_label(self) -> bool:
        return self.has_class("form-floating")

    @floating_label.setter
    def floating_label(self, value: bool):
        self.set_class("form-floating", value)


class InputEmail(InputElement):
    _default_input_type = "email"


class InputPassword(InputElement):
    _default_input_type = "password"


class InputText(InputElement):
    _default_input_type = "text"


class InputFloat(InputElement):
    _default_input_type = "number"
    _default_number_class = float

    def __init__(self,
                 label_text: str = None,
                 help_text: str = None,
                 floating_label: bool = False,
                 placeholder: str = None,
                 min: _default_number_class = None,
                 max: _default_number_class = None,
                 step: _default_number_class = None,
                 input_type=None,
                 id=None,
                 parent=None):
        super().__init__(label_text=label_text,
                         help_text=help_text,
                         floating_label=floating_label,
                         placeholder=placeholder,
                         input_type=input_type,
                         id=id,
                         parent=parent)

        if min is not None:
            self.min = min

        if max is not None:
            self.max = max

        if step is not None:
            self.step = step

    @property
    def value(self) -> _default_number_class:
        val = self._input.value
        if val == "":
            return None
        return self.__class__._default_number_class(self._input.value)

    @value.setter
    def value(self, value: _default_number_class):
        self._input.value = str(value) if value is not None else ""

    @property
    def min(self) -> _default_number_class:
        return self.__class__._default_number_class(self._input.get_attribute("min"))

    @min.setter
    def min(self, value: _default_number_class):
        self._input.set_attribute("min", str(value))

    @property
    def max(self) -> _default_number_class:
        return self.__class__._default_number_class(self._input.get_attribute("max"))

    @max.setter
    def max(self, value: _default_number_class):
        self._input.set_attribute("max", str(value))

    @property
    def step(self) -> _default_number_class:
        return self.__class__._default_number_class(self._input.get_attribute("step"))

    @step.setter
    def step(self, value: _default_number_class):
        self._input.set_attribute("step", str(value))


class InputRangeFloat(InputFloat):
    _default_input_type = "range"
    _default_input_class = InputFormControlRange


class InputInt(InputFloat):

    _default_number_class = int


class InputRangeInt(InputInt):
    _default_input_type = "range"
    _default_input_class = InputFormControlRange


class InputDate(InputElement):
    _default_input_type = "date"

    @property
    def value(self) -> dt.datetime:
        if self._input.value == "":
            return None
        return dt.datetime.strptime(self._input.value, "%Y-%m-%d").date()

    @value.setter
    def value(self, value: Union[dt.datetime, dt.date, str, None]):
        if isinstance(value, str):
            self._input.value = value
        self._input.value = value.strftime(
            "%Y-%m-%d") if value is not None else ""


class InputTime(InputElement):
    _default_input_type = "time"

    @property
    def value(self) -> dt.datetime:
        if self._input.value == "":
            return None
        return dt.datetime.strptime(self._input.value, "%H:%M").time()

    @value.setter
    def value(self, value: Union[dt.datetime, dt.time, str, None]):
        if isinstance(value, str):
            self._input.value = value
        self._input.value = value.strftime(
            "%H:%M") if value is not None else ""


class InputRange(InputElement):
    _default_input_type = "range"


class InputFile(InputElement):
    _default_input_type = "file"

    def __init__(self,
                 label_text: str = None,
                 help_text: str = None,
                 floating_label: bool = False,
                 placeholder: str = None,
                 input_type=None,
                 id=None,
                 parent=None):
        
        super().__init__(label_text=label_text,
                         help_text=help_text,
                         floating_label=floating_label,
                         placeholder=placeholder,
                         input_type=input_type,
                         id=id,
                         parent=parent)
        
        self._files = {}
        self._input.onchange = self._load_file
        self._on_file_change = None


    def _load_file(self, *args):
        async def read_file(event, f):
                        
            # FIXME: i think i am doing things too complicated here,
            # but my understanding of loading raw bytes from file
            # inputs in javascript (and their pyodide interacions) is very much limited

            name = f.name

            print("loaded ", name)

            uint8_array = Uint8Array.new(await f.arrayBuffer())
            buffer = io.BytesIO(bytearray(uint8_array))
            self._files[name] = buffer
            buffer.seek(0)
            buffer.name = name # not standardized, but works here # TODO: create base class for that purpose
            if self._on_file_change is not None:
                self._on_file_change(buffer)

        file_list = self._input.element.files.to_py()

        self._files = {}
    
        for f in file_list:
            # reader is a pyodide.JsProxy
            reader = FileReader.new()
    
            # Create a Python proxy for the callback function
            onload_event = create_proxy(lambda event, f=f: read_file(event, f))
    
            reader.onload = onload_event
    
            reader.readAsBinaryString(f)
    
        return

    @property
    def value(self):
        return self._files
    
    @property
    def onchange(self):
        return self._on_file_change
    
    @onchange.setter
    def onchange(self, value):
        self._on_file_change = value


class InputMultiFile(InputFile):

    def __init__(self,
                 label_text: str = None,
                 help_text: str = None,
                 floating_label: bool = False,
                 placeholder: str = None,
                 input_type=None,
                 id=None,
                 parent=None):
        super().__init__(label_text=label_text,
                         help_text=help_text,
                         floating_label=floating_label,
                         placeholder=placeholder,
                         input_type=input_type,
                         id=id,
                         parent=parent)
        self._input.set_attribute("multiple", True, is_boolean_attribute=True)

    @property
    def value(self):
        files = []
        for i in range(self._input.element.files.length):
            files.append(self._input.element.files.item(i).name)
        return files


class InputColor(InputElement):
    _default_input_type = "color"
    _default_input_class = InputFormControlColor


class InputDatalist(InputElement):

    def __init__(self,
                 options: List[str],
                 label_text: str = None,
                 help_text: str = None,
                 floating_label: bool = False,
                 placeholder: str = None,
                 input_type=None,
                 id=None,
                 parent=None):
        super().__init__(label_text=label_text,
                         help_text=help_text,
                         floating_label=floating_label,
                         placeholder=placeholder,
                         input_type=input_type,
                         id=id,
                         parent=parent)

        self._input.type = None

        self._options = options

        self._datalist = HTML.DataList(parent=self, id=self.id+"_datalist")

        for option in options:
            HTML.Option(inner_html=option, parent=self._datalist)

        self._input.set_attribute("list", self._datalist.id)

    @property
    def options(self):
        return self._options

    @options.setter
    def options(self, value):
        self._options = value
        for c in self._datalist.children:
            c.destroy()
        for option in self._options:
            HTML.Option(inner_html=option, parent=self._datalist)


class InputSelect(InputElement):

    _default_input_class = InputFormControlSelect

    def __init__(self,
                 options: List[str],
                 label_text: str = None,
                 help_text: str = None,
                 floating_label: bool = False,
                 placeholder: str = None,
                 multiple: bool = False,
                 input_type=None,
                 id=None,
                 parent=None):
        super().__init__(label_text=label_text,
                         help_text=help_text,
                         floating_label=floating_label,
                         placeholder=placeholder,
                         input_type=input_type,
                         id=id,
                         parent=parent)

        self._input.type = None

        self._options = options

        self.multiple = multiple

        for option in options:
            HTML.Option(inner_html=option, parent=self._input)

    @property
    def options(self):
        return self._options

    @options.setter
    def options(self, value):
        self._options = value
        for c in self._input.children:
            c.destroy()
        for option in self._options:
            HTML.Option(inner_html=option, parent=self._input)

    @property
    def multiple(self):
        return self._input.has_attribute("multiple")

    @multiple.setter
    def multiple(self, value):
        self._input.set_attribute("multiple", value, is_boolean_attribute=True)


class InputCheckboxSingle(InputElement):

    _default_class_name: str = "form-check"
    _default_input_type = "checkbox"
    _default_input_class = InputFormControlCheckbox
    _default_label_class = InputLabelCheckbox

    def __init__(self,
                 label_text: str = None,
                 help_text: str = None,
                 floating_label: bool = False,
                 placeholder: str = None,
                 name: str = None,
                 input_type=None,
                 id=None,
                 parent=None):
        super().__init__(label_text=label_text,
                         help_text=help_text,
                         floating_label=floating_label,
                         placeholder=placeholder,
                         input_type=input_type,
                         id=id,
                         parent=parent)

        if name is not None:
            self._input.set_attribute("name", name)

    @property
    def checked(self) -> bool:
        return self._input.element.checked

    @checked.setter
    def checked(self, value: bool):
        self._input.element.checked = value

    @property
    def value(self) -> bool:
        return self.checked

    @value.setter
    def value(self, value: bool):
        self.checked = value

    @property
    def inline(self) -> bool:
        return self.has_class("inline")

    @inline.setter
    def inline(self, value: bool):
        self.set_class("inline", value)


class InputRadioSingle(InputCheckboxSingle):
    _default_input_type = "radio"


class InputSwitchSingle(InputCheckboxSingle):
    _default_class_name: str = "form-check form-switch"


class InputCheckboxGroup(BootstrapContainer):

    _default_input_class = InputCheckboxSingle

    def __init__(self,
                 options: List[str],
                 inline: bool = False,
                 id: str = None,
                 label_text: Union[str, HTML.Element] = None,
                 group_name: str = None,
                 class_name: str = None,
                 parent: HTML.Element = None,
                 inner_html: str = None,) -> None:
        super().__init__(inner_html=inner_html,
                         id=id,
                         class_name=class_name,
                         parent=parent)

        if group_name is None:
            group_name = "id-" + str(uuid.uuid4())
        if label_text is not None:
            if isinstance(label_text, str):
                label_text = HTML.Label(inner_html=label_text, parent=self)
            else:
                self.append_child(label_text)

        self._options = options
        self._option_checkboxes = {}
        self._group_name = group_name

        for option in options:
            cb = self.__class__._default_input_class(
                option, name=group_name, parent=self)
            cb.inline = inline
            self._option_checkboxes[option] = cb

    @property
    def options(self):
        return self._options

    @options.setter
    def options(self, value):
        self._options = value
        for c in self._option_checkboxes.values():
            c.destroy()
        self._option_checkboxes = {}
        for option in self._options:
            cb = self.__class__._default_input_class(
                option, name=self._group_name, parent=self)
            self._option_checkboxes[option] = cb

    @property
    def value(self) -> List[str]:
        """
        returns a list of checked options
        """

        values = []
        for option in self._options:
            if self._option_checkboxes[option].checked:
                values.append(option)
        return values

    @value.setter
    def value(self, value: List[str]):
        for option in self._options:
            self._option_checkboxes[option].checked = option in value

    @property
    def option_checkboxes(self) -> Dict[str, InputCheckboxSingle]:
        """
        returns the form elements for each option as dictionary
        """
        return self._option_checkboxes


class InputRadioGroup(InputCheckboxGroup):
    _default_input_class = InputRadioSingle


class InputSwitchGroup(InputCheckboxGroup):
    _default_input_class = InputSwitchSingle

class Form(HTML.Form, BootstrapContainer):
    pass
    # TODO: implement more Form and form validation methods