# Copyright (c) 2019-2022 Manfred Moitzi
# License: MIT License
from __future__ import annotations
from typing import TYPE_CHECKING, Union, Optional
from ezdxf.lldxf.tags import Tags
from ezdxf.lldxf.const import DXFStructureError
from ezdxf.lldxf.const import (
    ACAD_XDICTIONARY,
    XDICT_HANDLE_CODE,
    APP_DATA_MARKER,
)

if TYPE_CHECKING:
    from ezdxf.audit import Auditor
    from ezdxf.document import Drawing
    from ezdxf.lldxf.tagwriter import AbstractTagWriter
    from ezdxf.entities import (
        Dictionary,
        DXFEntity,
        DXFObject,
        Placeholder,
        DictionaryVar,
        XRecord,
    )

__all__ = ["ExtensionDict"]


# Example for table head and -entries with extension dicts:
# AutodeskSamples\lineweights.dxf


class ExtensionDict:
    """Stores extended data of entities in app data 'ACAD_XDICTIONARY', app
    data contains just one entry to a hard-owned DICTIONARY objects, which is
    not shared with other entities, each entity copy has its own extension
    dictionary and the extension dictionary is destroyed when the owner entity
    is deleted from database.

    """

    __slots__ = ("_xdict",)

    def __init__(self, xdict: Union[str, Dictionary]):
        # 1st loading stage: xdict as string -> handle to dict
        # 2nd loading stage: xdict as DXF Dictionary
        self._xdict = xdict

    @property
    def dictionary(self) -> Dictionary:
        """Returns the underlying :class:`~ezdxf.entities.Dictionary` object."""
        xdict = self._xdict
        assert xdict is not None, "destroyed extension dictionary"
        assert not isinstance(xdict, str), f"dictionary handle #{xdict} not resolved"
        return xdict

    @property
    def handle(self) -> str:
        """Returns the handle of the underlying :class:`~ezdxf.entities.Dictionary`
        object.
        """
        return self.dictionary.dxf.handle

    def __getitem__(self, key: str):
        """Get self[key]."""
        return self.dictionary[key]

    def __setitem__(self, key: str, value):
        """Set self[key] to value.

        Only DXF objects stored in the OBJECTS section are allowed as content
        of the extension dictionary. DXF entities stored in layouts are not
        allowed.

        Raises:
            DXFTypeError: invalid DXF type

        """
        self.dictionary[key] = value

    def __delitem__(self, key: str):
        """Delete self[key], destroys referenced entity."""
        del self.dictionary[key]

    def __contains__(self, key: str):
        """Return `key` in self."""
        return key in self.dictionary

    def __len__(self):
        """Returns count of extension dictionary entries."""
        return len(self.dictionary)

    def keys(self):
        """Returns a :class:`KeysView` of all extension dictionary keys."""
        return self.dictionary.keys()

    def items(self):
        """Returns an :class:`ItemsView` for all extension dictionary entries as
        (key, entity) pairs. An entity can be a handle string if the entity
        does not exist.
        """
        return self.dictionary.items()

    def get(self, key: str, default=None) -> Optional[DXFEntity]:
        """Return extension dictionary entry `key`."""
        return self.dictionary.get(key, default)

    def discard(self, key: str) -> None:
        """Discard extension dictionary entry `key`."""
        return self.dictionary.discard(key)

    @classmethod
    def new(cls, owner_handle: str, doc: Drawing):
        xdict = doc.objects.add_dictionary(
            owner=owner_handle,
            # All data in the extension dictionary belongs only to the owner
            hard_owned=True,
        )
        return cls(xdict)

    def copy(self) -> ExtensionDict:
        """Deep copy of the extension dictionary all entries are virtual
        entities.
        """
        new_xdict = self.dictionary.copy()
        return ExtensionDict(new_xdict)

    @property
    def is_alive(self):
        """Returns ``True`` if the underlying :class:`~ezdxf.entities.Dictionary`
        object is not deleted.
        """
        # Can not check if _xdict (as handle or Dictionary) really exist:
        return self._xdict is not None

    @property
    def has_valid_dictionary(self):
        """Returns ``True`` if the underlying :class:`~ezdxf.entities.Dictionary`
        really exist and is valid.
        """
        xdict = self._xdict
        if xdict is None or isinstance(xdict, str):
            return False
        return xdict.is_alive

    def update_owner(self, handle: str) -> None:
        """Update owner tag of underlying :class:`~ezdxf.entities.Dictionary`
        object.

        Internal API.
        """
        assert self.is_alive, "destroyed extension dictionary"
        self.dictionary.dxf.owner = handle

    @classmethod
    def from_tags(cls, tags: Tags):
        assert tags is not None
        # Expected DXF structure:
        # [(102, '{ACAD_XDICTIONARY', (360, handle), (102, '}')]
        if len(tags) != 3 or tags[1].code != XDICT_HANDLE_CODE:
            raise DXFStructureError("ACAD_XDICTIONARY error.")
        return cls(tags[1].value)

    def load_resources(self, doc: Drawing) -> None:
        handle = self._xdict
        assert isinstance(handle, str)
        self._xdict = doc.entitydb.get(handle)  # type: ignore

    def export_dxf(self, tagwriter: AbstractTagWriter) -> None:
        assert self._xdict is not None
        xdict = self._xdict
        handle = xdict if isinstance(xdict, str) else xdict.dxf.handle
        tagwriter.write_tag2(APP_DATA_MARKER, ACAD_XDICTIONARY)
        tagwriter.write_tag2(XDICT_HANDLE_CODE, handle)
        tagwriter.write_tag2(APP_DATA_MARKER, "}")

    def destroy(self):
        """Destroy the underlying :class:`~ezdxf.entities.Dictionary` object."""
        if self.has_valid_dictionary:
            self._xdict.destroy()
        self._xdict = None

    def add_dictionary(self, name: str, hard_owned: bool = True) -> Dictionary:
        """Create a new :class:`~ezdxf.entities.Dictionary` object as
        extension dictionary entry `name`.
        """
        dictionary = self.dictionary
        doc = dictionary.doc
        assert doc is not None, "valid DXF document required"
        new_dict = doc.objects.add_dictionary(
            owner=dictionary.dxf.handle,
            hard_owned=hard_owned,
        )
        dictionary[name] = new_dict
        return new_dict

    def add_xrecord(self, name: str) -> XRecord:
        """Create a new :class:`~ezdxf.entities.XRecord` object as
        extension dictionary entry `name`.
        """
        dictionary = self.dictionary
        doc = dictionary.doc
        assert doc is not None, "valid DXF document required"
        xrecord = doc.objects.add_xrecord(dictionary.dxf.handle)
        dictionary[name] = xrecord
        return xrecord

    def add_dictionary_var(self, name: str, value: str) -> DictionaryVar:
        """Create a new :class:`~ezdxf.entities.DictionaryVar` object as
        extension dictionary entry `name`.
        """
        dictionary = self.dictionary
        doc = dictionary.doc
        assert doc is not None, "valid DXF document required"
        dict_var = doc.objects.add_dictionary_var(dictionary.dxf.handle, value)
        dictionary[name] = dict_var
        return dict_var

    def add_placeholder(self, name: str) -> Placeholder:
        """Create a new :class:`~ezdxf.entities.Placeholder` object as
        extension dictionary entry `name`.
        """
        dictionary = self.dictionary
        doc = dictionary.doc
        assert doc is not None, "valid DXF document required"
        placeholder = doc.objects.add_placeholder(dictionary.dxf.handle)
        dictionary[name] = placeholder
        return placeholder

    def link_dxf_object(self, name: str, obj: DXFObject) -> None:
        """Link `obj` to the extension dictionary as entry `name`.

        Linked objects are owned by the extensions dictionary and therefore
        cannot be a graphical entity, which have to be owned by a
        :class:`~ezdxf.layouts.BaseLayout`.

        Raises:
            DXFTypeError: `obj` has invalid DXF type

        """
        self.dictionary.link_dxf_object(name, obj)
