#   Copyright ETH 2018 - 2024 Zürich, Scientific IT Services
# 
#   Licensed under the Apache License, Version 2.0 (the "License");
#   you may not use this file except in compliance with the License.
#   You may obtain a copy of the License at
# 
#        http://www.apache.org/licenses/LICENSE-2.0
#   
#   Unless required by applicable law or agreed to in writing, software
#   distributed under the License is distributed on an "AS IS" BASIS,
#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#   See the License for the specific language governing permissions and
#   limitations under the License.
#
from tabulate import tabulate

from .utils import (
    check_datatype
)


class PropertyHolder:
    def __init__(self, openbis_obj, type=None):
        self.__dict__["_openbis"] = openbis_obj
        self.__dict__["_property_names"] = {}
        if type is None:
            return

        self.__dict__["_type"] = type
        if (
                "propertyAssignments" in type.data
                and type.data["propertyAssignments"] is not None
        ):
            for prop in type.data["propertyAssignments"]:
                property_name = prop["propertyType"]["code"].lower()
                self._property_names[property_name] = prop["propertyType"]
                self._property_names[property_name]["mandatory"] = prop["mandatory"]
                self._property_names[property_name]["showInEditView"] = prop[
                    "showInEditView"
                ]
                if prop["propertyType"]["dataType"] == "CONTROLLEDVOCABULARY":
                    pt = self._openbis.get_property_type(prop["propertyType"]["code"])
                    # get the vocabulary of a property type.
                    # In some cases, the «code» of an assigned property is not identical to the «vocabulary» attribute
                    voc = self._openbis.get_vocabulary(pt.vocabulary)
                    terms = voc.get_terms()
                    self._property_names[property_name]["terms"] = terms

    def _all_props(self):
        props = {}
        if not getattr(self, "_type"):
            return props
        for code in self._type.codes():
            props[code] = getattr(self, code)
        return props

    def all(self):
        """Returns the properties as an array"""
        props = {}
        for code in self._type.codes():
            props[code] = getattr(self, code)
        return props

    def all_nonempty(self):
        props = {}
        for code in self._type.codes():
            value = getattr(self, code)
            if value is not None:
                props[code] = value
        return props

    def __call__(self, *args):
        if len(args) == 0:
            return self.all()
        elif len(args) == 1:
            return getattr(self, args[0])
        elif len(args) == 2:
            return setattr(self, args[0], args[1])
        else:
            raise ValueError("called properties with more than 2 arguments")

    def get(self, *args):
        if len(args) == 0:
            return self.all()
        elif len(args) == 1 and not isinstance(args[0], list):
            return getattr(self, args[0])
        else:
            if isinstance(args[0], list):
                args = args[0]
            return {arg: getattr(self, arg, None) for arg in args}

    def set(self, *args):
        if len(args) == 2:
            setattr(self, args[0], args[1])
        elif len(args) == 1 and isinstance(args[0], dict):
            for key in args[0]:
                setattr(self, key, args[0][key])

    def __getitem__(self, key):
        """For properties that contain either a dot or a dash or any other non-valid method character,
        a user can use a key-lookup instead, e.g. sample.props['my-weird.property-name']
        """

        return getattr(self, key)

    def __getattr__(self, name):
        """attribute syntax can be found out by
        adding an underscore at the end of the property name
        """
        if name == "_ipython_canary_method_should_not_exist_":
            # make Jupyter use the _repr_html_ method
            return
        if name.endswith("_"):
            name = name.rstrip("_")
            if name in self._property_names:
                property_type = self._property_names[name]
                if property_type["dataType"] == "CONTROLLEDVOCABULARY":
                    return property_type["terms"]
                    # return self._get_terms(property_type['code'])
                else:
                    syntax = {property_type["label"]: property_type["dataType"]}
                    if property_type["dataType"] == "TIMESTAMP":
                        syntax["syntax"] = "YYYY-MM-DD HH:MIN:SS"
                    return syntax
            else:
                return

    def __setattr__(self, name, value):
        """This special method allows a PropertyHolder object
        to check the attributes that are assigned to that object
        """
        if name not in self._property_names:
            raise KeyError(
                f"No such property: «{name}». Allowed properties are: {', '.join(self._property_names.keys())}"
            )
        property_type = self._property_names[name]
        data_type = property_type["dataType"]
        if "multiValue" in property_type and property_type["multiValue"] is not True and type(
                value) == list and data_type.startswith('ARRAY_') is False:
            raise ValueError(
                f'Property type {property_type["code"]} is not a multi-value property!')
        if value == '':
            value = None
        if value is not None:
            if data_type == "CONTROLLEDVOCABULARY":
                terms = property_type["terms"]
                if "multiValue" in property_type and property_type["multiValue"] is True:
                    if type(value) != list:
                        value = [value]
                    for single_value in value:
                        if str(single_value).upper() not in terms.df["code"].values:
                            raise ValueError(
                                f"Value for attribute «{name}» must be one of these terms: {', '.join(terms.df['code'].values)}"
                            )
                else:
                    value = str(value).upper()
                    if value not in terms.df["code"].values:
                        raise ValueError(
                            f"Value for attribute «{name}» must be one of these terms: {', '.join(terms.df['code'].values)} VALUE:{value}"
                        )
            elif data_type == "SAMPLE":
                if "multiValue" in property_type and property_type["multiValue"] is True:
                    if type(value) != list:
                        value = [value]
            elif data_type in ("INTEGER", "BOOLEAN", "VARCHAR", "REAL", "ARRAY_INTEGER", "ARRAY_REAL", "ARRAY_STRING"):
                is_multi_value = property_type["multiValue"] is True if "multiValue" in property_type else False
                if not check_datatype(data_type, value, is_multi_value):
                    if is_multi_value:
                        raise ValueError(f"Multi-value property '{property_type['code']}' must be of type {data_type} - Provided value:{value}")
                    else:
                        raise ValueError(f"Property '{property_type['code']}' must be of type {data_type} - Provided value:{value}")
        self.__dict__[name] = value

    def __setitem__(self, key, value):
        """For properties that contain either a dot or a dash or any other non-valid method character,
        a user can use a key instead, e.g. sample.props['my-weird.property-name']
        """
        return setattr(self, key, value)

    def __dir__(self):
        return self._property_names

    def _repr_html_(self):
        def nvl(val, string=""):
            if val is None:
                return string
            elif val == "true":
                return True
            elif val == "false":
                return False
            return val

        html = """
            <table border="1" class="dataframe">
            <thead>
                <tr style="text-align: right;">
                <th>property</th>
                <th>value</th>
                <th>description</th>
                <th>type</th>
                <th>mandatory</th>
                </tr>
            </thead>
            <tbody>
        """

        for prop_name, prop in self._property_names.items():
            html += "<tr>"
            html += "".join(
                f"<td>{item}</td>"
                for item in [
                    prop_name,
                    nvl(getattr(self, prop_name, ""), ""),
                    prop.get("description"),
                    prop.get("dataType"),
                    prop.get("mandatory"),
                ]
            )
            html += "</tr>"

        html += """
            </tbody>
            </table>
        """
        return html

    def __repr__(self):
        def nvl(val, string=""):
            if val is None:
                return string
            elif val == "true":
                return True
            elif val == "false":
                return False
            return str(val)

        headers = ["property", "value", "mandatory"]

        lines = []
        for prop_name in self._property_names:
            lines.append([prop_name, nvl(getattr(self, prop_name, ""))])
        return tabulate(lines, headers=headers)
