from __future__ import annotations

import collections.abc
import typing
from pprint import pformat
from typing import Any, Dict

from factory_boss.errors import UndefinedValueError

if typing.TYPE_CHECKING:
    from factory_boss.entity import Entity
    from factory_boss.value_spec import Reference, ResolvedReference, ValueSpec


class Instance:
    """One instance of an entity, i.e., an object with zero or more fields."""

    def __init__(self, entity: "Entity"):
        self.entity = entity
        self.instance_values: Dict[str, InstanceValue] = {}
        self._dict: Dict[str, Any] = None

    def ivalue(self, name):
        return self.instance_values[name]

    def value(self, name):
        return self.instance_values[name].make_value()

    def relations(self) -> typing.List["InstanceValue"]:
        return [
            self.instance_values[n]
            for n, spec in self.entity.fields.items()
            if spec.type == "relation"
        ]

    def to_dict(self, with_related_objects: bool = True) -> Dict[str, Any]:
        """Return all values of this instance as a dictionary.

        Parameters
        ----------
        with_related_objects : bool, optional
            if True, related objects are placed into the dictionary. If False,
            they are not in the dictionary, but only the foreign key is set to the id
            (if applicable). Default: True

        Returns
        -------
        instance_dict : Dict[str, Any]
            A dictionary representing this instance.

        """
        undefined_values = [n for n, v in self.instance_values.items() if not v.defined]
        if undefined_values:
            raise UndefinedValueError(
                f"Cannot create dict because the following values of "
                f"Instance('{self.entity.name}') are undefined: {undefined_values}"
            )
        if self._dict is None:
            self._dict = {}
            for name, ivalue in self.instance_values.items():
                value = ivalue.make_value()
                if (
                    not with_related_objects
                    and self.entity.fields[name].type == "relation"
                ):
                    continue
                if isinstance(value, Instance):
                    value = value.to_dict()
                if isinstance(value, list):
                    # unwrap relation lists
                    value = [v.to_dict() for v in value]
                self._dict[name] = value
        return self._dict

    def __repr__(self):
        return f"Instance({pformat(self.instance_values)})"


class InstanceValue:
    """One field of an instance, i.e., one field.

    It is owned by one instance, has a specification and has a value unless it's
    undefined.

    Parameters:
    -----------
    context : Instance
        instance which is used to resolve references. Default: `owner`
    """

    def __init__(
        self, name: str, spec: "ValueSpec", owner: Instance, context: Instance = None
    ):
        self.name = name
        self.owner = owner
        self.spec = spec
        self._value = None
        self.defined = False
        self.context = context or owner
        self._resolved_references: Dict[Reference, ResolvedReference] = {}

    def value(self):
        if self.defined:
            return self._value
        else:
            raise UndefinedValueError(f"value of {self.name} is not defined")

    def make_value(self):
        if not self.defined:
            self.defined = True
            self._value = self.spec.generate_value(self.resolved_references())
        return self._value

    def override_value(self, value):
        self._value = value
        self.defined = True

    def add_resolved_reference(self, reference: "ResolvedReference"):
        self._resolved_references[reference.parent] = reference

    def resolved_references(self):
        return self._resolved_references

    def unresolved_references(self):
        return self.spec.references()

    def __repr__(self):
        if self.defined:
            if isinstance(self._value, collections.abc.Collection):
                value_str = f"{len(self._value)} entries"
            else:
                value_str = self._value
        else:
            value_str = "<UNDEFINED>"
        return f"InstanceValue_{id(self)}({self.name}, {value_str})"


class CopiedInstanceValue(InstanceValue):
    def __init__(self, name, owner, copy_of: InstanceValue):
        self.name = name
        self.owner = owner
        self.copy_of = copy_of
        self.reference = Reference("<autogenerated>")
        self.resolved_reference = ResolvedReference(None, copy_of)

    def value(self):
        return self.copy_of.value()

    def make_value(self):
        return self.copy_of.make_value()

    def unresolved_references(self):
        return []

    def resolved_references(self):
        return {self.reference: self.resolved_reference}
