# Licensed under the GNU Lesser General Public License v3.0.
# ezudesign Copyright (C) 2023 numlinka.

__all__ = [
    "ConfigurationBaseException",
    "ConfigNameDoesNotExist",
    "ConfigNameAlreadyExist",
    "ConfigValueOutOfRange",
    "ConfigFileTypeNotSpecified",
    "ConfigFileTypeNotSupported",
    "ValueRange",
    "IntVariable",
    "FloatVariable",
    "StrVariable",
    "Configuration"
]

# std
import copy
import json
import inspect
import threading

from base64 import b64encode, b64decode
from typing import Iterable, Mapping, Optional, Self, Any
from dataclasses import dataclass, field

# site
from typex import Static


class ConfigurationBaseException (Exception): ...
class ConfigNameDoesNotExist (ConfigurationBaseException): ...
class ConfigNameAlreadyExist (ConfigurationBaseException): ...
class ConfigValueOutOfRange (ConfigurationBaseException): ...
class ConfigFileTypeNotSpecified (ConfigurationBaseException): ...
class ConfigFileTypeNotSupported (ConfigurationBaseException): ...


class ConfigFileType (Static):
    JSON = "json"


@dataclass
class ValueRange (object):
    min: int | float
    max: int | float
    round: int = field(default=0)

    def __post_init__(self):
        if not isinstance(self.min, (int, float)):
            raise TypeError(f"Expected `min` to be `int` or `float`, but got {type(self.min)}.")

        if not isinstance(self.max, (int, float)):
            raise TypeError(f"Expected `max` to be `int` or `float`, but got {type(self.max)}.")

        if not isinstance(self.round, int):
            raise TypeError(f"Expected `round` to be `int`, but got {type(self.round)}.")

        if self.round < 0:
            raise ValueError(f"Expected `round` to be greater than or equal to 0, but got {self.round}.")

        if self.min > self.max:
            raise ValueError(f"Expected `min` to be less than or equal to `max`, but got {self.min} and {self.max}.")


@dataclass
class LastState (object):
    filepath: Optional[str] = field(default=None)
    filetype: Optional[str] = field(default=None)
    encoding: str = field(default="utf-8")
    base64: bool = field(default=False)


class MetaInt (type):
    def __instancecheck__(cls, instance) -> bool:
        if isinstance(instance, int):
            return True

        return super().__instancecheck__(instance)


class MetaFloat (type):
    def __instancecheck__(cls, instance) -> bool:
        if isinstance(instance, float):
            return True

        return super().__instancecheck__(instance)


class MetaStr (type):
    def __instancecheck__(cls, instance) -> bool:
        if isinstance(instance, str):
            return True

        return super().__instancecheck__(instance)


class Variable (object):
    _default = None
    _ranges = None
    default = None
    ranges = None

    def __new__(cls, value: Optional[int | float | str] = None, *args, **kwargs) -> Self:
        if cls is Variable:
            raise TypeError("Cannot create an instance of the abstract class Variable.")

        if value is None:
            value = cls._default

        instance = super(Variable, cls).__new__(cls, value)

        return instance

    def __init__(self,
                 value: Optional[int | float | str] = None,
                 default: Optional[int | float | str] = None,
                 ranges: Optional[Iterable[int | float | str] | ValueRange] = None
                 ) -> None:
        self.set_default(default if default is not None else (value if value is not None else self._default))
        self.set_ranges(ranges)

    def _set_attribute(self, stem: "ConfigurationControl", name: str) -> None:
        self.__stem = stem
        self.__name = name

    def set_default(self, value: Optional[int | float | str]) -> None:
        if value is None:
            return

        if not isinstance(value, type(self)):
            raise TypeError(f"Expected `value` to be `{type(self)}`, but got {type(value)}.")

        self._default = value

    def set_ranges(self, ranges: Optional[Iterable[int | float | str] | ValueRange]) -> None:
        if ranges is None:
            return

        if isinstance(ranges, Iterable):
            if not all(isinstance(x, type(self)) for x in ranges):
                raise TypeError(f"Expected all elements in `ranges` to be `{type(self)}`, but got {ranges}.")

        elif isinstance(ranges, ValueRange):
            ...  # ValueRange is already validated in its __post_init__

        else:
            raise TypeError(f"Expected `ranges` to be Iterable or ValueRange, but got {type(ranges)}.")

        self._ranges = copy.copy(ranges)

    def new_value(self, value: int | float | str) -> Self:
        if not isinstance(value, type(self)):
            raise TypeError(f"Expected `value` to be `{type(self)}`, but got {type(value)}.")

        cls = type(self)
        instance = cls(value, self._default, self._ranges)
        return instance

    def is_valid(self, value: int | float | str) -> bool:
        if not isinstance(value, type(self)):
            return False

        if self._ranges is None:
            return True

        if isinstance(self._ranges, ValueRange) and isinstance(value, (int, float)):
            if self._ranges.min <= value <= self._ranges.max:
                return True

        elif isinstance(self._ranges, Iterable):
            if value in self._ranges:
                return True

        return False

    @property
    def value(self) -> Self:
        return self

    @property
    def stem(self) -> "Configuration":
        return self.__stem.surface

    @property
    def name(self) -> str:
        return self.__name

    def set(self, value: int | float | str) -> None:
        self.__stem.set(self.__name, value)


