"""View implementation for RemoteFileInput."""

from functools import partial
from tempfile import NamedTemporaryFile
from typing import Any, Iterable, Optional, Tuple, Union, cast

from trame.app import get_server
from trame.widgets import client, html
from trame.widgets import vuetify3 as vuetify
from trame_client.widgets.core import AbstractElement
from trame_server.core import State

from nova.mvvm._internal.utils import rgetdictvalue
from nova.mvvm.trame_binding import TrameBinding
from nova.trame._internal.utils import get_state_name, get_state_param, set_state_param
from nova.trame.model.remote_file_input import RemoteFileInputModel
from nova.trame.view.layouts import VBoxLayout
from nova.trame.view_model.remote_file_input import RemoteFileInputViewModel

from .input_field import InputField


class RemoteFileInput:
    """Generates a file selection dialog for picking files off of the server.

    You cannot use typical Trame :code:`with` syntax to add children to this.
    """

    def __init__(
        self,
        v_model: Union[str, Tuple],
        allow_files: Union[bool, Tuple] = True,
        allow_folders: Union[bool, Tuple] = False,
        base_paths: Union[Iterable[str], str, None] = None,
        dialog_props: Optional[dict[str, Any]] = None,
        extensions: Union[Iterable[str], str, None] = None,
        input_props: Optional[dict[str, Any]] = None,
        return_contents: Union[bool, Tuple] = False,
        use_bytes: Union[bool, Tuple] = False,
    ) -> None:
        """Constructor for RemoteFileInput.

        For all parameters, tuples have a special syntax. See :ref:`TrameTuple <api_trame_tuple>` for a description of
        it.

        Parameters
        ----------
        v_model : Union[str, Tuple]
            The v-model for this component. If this references a Pydantic configuration variable, then this component
            will attempt to load a label, hint, and validation rules from the configuration for you automatically.
        allow_files : Union[bool, Tuple], optional
            If true, the user can save a file selection.
        allow_folders : Union[bool, Tuple], optional
            If true, the user can save a folder selection.
        base_paths : Union[Iterable[str], str], optional
            Only files under these paths will be shown. Typical Trame binding syntax doesn't work here as tuples are
            interpreted as literal extensions to filter with. Instead, you can pass a string with a JavaScript
            expression to bind this parameter.
        dialog_props : Dict[str, typing.Any], optional
            Props to be passed to VDialog.
        extensions : Union[Iterable[str], str], optional
            Only files with these extensions will be shown by default. The user can still choose to view all files.
            Typical Trame binding syntax doesn't work here as tuples are interpreted as literal extensions to filter
            with. Instead, you can pass a string with a JavaScript expression to bind this parameter.
        input_props : Dict[str, typing.Any], optional
            Props to be passed to InputField.
        return_contents : Union[bool, Tuple], optional
            If true, then the v_model will contain the contents of the file. If false, then the v_model will contain the
            path of the file. Defaults to false.
        use_bytes : Union[bool, Tuple], optional
            If true, then the file contents will be treated as bytestreams when calling decode_file.

        Returns
        -------
        None
        """
        self.server = get_server(None, client_type="vue3")

        self.v_model = v_model
        self.allow_files = allow_files
        self.allow_folders = allow_folders
        self.base_paths = base_paths if base_paths else ["/"]
        self.dialog_props = dict(dialog_props) if dialog_props else {}
        self.extensions = extensions if extensions else []
        self.input_props = dict(input_props) if input_props else {}
        self.return_contents = return_contents
        self.use_bytes = use_bytes

        if "__events" not in self.input_props:
            self.input_props["__events"] = []
        self.input_props["__events"].append("change")

        if "width" not in self.dialog_props:
            self.dialog_props["width"] = 600

        self._create_model()
        self._create_viewmodel()
        self._setup_bindings()

        self.create_ui()

    @property
    def state(self) -> State:
        return self.server.state

    def create_ui(self) -> None:
        with cast(
            AbstractElement,
            InputField(
                v_model=self.v_model,
                change=(self.vm.select_file, "[$event.target.value]"),
                **self.input_props,
            ),
        ) as input:
            if isinstance(input.classes, str):
                input.classes += " nova-remote-file-input"
            else:
                input.classes = "nova-remote-file-input"
            self.vm.init_view()

            with vuetify.Template(v_slot_append=True):
                with vuetify.VBtn(icon=True, size="small", click=self.vm.open_dialog):
                    vuetify.VIcon("mdi-folder-open")

                    with vuetify.VDialog(
                        v_model=self.vm.get_dialog_state_name(), activator="parent", height="100vh", **self.dialog_props
                    ):
                        with vuetify.VCard(classes="pa-4"):
                            with VBoxLayout():
                                vuetify.VTextField(
                                    v_model=self.vm.get_filter_state_name(),
                                    classes="mb-4 px-4",
                                    label=input.label,
                                    variant="outlined",
                                    update_modelValue=(self.vm.filter_paths, "[$event]"),
                                )

                            if self.allow_files and self.extensions:
                                with html.Div(v_if=(f"{self.vm.get_showing_all_state_name()}",)):
                                    vuetify.VListSubheader("All Available Files")
                                    vuetify.VBtn(
                                        "Don't show all",
                                        classes="mb-4",
                                        size="small",
                                        click=self.vm.toggle_showing_all_files,
                                    )
                                with html.Div(v_else=True):
                                    vuetify.VListSubheader(
                                        "Available Files with Extensions: "
                                        + ", ".join(
                                            get_state_param(self.state, (self.extensions,))
                                            if isinstance(self.extensions, str)
                                            else self.extensions
                                        )
                                    )
                                    vuetify.VBtn(
                                        "Show all",
                                        classes="mb-4",
                                        size="small",
                                        click=self.vm.toggle_showing_all_files,
                                    )
                            elif self.allow_files:
                                vuetify.VListSubheader("Available Files")
                            else:
                                vuetify.VListSubheader("Available Folders")

                            with vuetify.VList(classes="mb-4"):
                                self.vm.populate_file_list()

                                vuetify.VListItem(
                                    "{{ file.path }}",
                                    v_for=f"file, index in {self.vm.get_file_list_state_name()}",
                                    classes=(
                                        f"index < {self.vm.get_file_list_state_name()}.length - 1 "
                                        "? 'border-b-thin' "
                                        ": ''",
                                    ),
                                    prepend_icon=("file.directory ? 'mdi-folder' : 'mdi-file'",),
                                    click=(self.vm.select_file, "[file]"),
                                )
                                html.P(
                                    (
                                        "No files could be found, either because none exist or you lack permission to "
                                        "read this directory. Select .. to return to the previous directory."
                                    ),
                                    v_if=(
                                        f"{self.vm.get_file_list_state_name()}.length < 2 && "
                                        f"{self.vm.get_file_list_state_name()}[0].path === '..'"
                                    ),
                                    classes="pa-4",
                                )

                            vuetify.VSpacer()

                            with html.Div(classes="text-center"):
                                vuetify.VBtn(
                                    "OK",
                                    classes="mr-4",
                                    disabled=(f"!{self.vm.get_valid_selection_state_name()}",),
                                    click=self.vm.close_dialog,
                                )
                                vuetify.VBtn(
                                    "Cancel",
                                    color="lightgrey",
                                    click=partial(self.vm.close_dialog, cancel=True),
                                )

    def _create_model(self) -> None:
        self.model = RemoteFileInputModel()

    def _create_viewmodel(self) -> None:
        binding = TrameBinding(self.state)

        if isinstance(self.v_model, tuple):
            model_name = self.v_model[0]
        else:
            model_name = self.v_model

        self.set_v_model = client.JSEval(
            exec=f"{model_name} = $event; flushState('{model_name.split('.')[0].split('[')[0]}');"
        ).exec

        self.vm = RemoteFileInputViewModel(self.model, binding)

        self.vm.dialog_bind.connect(self.vm.get_dialog_state_name())
        self.vm.file_list_bind.connect(self.vm.get_file_list_state_name())
        self.vm.filter_bind.connect(self.vm.get_filter_state_name())
        self.vm.on_close_bind.connect(client.JSEval(exec=f"{self.vm.get_dialog_state_name()} = false;").exec)
        self.vm.showing_all_bind.connect(self.vm.get_showing_all_state_name())
        self.vm.valid_selection_bind.connect(self.vm.get_valid_selection_state_name())

    # This method sets up Trame state change listeners for each binding parameter that can be changed directly by this
    # component. This allows us to communicate the changes to the developer's bindings without requiring our own. We
    # don't want bindings in the internal implementation as our callbacks could compete with the developer's.
    def _setup_bindings(self) -> None:
        # If the bindings were given initial values, write these to the state.
        self._last_allow_files = set_state_param(self.state, self.allow_files)
        self._last_allow_folders = set_state_param(self.state, self.allow_folders)
        self._last_base_paths = (
            get_state_param(self.state, (self.base_paths,)) if isinstance(self.base_paths, str) else self.base_paths
        )
        self._last_extensions = (
            get_state_param(self.state, (self.extensions,)) if isinstance(self.extensions, str) else self.extensions
        )
        self._last_return_contents = set_state_param(self.state, self.return_contents)

        # Now we need to propagate the state to this component's view model.
        self.vm.set_binding_parameters(
            allow_files=self.allow_files,
            allow_folders=self.allow_folders,
            base_paths=self._last_base_paths,
            extensions=self._last_extensions,
        )
        self._setup_update_binding(self._last_return_contents)

        # Now we set up the change listeners for all bound parameters. These are responsible for updating the component
        # when other portions of the application manipulate these parameters.
        if isinstance(self.allow_files, tuple):

            @self.state.change(get_state_name(self.allow_files[0]))
            def on_allow_files_change(**kwargs: Any) -> None:
                if isinstance(self.allow_files, bool):
                    return
                allow_files = rgetdictvalue(kwargs, self.allow_files[0])
                if allow_files != self._last_allow_files:
                    self.vm.set_binding_parameters(
                        allow_files=set_state_param(self.state, self.allow_files, allow_files)
                    )

        if isinstance(self.allow_folders, tuple):

            @self.state.change(get_state_name(self.allow_folders[0]))
            def on_allow_folders_change(**kwargs: Any) -> None:
                if isinstance(self.allow_folders, bool):
                    return
                allow_folders = rgetdictvalue(kwargs, self.allow_folders[0])
                if allow_folders != self._last_allow_folders:
                    self.vm.set_binding_parameters(
                        allow_folders=set_state_param(self.state, self.allow_folders, allow_folders)
                    )

        if isinstance(self.base_paths, str):

            @self.state.change(get_state_name(self.base_paths))
            def on_base_paths_change(**kwargs: Any) -> None:
                if isinstance(self.base_paths, bool):
                    return
                base_paths = rgetdictvalue(kwargs, self.base_paths)
                if base_paths != self._last_base_paths:
                    self.vm.set_binding_parameters(base_paths=set_state_param(self.state, self.base_paths, base_paths))

        if isinstance(self.extensions, str):

            @self.state.change(get_state_name(self.extensions))
            def on_extensions_change(**kwargs: Any) -> None:
                if isinstance(self.extensions, bool):
                    return
                extensions = rgetdictvalue(kwargs, self.extensions)
                if extensions != self._last_extensions:
                    self.vm.set_binding_parameters(extensions=set_state_param(self.state, self.extensions, extensions))

        if isinstance(self.return_contents, tuple):

            @self.state.change(get_state_name(self.return_contents[0]))
            def on_return_contents_change(**kwargs: Any) -> None:
                if isinstance(self.return_contents, bool):
                    return
                return_contents = rgetdictvalue(kwargs, self.return_contents[0])
                if return_contents != self._last_return_contents:
                    self._setup_update_binding(return_contents)

    def _setup_update_binding(self, read_file: bool) -> None:
        self.vm.reset_update_binding()
        if read_file:
            self.vm.on_update_bind.connect(self.read_file)
        else:
            self.vm.on_update_bind.connect(self.set_v_model)

    def read_file(self, file_path: str) -> None:
        with open(file_path, mode="rb") as file:
            self.decode_file(file.read())

    def decode_file(self, bytestream: bytes, set_contents: bool = False) -> None:
        use_bytes = get_state_param(self.state, self.use_bytes)

        decoded_content = bytestream.decode("latin1")
        if set_contents:
            self.set_v_model(decoded_content)
        else:
            if use_bytes:
                with NamedTemporaryFile(mode="wb", delete=False) as temp_file:
                    temp_file.write(bytestream)
                    temp_file.flush()
                    self.set_v_model(temp_file.name)
            else:
                with NamedTemporaryFile(mode="w", delete=False, encoding="utf-8") as temp_file:
                    temp_file.write(decoded_content)
                    temp_file.flush()
                    self.set_v_model(temp_file.name)

    def select_file(self, value: str) -> None:
        """Programmatically set the v_model value."""
        self.vm.select_file(value)

    def open_dialog(self) -> None:
        """Programmatically opens the dialog for selecting a file."""
        self.vm.open_dialog()
