"""
File consists of several classes for the different elements of a device.
"""
from __future__ import annotations
from enum import Enum, auto
import uuid
from typing import List, Dict, Optional
import json


# TODO
# - Docstrings
# - Beschreibung von Teilen (-> properties) -> Raus aus dem Konstruktor rein in ein dict. (Deep-Copy)
#   - Erstmal als Shallow Copy umgesetzt, wir verwenden momentan keine nested dicts
# - Minimalbeispiel für KPIs -> halb umgesetzt -> mit get_components veranschaulichen
# - Erlaube Clone bei Assembly (jedes child muss durch durch Klon ersetzt werden)
# - Änderungen an Beispiel umsetzen

# -> Integriere AggregationLayer und die Funktionen in die Klassen (evtl. per Vererbung?) -> Nä Semester
# - Erlaube Listen bei add_component und add_assembly  (-> Nä Semester)
# - Gute String Darstellung -> Ist so schon ok bisher? -> Nä Semester
# - Export als GraphViz -> Nä Semeseter

class AggregationLayer(Enum):
    SYSTEM = auto()
    ASSEMBLY = auto()
    SUBASSEMBLY = auto()
    COMPONENT = auto()


class LegoComponent:
    def __init__(self, label: Optional[str] = None, datasheet: Optional[dict] = None, *more_properties: dict, **kwargs) -> None:
        self.uuid: uuid.UUID = uuid.uuid4()
        self.parent: None | LegoAssembly = None
        self.layer: AggregationLayer = AggregationLayer.COMPONENT
        self.properties: dict = {}
        if label is not None:
            self.properties['label'] = label
        if datasheet is not None:
            self.properties.update(datasheet)
            self.properties[]
        for prop in more_properties:
            if isinstance(prop, dict):
                self.properties.update(prop)
            else:
                raise ValueError(f"Unexpected argument type: {type(more_properties)}")
        for key, value in kwargs.items():
            self.properties[key] = value




    def clone(self) -> LegoComponent:
        clone = LegoComponent(
            None,
            self.properties,
        )
        return clone

    def get_root_assembly(self):
        if self.parent is None:
            return None
        current_assembly = self.parent
        while current_assembly.parent is not None:
            current_assembly = current_assembly.parent
        return current_assembly

    def to_dict(self) -> Dict:
        ATTRIBUTES = [
            "uuid",
            "label",
            "category",
            "lego_id",
            "cost",
            "mass",
            "delivery_time",
            "layer",
            "properties",
        ]
        dict_ = {}
        # store attributes
        for attr in ATTRIBUTES:
            dict_[attr] = getattr(self, attr)

        dict_ = {"component": dict_}
        return dict_

    # TODO good string representation
    def __str__(self):
        return self.__repr__()
        return (
            f"Item(id={self.uuid}, item_number={self.lego_id}, "
            f"mass={self.mass}, delivery_time={self.delivery_time}, "
            f"parent_id={self.parent})"
        )

    # TODO good repr representation
    def __repr__(self):
        return f"LegoComponent {self.properties['label']} [{self.uuid}]"


class LegoAssembly:
    def __init__(self, layer: AggregationLayer, label: Optional[str] = None, *properties: dict , **kwargs) -> None:
        self.uuid: uuid.UUID = uuid.uuid4()
        self.parent: None | LegoAssembly = None
        self.properties: dict = {}
        if label is not None:
            self.properties['label'] = label
        self.layer: AggregationLayer = layer
        self.properties.update(properties)
        self.components: List[LegoComponent] = []
        self.assemblies: List[LegoAssembly] = []

    def add_component(self, component: LegoComponent) -> None:
        if not isinstance(component, LegoComponent):
            raise TypeError(
                f"Argument should be of type {LegoComponent.__name__}, "
                f"got {type(component).__name__} instead."
            )
        if self.get_root_assembly().contains_uuid(component.uuid):
            raise AssertionError(
                f"This assembly or a subassembly already contains the component with ID "
                f"{component.uuid}."
            )
        component.parent = self
        self.components.append(component)

    def add_assembly(self, assembly: LegoAssembly) -> None:
        if not isinstance(assembly, LegoAssembly):
            raise TypeError(
                f"Argument should be of type {LegoAssembly.__name__}, "
                f"got {type(assembly).__name__} instead."
            )
        if self.get_root_assembly().contains_uuid(assembly.uuid):
            raise AssertionError(
                f"This assembly or a subassembly already contains the assembly with ID "
                f"{assembly.uuid}."
            )
        assembly.parent = self
        self.assemblies.append(assembly)

    def children(self) -> Dict[str, List[LegoComponent] | List[LegoAssembly]]:
        return {"components": self.components, "assemblies": self.assemblies}

    def get_component_list(self, max_depth: int = -1) -> List[LegoComponent]:
        component_list = []
        component_list.extend(self.components)
        if max_depth > 0:
            for assembly in self.assemblies:
                component_list.extend(assembly.get_component_list(max_depth - 1))
        return component_list

    def get_root_assembly(self) -> LegoAssembly:
        current_assembly = self
        while current_assembly.parent is not None:
            current_assembly = current_assembly.parent
        return current_assembly

    def contains_uuid(self, uuid_: uuid.UUID):
        # check component ids
        component_ids = list(map(lambda c: c.uuid, self.components))
        if uuid_ in component_ids:
            return True
        # check assembly ids
        assembly_ids = list(map(lambda a: a.uuid, self.assemblies))
        if uuid_ in assembly_ids:
            return True
        # recursively check assemblies
        for assembly in self.assemblies:
            if assembly.contains_uuid(uuid_):
                return True
        return False

    def to_dict(self) -> Dict:
        ATTRIBUTES = ["uuid", "label", "layer", "properties"]
        dict_ = {}
        # store attributes
        for attr in ATTRIBUTES:
            dict_[attr] = getattr(self, attr)
        # store components
        dict_["components"] = [component.to_dict() for component in self.components]
        dict_["assemblies"] = [assembly.to_dict() for assembly in self.assemblies]

        return {"assembly": dict_}

    # TODO find good string representation
    def __repr__(self):
        return f"LegoAssembly {self.properties['label']} [{self.uuid}]"


def print_assembly_tree(root, level=0, is_last=False):
    # print component
    assembly_padding = ""
    if level > 0:
        assembly_padding += "│   " * (level - 1)
        if is_last:
            assembly_padding += "└── "
        else:
            assembly_padding += "├── "
    print(f"{assembly_padding}{root}")
    # recursively print child components
    for i, assembly in enumerate(root.assemblies):
        is_last_ = i == len(root.assemblies) - 1 and len(root.components) == 0
        print_assembly_tree(assembly, level + 1, is_last_)
    # print items
    for i, item in enumerate(root.components):
        component_padding = "│   " * level if not is_last else "    "
        component_padding += "├── " if i < len(root.components) - 1 else "└── "
        print(f"{component_padding}{item}")

## TODO maybe add Components entry from dict?
class KPIEncoder(json.JSONEncoder):
    def default(self, o):
        if isinstance(o, uuid.UUID):
            return "kpi-" + str(o)
        if isinstance(o, (AggregationLayer)):
            return "kpi-" + o.label
        return super().default(o)