class IntVariable (Variable, int, metaclass=MetaInt):
    _default: int = 0
    _ranges: Optional[Iterable[int] | ValueRange] = None

    def __init__(self,
                 value: Optional[int] = None,
                 default: Optional[int] = None,
                 ranges: Optional[Iterable[int] | ValueRange] = None
                 ) -> None:
        super().__init__(value, default, ranges)

    @property
    def default(self) -> Optional[int]:
        return self._default

    @property
    def ranges(self) -> Optional[Iterable[int] | ValueRange]:
        return copy.copy(self._ranges)


class FloatVariable (Variable, float, metaclass=MetaFloat):
    _default: float = 0.0
    _ranges: Optional[Iterable[float] | ValueRange] = None

    def __init__(self,
                 value: Optional[float] = None,
                 default: Optional[float] = None,
                 ranges: Optional[Iterable[float] | ValueRange] = None
                 ) -> None:
        super().__init__(value, default, ranges)

    @property
    def default(self) -> Optional[float]:
        return self._default

    @property
    def ranges(self) -> Optional[Iterable[float] | ValueRange]:
        return copy.copy(self._ranges)


class StrVariable (Variable, str, metaclass=MetaStr):
    _default: str = ""
    _ranges: Optional[Iterable[str]] = None

    def __init__(self,
                 value: Optional[str] = None,
                 default: Optional[str] = None,
                 ranges: Optional[Iterable[str]] = None
                 ) -> None:
        super().__init__(value, default, ranges)

    @property
    def default(self) -> Optional[str]:
        return self._default

    @property
    def ranges(self) -> Optional[Iterable[str]]:
        return copy.copy(self._ranges)


class Configuration (object):
    def __init__(self) -> None:
        self.ctrl = ConfigurationControl(self)
        items = [x for x in dir(self) if not x.startswith("_") and x != "ctrl"]
        getattribute = super().__getattribute__
        for name in items:
            item = getattribute(name)
            if not isinstance(item, Variable):
                continue

            self.ctrl.new(name, item)

    def __setattr__(self, name, value: Any) -> None:
        if name.startswith("_") or name == "ctrl":
            return super().__setattr__(name, value)

        self.ctrl.set(name, value)

    def __setitem__(self, name, value) -> None:
        self.ctrl.set(name, value)

    def __getitem__(self, name) -> Variable | int | float | str:
        return self.ctrl.get(name)

    def __getattribute__(self, name) -> Any:
        if name.startswith("_") or name == "ctrl":
            return super().__getattribute__(name)

        return self.ctrl.get(name)


