#  Copyright (c) 2021 Mira Geoscience Ltd.
#
#  This file is part of geoapps.
#
#  geoapps is distributed under the terms and conditions of the MIT License
#  (see LICENSE file at the root of this source code package).

from __future__ import annotations

from collections import OrderedDict
from uuid import UUID

import ipywidgets as widgets
import numpy as np
from geoh5py.data import FloatData, IntegerData, ReferencedData
from geoh5py.objects.object_base import ObjectBase
from geoh5py.workspace import Workspace
from ipywidgets import Dropdown, FloatText, SelectMultiple, VBox

from geoapps.base import BaseApplication
from geoapps.utils import utils


class ObjectDataSelection(BaseApplication):
    """
    Application to select an object and corresponding data
    """

    defaults = {}
    _data = None
    _objects = None
    _add_groups = False
    _select_multiple = False
    _object_types = ()
    _exclusion_types = ()
    _find_label = []

    def __init__(self, **kwargs):
        self._data_panel = None

        super().__init__(**kwargs)

    @property
    def add_groups(self):
        """
        bool: Add data groups to the list of data choices
        """
        return self._add_groups

    @add_groups.setter
    def add_groups(self, value):
        assert isinstance(value, (bool, str)), "add_groups must be of type bool"
        self._add_groups = value

    @property
    def data(self) -> Dropdown | SelectMultiple:
        """
        Data selector
        """
        if getattr(self, "_data", None) is None:
            if self.select_multiple:
                self._data = SelectMultiple(
                    description="Data: ",
                )
            else:
                self._data = Dropdown(
                    description="Data: ",
                )
            if self._objects is not None:
                self.update_data_list(None)

        return self._data

    @data.setter
    def data(self, value):
        assert isinstance(
            value, (Dropdown, SelectMultiple)
        ), f"'Objects' must be of type {Dropdown} or {SelectMultiple}"
        self._data = value

    @property
    def data_panel(self) -> VBox:
        if getattr(self, "_data_panel", None) is None:
            self._data_panel = VBox([self.objects, self.data])

        return self._data_panel

    @property
    def main(self) -> VBox:
        """
        :obj:`ipywidgets.VBox`: A box containing all widgets forming the application.
        """
        # self.__populate__(**self.defaults)
        if self._main is None:
            self._main = self.data_panel
            self.update_data_list(None)

        return self._main

    @property
    def objects(self) -> Dropdown:
        """
        Object selector
        """
        if getattr(self, "_objects", None) is None:
            self.objects = Dropdown(description="Object:")

        return self._objects

    @objects.setter
    def objects(self, value):
        assert isinstance(value, Dropdown), f"'Objects' must be of type {Dropdown}"
        self._objects = value
        self._objects.observe(self.update_data_list, names="value")

    @property
    def object_types(self):
        """
        Entity type
        """
        return self._object_types

    @object_types.setter
    def object_types(self, entity_types):
        if not isinstance(entity_types, tuple):
            entity_types = tuple(entity_types)

        for entity_type in entity_types:
            assert issubclass(
                entity_type, ObjectBase
            ), f"Provided object_types must be instances of {ObjectBase}"

        self._object_types = entity_types

    @property
    def exclusion_types(self):
        """
        Entity type
        """
        if getattr(self, "_exclusion_types", None) is None:
            self._exclusion_types = []

        return self._exclusion_types

    @exclusion_types.setter
    def exclusion_types(self, entity_types):
        if not isinstance(entity_types, tuple):
            entity_types = tuple(entity_types)

        for entity_type in entity_types:
            assert issubclass(
                entity_type, ObjectBase
            ), f"Provided exclusion_types must be instances of {ObjectBase}"

        self._exclusion_types = tuple(entity_types)

    @property
    def find_label(self):
        """
        Object selector
        """
        if getattr(self, "_find_label", None) is None:
            return []

        return self._find_label

    @find_label.setter
    def find_label(self, values):
        """
        Object selector
        """
        if not isinstance(values, list):
            values = [values]

        for value in values:
            assert isinstance(
                value, str
            ), f"Labels to find must be strings. Value {value} of type {type(value)} provided"
        self._find_label = values

    @property
    def select_multiple(self):
        """
        bool: ALlow to select multiple data
        """
        if getattr(self, "_select_multiple", None) is None:
            self._select_multiple = False

        return self._select_multiple

    @select_multiple.setter
    def select_multiple(self, value):
        if getattr(self, "_data", None) is not None:
            options = self._data.options
        else:
            options = []

        self._select_multiple = value

        if value:
            self._data = SelectMultiple(description="Data: ", options=options)
        else:
            self._data = Dropdown(description="Data: ", options=options)

    @property
    def workspace(self) -> Workspace | None:
        """
        Target geoh5py workspace
        """
        if (
            getattr(self, "_workspace", None) is None
            and getattr(self, "_h5file", None) is not None
        ):
            self.workspace = Workspace(self.h5file)
        return self._workspace

    @workspace.setter
    def workspace(self, workspace):
        assert isinstance(workspace, Workspace), f"Workspace must of class {Workspace}"
        self.base_workspace_changes(workspace)

        # Refresh the list of objects
        self.update_objects_list()

    def get_selected_entities(self):
        """
        Get entities from an active geoh5py Workspace
        """
        if getattr(self, "_workspace", None) is not None and self._workspace.get_entity(
            self.objects.value
        ):
            for entity in self._workspace.get_entity(self.objects.value):
                if isinstance(entity, ObjectBase):
                    obj = entity

            if isinstance(self.data, Dropdown):
                values = [self.data.value]
            else:
                values = self.data.value

            data = []
            for value in values:
                if any([pg.uid == value for pg in obj.property_groups]):
                    data += [
                        self.workspace.get_entity(prop)[0]
                        for prop in obj.find_or_create_property_group(
                            name=self.data.uid_name_map[value]
                        ).properties
                    ]
                elif self.workspace.get_entity(value):
                    data += self.workspace.get_entity(value)

            return obj, data
        else:
            return None, None

    def update_data_list(self, _):
        refresh = self.refresh.value
        self.refresh.value = False
        if getattr(self, "_workspace", None) is not None and self._workspace.get_entity(
            self.objects.value
        ):
            for entity in self._workspace.get_entity(self.objects.value):
                if isinstance(entity, ObjectBase):
                    obj = entity

            if getattr(obj, "get_data_list", None) is None:
                return

            options = [["", None]]

            if (self.add_groups or self.add_groups == "only") and obj.property_groups:
                options = (
                    options
                    + [["-- Groups --", None]]
                    + [[p_g.name, p_g.uid] for p_g in obj.property_groups]
                )

            if self.add_groups != "only":
                options += [["--- Channels ---", None]]
                children_list = {
                    child.uid: child.name
                    for child in obj.children
                    if isinstance(child, (IntegerData, FloatData))
                }
                ordered = OrderedDict(sorted(children_list.items(), key=lambda t: t[1]))
                options += [
                    [name, uid]
                    for uid, name in ordered.items()
                    if "visual parameter" not in name.lower()
                ]

                options += [["X", "X"], ["Y", "Y"], ["Z", "Z"]]

            value = self.data.value
            self.data.options = options

            self.update_uid_name_map()

            if self.select_multiple and any([val in options for val in value]):
                self.data.value = [val for val in value if val in options]
            elif value in dict(options).values():
                self.data.value = value
            elif self.find_label:
                self.data.value = utils.find_value(self.data.options, self.find_label)
        else:
            self.data.options = []
            self.data.uid_name_map = {}

        self.refresh.value = refresh

    def update_objects_list(self):
        if getattr(self, "_workspace", None) is not None:
            value = self.objects.value

            if len(self.object_types) > 0:
                obj_list = [
                    obj
                    for obj in self._workspace.objects
                    if isinstance(obj, self.object_types)
                ]
            else:
                obj_list = self._workspace.objects

            if len(self.exclusion_types) > 0:
                obj_list = [
                    obj for obj in obj_list if not isinstance(obj, self.exclusion_types)
                ]

            options = [["", None]] + [
                [obj.parent.name + "/" + obj.name, obj.uid] for obj in obj_list
            ]

            # if value in list(dict(options).values()):  # Silent update
            #     self.objects.unobserve(self.update_data_list, names="value")
            #     self.objects.options = options
            #     self.objects.value = value
            #     self._objects.observe(self.update_data_list, names="value")
            # else:
            self.objects.options = options
            # if value in list(dict(options).values()):
            #     self.objects.value = value

    def update_uid_name_map(self):
        """
        Update the dictionary that maps uuid to name.
        """
        uid_name = {}
        for key, value in self.data.options:
            if isinstance(value, UUID):
                uid_name[value] = key
            elif isinstance(value, str) and value in "XYZ":
                uid_name[value] = value
        self.data.uid_name_map = uid_name


