#!/usr/bin/env python
# -*- coding: UTF-8 -*-
#
# Copyright 2022-2023 NXP
#
# SPDX-License-Identifier: BSD-3-Clause
"""Module to manage used databases in SPSDK."""


import dataclasses
from dataclasses import dataclass
from typing import Any, Dict, List, Optional

from spsdk.exceptions import SPSDKTypeError, SPSDKValueError
from spsdk.utils.misc import find_first, load_configuration


@dataclass
class Revision:
    """Revision dataclass represents a single device revision."""

    name: str
    is_latest: bool
    data_file: Optional[str] = None
    attributes: dict = dataclasses.field(default_factory=dict)

    @staticmethod
    def load(name: str, revision: dict, is_latest: bool = False) -> "Revision":
        """Loads the revision from dictionary.

        :param name: The revision name.
        :param revision: Revision data.
        :param is_latest: Is latest device revision.
        :raises SPSDKTypeError: In case the revision is not a dictionary type
        :return: The Revision object.
        """
        if not isinstance(revision, dict):
            raise SPSDKTypeError(f"Revision {name} must be a dict type.")
        rev = Revision(name=name, is_latest=is_latest)
        rev.data_file = revision.get("data_file")
        rev.attributes = revision.get("attributes", {})
        return rev


class Revisions(List[Revision]):
    """List of device revisions."""

    @property
    def revision_names(self) -> List[str]:
        """Get list of revisions.

        :return: List of all supported device version.
        """
        return [rev.name for rev in self]

    def get(self, name: Optional[str] = None) -> Revision:
        """Get the revision by its name.

        If name is not specified, or equal to 'latest', then the latest revision is returned.

        :param name: The revision name.
        :return: The Revision object.
        """
        if name is None or name == "latest":
            return self.get_latest()
        return self.get_by_name(name)

    def get_by_name(self, name: str) -> Revision:
        """Get the required revision.

        :param name: Required revision name
        :raises SPSDKValueError: Incase of invalid device or revision value.
        :return: The Revision object.
        """
        revision = find_first(self, lambda rev: rev.name == name)
        if not revision:
            raise SPSDKValueError(f"Requested revision {revision} is not supported.")
        return revision

    def get_latest(self) -> Revision:
        """Get latest revision for device.

        :raises SPSDKValueError: Incase of there is no latest revision defined.
        :return: The Revision object.
        """
        revision = find_first(self, lambda rev: rev.is_latest)
        if not revision:
            raise SPSDKValueError("No latest revision has been defined.")
        return revision

    @staticmethod
    def load(revisions: dict, latest: Optional[str] = None) -> "Revisions":
        """Loads the revisions list from dictionary.

        :param revisions: Revisions data.
        :param latest: Name of latest revision.
        :raises SPSDKTypeError: In case the revisions parameter is not a dictionary type
        :return: The Revisions object.
        """
        if not isinstance(revisions, dict):
            raise SPSDKTypeError(f"Revisions must be a dictionary type, not {type(revisions)}.")
        return Revisions(
            Revision.load(name, val, is_latest=name == latest) for name, val in revisions.items()
        )


@dataclass
class Device:
    """Device dataclass represents a single device."""

    name: str
    device_alias: Optional[str] = None
    revisions: Revisions = dataclasses.field(default_factory=Revisions)
    attributes: dict = dataclasses.field(default_factory=dict)

    @staticmethod
    def load(name: str, device: dict) -> "Device":
        """Loads the device from dictionary.

        :param name: The name of device.
        :param device: Device data.
        :return: The Device object.
        """
        dev = Device(name=name)
        if "device_alias" in device:
            dev.device_alias = device["device_alias"]
        dev.revisions = Revisions.load(
            device.get("revisions", {}), latest=device.get("latest", None)
        )
        dev.attributes = device.get("attributes", {})
        return dev


class Devices(List[Device]):
    """List of devices."""

    @property
    def device_names(self) -> List[str]:
        """Get the list of all device names."""
        return [dev.name for dev in self]

    def get_by_name(self, name: str) -> Device:
        """Return database device structure.

        :param name: String Key with device name.
        :raises SPSDKValueError: In case the device with given name does not exist
        :return: Dictionary device configuration structure or None:
        """
        dev = find_first(self, lambda dev: dev.name == name)
        if not dev:
            raise SPSDKValueError(f"The device with name {name} is not in the database.")
        if dev.device_alias:
            return self.get_by_name(dev.device_alias)
        return dev

    @staticmethod
    def load(devices: dict) -> "Devices":
        """Loads the device from dictionary.

        :param devices: Devices data.
        :return: The Devices object.
        """
        return Devices(Device.load(name, val) for name, val in devices.items())

    @staticmethod
    def load_from_file(path: str) -> "Devices":
        """Loads the device from database file.

        :param path: path to database file.
        :return: The Devices object.
        """
        config: Dict[str, Any] = load_configuration(path)
        return Devices.load(config["devices"])


class Database:
    """Class that helps manage used databases in SPSDK."""

    def __init__(self, path: str) -> None:
        """Register Configuration class constructor.

        :param path: The path to configuration JSON file.
        """
        self.path = path
        config: Dict[str, Any] = load_configuration(path)
        try:
            self._devices = Devices.load(config["devices"])
        except (SPSDKValueError, SPSDKTypeError) as exc:
            if exc.description:
                exc.description += f"File path: {self.path}"
            raise exc
        self.attributes: dict = config.get("attributes", {})

    @classmethod
    def get_devices(cls, path: str) -> Devices:
        """Classmethod to get list of supported devices.

        :param path: Path to database file.
        :return: List of all supported devices.
        """
        return Devices.load_from_file(path)

    @property
    def devices(self) -> Devices:
        """Get the list of devices stored in the database."""
        return self._devices

    def get_device_value(
        self,
        key: str,
        device: Optional[str] = None,
        revision: str = "latest",
        default: Optional[Any] = None,
    ) -> Any:
        """Return any parameter by key.

        :param key: The Key of the parameter to be returned.
        :param device: The device name.
        :param revision: The revision of the silicon.
        :param default: The default Value in case that is not specified in config file.
        :return: The Value of parameter by handled Key.
        """
        if device and device in self.devices.device_names:
            dev = self.devices.get_by_name(device)
            try:
                rev = dev.revisions.get(revision)
            except SPSDKValueError:
                rev = None
            if rev and key in rev.attributes:
                return rev.attributes[key]
            if key in dev.attributes:
                return dev.attributes[key]
        return self.attributes.get(key, default)