class ConfigurationControl (object):
    def __init__(self, surface: Configuration) -> None:
        self.surface = surface
        self._lock = threading.RLock()
        self.__items: dict[str, Variable] = {}
        self.__invalid: dict[str, int | float | str] = {}
        self.__last = LastState()

    @property
    def last_filepath(self) -> Optional[str]:
        with self._lock:
            return self.__last.filepath

    @property
    def last_filetype(self) -> Optional[str]:
        with self._lock:
            return self.__last.filetype

    @property
    def last_encoding(self) -> str:
        with self._lock:
            return self.__last.encoding

    @property
    def last_base64(self) -> bool:
        with self._lock:
            return self.__last.base64

    def new(self, name: str, item: Variable) -> None:
        if not isinstance(name, str):
            raise TypeError(f"Expected `name` to be `str`, but got {type(name)}.")

        if inspect.isclass(item) and not issubclass(item, Variable):
            raise TypeError(f"Expected `item` to be a subclass of `Variable`, but got {type(item)}.")

        with self._lock:
            if name in self.__items:
                raise ConfigNameAlreadyExist(f"The config name `{name}` already exists.")

            self.__items[name] = item
            item._set_attribute(self, name)

    def set(self, name: str, value: Optional[int | float | str] = None) -> None :
        if not isinstance(name, str):
            raise TypeError(f"Expected `name` to be `str`, but got {type(name)}.")

        if value is None:
            return

        with self._lock:
            if name not in self.__items:
                self.__invalid[name] = value
                return

            item = self.__items[name]
            if not item.is_valid(value):
                raise ConfigValueOutOfRange(f"The value `{value}` is out of range for the config key `{name}`.")

            new_item = item.new_value(value)
            new_item._set_attribute(self, name)
            self.__items[name] = new_item

    def set_ranges(self, name: str, ranges: Iterable[int | float | str] | ValueRange):
        if not isinstance(name, str):
            raise TypeError(f"Expected `name` to be `str`, but got {type(name)}.")

        with self._lock:
            if name not in self.__items:
                raise ConfigNameDoesNotExist(f"The config name `{name}` does not exist.")

            item = self.__items[name]
            item.set_ranges(ranges)

    def get(self, name: str) -> Variable | int | float | str:
        if not isinstance(name, str):
            raise TypeError(f"Expected `name` to be `str`, but got {type(name)}.")

        with self._lock:
            if name in self.__items:
                return self.__items[name]

            elif name in self.__invalid:
                return self.__invalid[name]

            raise ConfigNameDoesNotExist(f"The config name `{name}` does not exist.")

    def get_default(self, name: str) -> Optional[int | float | str]:
        if not isinstance(name, str):
            raise TypeError(f"Expected `name` to be `str`, but got {type(name)}.")

        with self._lock:
            if name not in self.__items:
                raise ConfigNameDoesNotExist(f"The config name `{name}` does not exist.")

            item = self.__items[name]
            return item.default

    def get_ranges(self, name: str) -> Optional[Iterable[int | float | str] | ValueRange]:
        if not isinstance(name, str):
            raise TypeError(f"Expected `name` to be `str`, but got {type(name)}.")

        with self._lock:
            if name not in self.__items:
                raise ConfigNameDoesNotExist(f"The config name `{name}` does not exist.")

            item = self.__items[name]
            return item.ranges

    def load_dict(self, data: Mapping[str, int | float | str]) -> list[str]:
        if not isinstance(data, Mapping):
            raise TypeError(f"Expected `data` to be `Mapping`, but got {type(data)}.")

        lst = []

        for name, value in data.items():
            try:
                self.set(name, value)

            except Exception as _:
                lst.append(name)

        return lst

    def load_json(self, filepath: str, encoding: str = "utf-8", *, base64: bool = False) -> list[str]:
        with open(filepath, "r", encoding=encoding) as file_obj:
            content = file_obj.read()
            if base64:
                content = b64decode(content).decode(encoding)
            data = json.loads(content)

        with self._lock:
            self.__last.filepath = filepath
            self.__last.filetype = ConfigFileType.JSON
            self.__last.encoding = encoding
            self.__last.base64 = base64

        return self.load_dict(data)

    def save(self, filepath: Optional[str] = None, filetype: Optional[str] = None,
             encoding: Optional[str] = None, *, base64: Optional[bool] = None) -> None:
        if not isinstance(filepath, str) and filepath is not None:
            raise TypeError(f"Expected `filepath` to be `str` or None, but got {type(filepath)}.")

        if not isinstance(filetype, str) and filetype is not None:
            raise TypeError(f"Expected `filetype` to be `str` or None, but got {type(filetype)}.")

        if not isinstance(encoding, str) and encoding is not None:
            raise TypeError(f"Expected `encoding` to be `str` or None, but got {type(encoding)}.")

        if not isinstance(base64, bool) and base64 is not None:
            raise TypeError(f"Expected `base64` to be `bool` or None, but got {type(base64)}.")

        filepath = filepath or self.last_filepath
        filetype = filetype or self.last_filetype
        encoding = encoding or self.last_encoding
        base64 = self.last_base64 if base64 is None else base64

        if filepath is None or filetype is None:
            raise ConfigFileTypeNotSpecified(f"The config file type is not specified.")

        match filetype:
            case ConfigFileType.JSON:
                self.save_json(filepath, encoding, base64=base64)

            case _:
                raise ConfigFileTypeNotSupported(f"The config file type `{filetype}` is not supported.")

    def save_dict(self) -> dict[str, int | float | str | Variable]:
        with self._lock:
            return {name: item.value for name, item in self.__items.items()}

    def save_json(self, filepath: str, encoding: str = "utf-8", *, base64: bool = False) -> None:
        with self._lock:
            data = {name: item.value for name, item in self.__items.items()}

        if not base64:
            content = json.dumps(data, ensure_ascii=False, sort_keys=False, indent=4)

            with open(filepath, "w", encoding=encoding) as fobj:
                fobj.write(content)
            return

        content = json.dumps(data, ensure_ascii=False, sort_keys=False)
        content = b64encode(content.encode(encoding))
        with open(filepath, "wb") as fobj:
            fobj.write(content)