class LineOptions(ObjectDataSelection):
    """
    Unique lines selection from selected data channel
    """

    defaults = {"find_label": "line"}
    _multiple_lines = None

    def __init__(self, **kwargs):

        self.defaults.update(**kwargs)

        super().__init__(**self.defaults)

        self.objects.observe(self.update_data_list, names="value")
        self.data.observe(self.update_line_list, names="value")
        self.data.description = "Lines field"

    @property
    def main(self):
        if self._main is None:
            self._main = VBox([self._data, self.lines])

        return self._main

    @property
    def lines(self):
        """
        Widget.SelectMultiple or Widget.Dropdown
        """
        if getattr(self, "_lines", None) is None:
            if self.multiple_lines:
                self._lines = widgets.SelectMultiple(
                    description="Select lines:",
                )
            else:
                self._lines = widgets.Dropdown(
                    description="Select line:",
                )

        return self._lines

    @property
    def multiple_lines(self):
        if getattr(self, "_multiple_lines", None) is None:
            self._multiple_lines = True

        return self._multiple_lines

    @multiple_lines.setter
    def multiple_lines(self, value):
        assert isinstance(
            value, bool
        ), f"'multiple_lines' property must be of type {bool}"
        self._multiple_lines = value

    def update_line_list(self, _):
        _, data = self.get_selected_entities()
        if data and getattr(data[0], "values", None) is not None:
            if isinstance(data[0], ReferencedData):
                self.lines.options = [""] + list(data[0].value_map.map.values())
            else:
                self.lines.options = [""] + np.unique(data[0].values).tolist()

        else:
            self.lines.options = [""]


class TopographyOptions(ObjectDataSelection):
    """
    Define the topography used by the inversion
    """

    defaults = {}

    def __init__(
        self, option_list=["None", "Object", "Relative to Sensor", "Constant"], **kwargs
    ):
        self.defaults.update(**kwargs)
        self.find_label = ["topo", "dem", "dtm", "elevation", "Z"]
        self._offset = FloatText(description="Vertical offset (+ve up)")
        self._constant = FloatText(
            description="Elevation (m)",
        )
        self.option_list = {
            "None": widgets.Label("No topography"),
            "Object": self.data_panel,
            "Relative to Sensor": self._offset,
            "Constant": self._constant,
        }
        self._options = widgets.RadioButtons(
            options=option_list,
            description="Define by:",
        )
        self.options.observe(self.update_options)

        super().__init__(**self.defaults)

    @property
    def panel(self):
        return self._panel

    @property
    def constant(self):
        return self._constant

    @property
    def main(self):
        if self._main is None:
            self._main = VBox([self.options, self.option_list[self.options.value]])

        return self._main

    @property
    def offset(self):
        return self._offset

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

    def update_options(self, _):
        self.main.children = [
            self.options,
            self.option_list[self.options.value],
        ]
