# Copyright (c) 2018-2022 Manfred Moitzi
# License: MIT License
from __future__ import annotations
from typing import (
    TYPE_CHECKING,
    Iterator,
    cast,
    Optional,
    TypeVar,
    Generic,
)
from ezdxf.lldxf.const import (
    DXFValueError,
    DXFKeyError,
    INVALID_NAME_CHARACTERS,
)
from ezdxf.lldxf.validator import make_table_key, is_valid_table_name

if TYPE_CHECKING:
    from ezdxf.document import Drawing
    from ezdxf.entities import DXFObject, Dictionary


def validate_name(name: str) -> str:
    name = name[:255]
    if not is_valid_table_name(name):
        raise DXFValueError(
            f"table name '{name}' contains invalid characters: {INVALID_NAME_CHARACTERS}"
        )
    return name


T = TypeVar("T", bound="DXFObject")


class ObjectCollection(Generic[T]):
    """
    Note:
    ObjectCollections may contain entries where the name stored in the entity as
    "name" attribute diverges from the key in the DICTIONARY object e.g. MLEADERSTYLE
    collection may have entries for "Standard" and "Annotative" but both MLEADERSTYLE
    objects have the name "Standard".

    """
    def __init__(
        self,
        doc: Drawing,
        dict_name: str = "ACAD_MATERIAL",
        object_type: str = "MATERIAL",
    ):
        self.doc: Drawing = doc
        self.object_dict_name = dict_name
        self.object_type: str = object_type
        self.object_dict: Dictionary = doc.rootdict.get_required_dict(
            dict_name
        )

    def update_object_dict(self) -> None:
        self.object_dict = self.doc.rootdict.get_required_dict(self.object_dict_name)

    def create_required_entries(self) -> None:
        pass

    def __iter__(self) -> Iterator[tuple[str, T]]:
        return self.object_dict.items()

    def __len__(self) -> int:
        return len(self.object_dict)

    def __contains__(self, name: str) -> bool:
        return self.has_entry(name)

    def __getitem__(self, name: str) -> T:
        entry = self.get(name)
        if entry is None:
            raise DXFKeyError(name)
        return entry

    @property
    def handle(self) -> str:
        """Returns the DXF handle of the DICTIONARY object."""
        return self.object_dict.dxf.handle

    @property
    def is_hard_owner(self) -> bool:
        """Returns ``True`` if the collection is hard owner of entities.
        Hard owned entities will be destroyed by deleting the dictionary.
        """
        return self.object_dict.is_hard_owner

    def has_entry(self, name: str) -> bool:
        return self.get(name) is not None

    def is_unique_name(self, name: str) -> bool:
        name = make_table_key(name)
        for entry_name in self.object_dict.keys():
            if make_table_key(entry_name) == name:
                return False
        return True

    def get(self, name: str, default: Optional[T] = None) -> Optional[T]:
        """Get object by name. Object collection entries are case-insensitive.

        Args:
            name: object name as string
            default: default value

        """
        name = make_table_key(name)
        for entry_name, obj in self.object_dict.items():
            if make_table_key(entry_name) == name:
                return obj
        return default

    def new(self, name: str) -> T:
        """Create a new object of type `self.object_type` and store its handle
        in the object manager dictionary.  Object collection entry names are
        case-insensitive and limited to 255 characters.

        Args:
            name: name of new object as string

        Returns:
            new object of type `self.object_type`

        Raises:
            DXFValueError: if object name already exist or is invalid

        (internal API)

        """
        name = validate_name(name)
        if not self.is_unique_name(name):
            raise DXFValueError(
                f"{self.object_type} entry {name} already exists."
            )
        return self._new(name, dxfattribs={"name": name})

    def duplicate_entry(self, name: str, new_name: str) -> T:
        """Returns a new table entry `new_name` as copy of `name`,
        replaces entry `new_name` if already exist.

        Raises:
             DXFValueError: `name` does not exist

        """
        entry = self.get(name)
        if entry is None:
            raise DXFValueError(f"entry '{name}' does not exist")
        new_name = validate_name(new_name)
        # remove existing entry
        existing_entry = self.get(new_name)
        if existing_entry is not None:
            self.delete(new_name)

        entitydb = self.doc.entitydb
        if entitydb:
            new_entry = entitydb.duplicate_entity(entry)
        else:  # only for testing!
            new_entry = entry.copy()
        if new_entry.dxf.is_supported("name"):
            new_entry.dxf.name = new_name
        self.object_dict.add(new_name, new_entry)  # type: ignore
        owner_handle = self.object_dict.dxf.handle
        new_entry.dxf.owner = owner_handle
        new_entry.set_reactors([owner_handle])
        return new_entry  # type: ignore

    def _new(self, name: str, dxfattribs: dict) -> T:
        objects = self.doc.objects
        assert objects is not None

        owner = self.object_dict.dxf.handle
        dxfattribs["owner"] = owner
        obj = objects.add_dxf_object_with_reactor(
            self.object_type, dxfattribs=dxfattribs
        )
        self.object_dict.add(name, obj)
        return cast(T, obj)

    def delete(self, name: str) -> None:
        objects = self.doc.objects
        assert objects is not None

        obj = self.get(name)  # case insensitive
        if obj is not None:
            # The underlying DICTIONARY is not case-insensitive implemented,
            # get real object name if available
            if obj.dxf.is_supported("name"):
                name = obj.dxf.get("name", name)
            self.object_dict.discard(name)
            objects.delete_entity(obj)

    def clear(self) -> None:
        """Delete all entries."""
        self.object_dict.clear()
