# Copyright (c) 2019-2022 Manfred Moitzi
# License: MIT License
""" :class:`DXFEntity` is the super class of all DXF entities.

The current entity system uses the features of the latest supported DXF version.

The stored DXF version of the document is used to warn users if they use
unsupported DXF features of the current DXF version.

The DXF version of the document can be changed at runtime or overridden by
exporting, but unsupported DXF features are just ignored by exporting.

Ezdxf does no conversion between different DXF versions, this package is
still not a CAD application.

"""
from __future__ import annotations
from typing import (
    TYPE_CHECKING,
    Any,
    Iterable,
    Iterator,
    Optional,
    Type,
    TypeVar,
    Callable,
)
import copy
import logging
import uuid
from ezdxf import options
from ezdxf.lldxf import const
from ezdxf.lldxf.tags import Tags
from ezdxf.lldxf.types import DXFTag
from ezdxf.lldxf.extendedtags import ExtendedTags
from ezdxf.lldxf.attributes import DXFAttr, DXFAttributes, DefSubclass
from ezdxf.tools import set_flag_state
from . import factory
from .appdata import AppData, Reactors
from .dxfns import DXFNamespace, SubclassProcessor
from .xdata import XData
from .xdict import ExtensionDict

if TYPE_CHECKING:
    from ezdxf.audit import Auditor
    from ezdxf.document import Drawing
    from ezdxf.entities import DXFGraphic, Insert
    from ezdxf.lldxf.attributes import DXFAttr
    from ezdxf.lldxf.tagwriter import AbstractTagWriter
    from ezdxf.math import Matrix44
    from ezdxf import xref

__all__ = ["DXFEntity", "DXFTagStorage", "base_class", "SubclassProcessor"]
logger = logging.getLogger("ezdxf")

# Dynamic attributes created only at request:
# Source entity of a copy or None if not a copy:
DYN_SOURCE_OF_COPY_ATTRIBUTE = "_source_of_copy"
# UUID created on demand by uuid.uuid4()
DYN_UUID_ATTRIBUTE = "_uuid"
# Source block reference, which created the virtual entity, bound entities can
# not have such an attribute:
DYN_SOURCE_BLOCK_REFERENCE_ATTRIBUTE = "_source_block_reference"

base_class: DefSubclass = DefSubclass(
    None,
    {
        "handle": DXFAttr(5),
        # owner: Soft-pointer ID/handle to owner BLOCK_RECORD object
        # This tag is not supported by DXF R12, but is used intern to unify entity
        # handling between DXF R12 and DXF R2000+
        # Do not write this tag into DXF R12 files!
        "owner": DXFAttr(330),
        # Application defined data can only appear here:
        # 102, {APPID ... multiple entries possible DXF R12?
        # 102, {ACAD_REACTORS ... one entry DXF R2000+, optional
        # 102, {ACAD_XDICTIONARY  ... one entry DXF R2000+, optional
    },
)

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


class DXFEntity:
    """Common super class for all DXF entities."""

    DXFTYPE = "DXFENTITY"  # storing as class var needs less memory
    DXFATTRIBS = DXFAttributes(base_class)  # DXF attribute definitions

    # Default DXF attributes are set at instantiating a new object, the
    # difference to attribute default values is, that this attributes are
    # really set, this means there is an real object in the dxf namespace
    # defined, where default attribute values get returned on access without
    # an existing object in the dxf namespace.
    DEFAULT_ATTRIBS: dict[str, Any] = {}
    MIN_DXF_VERSION_FOR_EXPORT = const.DXF12

    def __init__(self) -> None:
        """Default constructor. (internal API)"""
        # Public attributes for package users
        self.doc: Optional[Drawing] = None
        self.dxf: DXFNamespace = DXFNamespace(entity=self)

        # None public attributes for package users
        # create extended data only if needed:
        self.appdata: Optional[AppData] = None
        self.reactors: Optional[Reactors] = None
        self.extension_dict: Optional[ExtensionDict] = None
        self.xdata: Optional[XData] = None
        self.proxy_graphic: Optional[bytes] = None

        # For documentation:
        # Dynamic attributes, created only at request:
        # DYN_SOURCE_OF_COPY_ATTRIBUTE
        # DYN_UUID_ATTRIBUTE
        # DYN_SOURCE_BLOCK_REFERENCE_ATTRIBUTE

    @property
    def uuid(self) -> uuid.UUID:
        """Returns a UUID, which allows to distinguish even
        virtual entities without a handle.

        Dynamic attribute: this UUID will be created at the first request.

        """
        uuid_ = getattr(self, DYN_UUID_ATTRIBUTE, None)
        if uuid_ is None:
            uuid_ = uuid.uuid4()
            setattr(self, DYN_UUID_ATTRIBUTE, uuid_)
        return uuid_

    @classmethod
    def new(
        cls: Type[T],
        handle: Optional[str] = None,
        owner: Optional[str] = None,
        dxfattribs=None,
        doc: Optional[Drawing] = None,
    ) -> T:
        """Constructor for building new entities from scratch by ezdxf.

        NEW process:

        This is a trusted environment where everything is under control of
        ezdxf respectively the package-user, it is okay to raise exception
        to show implementation errors in ezdxf or usage errors of the
        package-user.

        The :attr:`Drawing.is_loading` flag can be checked to distinguish the
        NEW and the LOAD process.

        Args:
            handle: unique DXF entity handle or None
            owner: owner handle if entity has an owner else None or '0'
            dxfattribs: DXF attributes
            doc: DXF document

        (internal API)
        """
        entity = cls()
        entity.doc = doc
        entity.dxf.handle = handle
        entity.dxf.owner = owner
        attribs = dict(cls.DEFAULT_ATTRIBS)
        attribs.update(dxfattribs or {})
        entity.update_dxf_attribs(attribs)
        # Only this method triggers the post_new_hook()
        entity.post_new_hook()
        return entity

    def post_new_hook(self):
        """Post-processing and integrity validation after entity creation.

        Called only if created by ezdxf (see :meth:`DXFEntity.new`),
        not if loaded from an external source.

        (internal API)
        """
        pass

    def post_bind_hook(self):
        """Post-processing and integrity validation after binding entity to a
        DXF Document. This method is triggered by the :func:`factory.bind`
        function only when the entity was created by ezdxf.

        If the entity was loaded in the 1st loading stage, the
        :func:`factory.load` functions also calls the :func:`factory.bind`
        to bind entities to the loaded document, but not all entities are
        loaded at this time. To avoid problems this method will not be called
        when loading content from DXF file, but :meth:`post_load_hook` will be
        triggered for loaded entities at a later and safer point in time.

        (internal API)
        """
        pass

    @classmethod
    def load(cls: Type[T], tags: ExtendedTags, doc: Optional[Drawing] = None) -> T:
        """Constructor to generate entities loaded from an external source.

        LOAD process:

        This is an untrusted environment where valid structure are not
        guaranteed and errors should be fixed, because the package-user is not
        responsible for the problems and also can't fix them, raising
        exceptions should only be done for unrecoverable issues.
        Log fixes for debugging!

            Be more like BricsCAD and not as mean as AutoCAD!

        The :attr:`Drawing.is_loading` flag can be checked to distinguish the
        NEW and the LOAD process.

        Args:
            tags: DXF tags as :class:`ExtendedTags`
            doc: DXF Document

        (internal API)
        """
        # This method does not trigger the post_new_hook()
        entity = cls()
        entity.doc = doc
        dxfversion = doc.dxfversion if doc else None
        entity.load_tags(tags, dxfversion=dxfversion)
        return entity

    def load_tags(self, tags: ExtendedTags, dxfversion: Optional[str] = None) -> None:
        """Generic tag loading interface, called if DXF document is loaded
        from external sources.

        1. Loading stage which set the basic DXF attributes, additional
           resources (DXF objects) are not loaded yet. References to these
           resources have to be stored as handles and can be resolved in the
        2. Loading stage: :meth:`post_load_hook`.

        (internal API)
        """
        if tags:
            if len(tags.appdata):
                self.setup_app_data(tags.appdata)
            if len(tags.xdata):
                try:  # the common case - fast path
                    self.xdata = XData(tags.xdata)
                except const.DXFValueError:  # contains invalid group codes
                    self.xdata = XData.safe_init(tags.xdata)
                    logger.debug(f"removed invalid XDATA from {tags.entity_name()}")

            processor = SubclassProcessor(tags, dxfversion=dxfversion)
            self.dxf = self.load_dxf_attribs(processor)

    def load_dxf_attribs(
        self, processor: Optional[SubclassProcessor] = None
    ) -> DXFNamespace:
        """Load DXF attributes into DXF namespace."""
        return DXFNamespace(processor, self)

    def post_load_hook(self, doc: Drawing) -> Optional[Callable]:
        """The 2nd loading stage when loading DXF documents from an external
        source, for the 1st loading stage see :meth:`load_tags`.

        This stage is meant to convert resource handles into :class:`DXFEntity`
        objects. This is an untrusted environment where valid structure are not
        guaranteed, raise exceptions only for unrecoverable structure errors
        and fix everything else. Log fixes for debugging!

        Some fixes can not be applied at this stage, because some structures
        like the OBJECTS section are not initialized, in this case return a
        callable, which will be executed after the DXF document is fully
        initialized, for an example see :class:`Image`.

        Triggered in method: :meth:`Drawing._2nd_loading_stage`

        Examples for two stage loading:
        Image, Underlay, DXFGroup, Dictionary, Dimstyle, MText

        """
        if self.extension_dict is not None:
            self.extension_dict.load_resources(doc)
        return None

    @classmethod
    def from_text(cls: Type[T], text: str, doc: Optional[Drawing] = None) -> T:
        """Load constructor from text for testing. (internal API)"""
        return cls.load(ExtendedTags.from_text(text), doc)

    @classmethod
    def shallow_copy(cls: Type[T], other: DXFEntity) -> T:
        """Copy constructor for type casting e.g. Polyface and Polymesh.
        (internal API)
        """
        entity = cls()
        entity.doc = other.doc
        entity.dxf = other.dxf
        entity.extension_dict = other.extension_dict
        entity.reactors = other.reactors
        entity.appdata = other.appdata
        entity.xdata = other.xdata
        entity.proxy_graphic = other.proxy_graphic
        entity.dxf.rewire(entity)
        # Do not set copy state, this is not a real copy!
        return entity

    def raw_copy(self: T) -> T:
        """Returns a raw copy of `self` but without handle, owner and reactors.

        This is the first stage of the copy process, see copy() method.

        (internal API)
        """
        entity = self.__class__()
        doc = self.doc
        entity.doc = doc
        # copy and bind dxf namespace to new entity
        entity.dxf = self.dxf.copy(entity)
        entity.dxf.reset_handles()

        xdict = self.extension_dict
        if xdict is not None and doc is not None and xdict.is_alive:
            # All linked DXF objects are copied and added to the OBJECTS section:
            entity.extension_dict = xdict.copy()
        else:
            entity.extension_dict = None

        # Do not copy reactors:
        entity.reactors = None
        entity.proxy_graphic = self.proxy_graphic  # immutable bytes

        # if appdata contains handles, they are treated as shared resources
        entity.appdata = copy.deepcopy(self.appdata)

        # if xdata contains handles, they are treated as shared resources
        entity.xdata = copy.deepcopy(self.xdata)
        return entity

    def copy_data(self, entity: DXFEntity) -> None:
        """Copy entity data like vertices or attribs to the copy of the entity.

        This is the second stage of the copy process, see copy() method.

        (internal API)
        """
        pass

    def copy(self: T) -> T:
        """Internal entity copy for usage in the same document or as virtual entity.

        Returns a copy of `self` but without handle, owner and reactors.
        This copy is NOT stored in the entity database and does NOT reside
        in any layout, block, table or objects section!
        The extension dictionary will be copied for entities bound to a valid
        DXF document. The reactors are not copied.

        (internal API)
        """
        clone = self.raw_copy()
        clone.set_source_of_copy(self)
        # DO NOT COPY DYN_SOURCE_BLOCK_REFERENCE_ATTRIBUTE.
        # Copying an entity from a block reference, takes it out of context of
        # this block reference!
        self.copy_data(clone)
        return clone

    def copy_external(self: T) -> T:
        """External entity copy for usage in another document, this copy mode requires
        registering and mapping of resources, see also:

            - DXFEntity.register_resources()
            - DXFEntity.map_resources()

        Introduced for usage by the xref module.

        (internal API)
        """
        clone = self.raw_copy()
        # source of copy is not required
        self.copy_data(clone)
        return clone

    def __deepcopy__(self, memodict: Optional[dict] = None):
        """Some entities maybe linked by more than one entity, to be safe use
        `memodict` for bookkeeping.

        (internal API)
        """
        memodict = memodict or {}
        try:
            return memodict[id(self)]
        except KeyError:
            copy = self.copy()
            memodict[id(self)] = copy
            return copy

    def set_source_of_copy(self, source: Optional[DXFEntity]):
        """Set immediate source entity of a copy.

        Also used from outside to set the source of sub-entities
        of disassembled entities (POLYLINE, LWPOLYLINE, ...).

        (Internal API)
        """
        if isinstance(source, DXFEntity) and not source.is_alive:
            source = None
        # dynamic attribute: exist only in copies:
        setattr(self, DYN_SOURCE_OF_COPY_ATTRIBUTE, source)

    def del_source_of_copy(self) -> None:
        """Delete source of copy reference.

        (Internal API)
        """
        if hasattr(self, DYN_SOURCE_OF_COPY_ATTRIBUTE):
            delattr(self, DYN_SOURCE_OF_COPY_ATTRIBUTE)

    @property
    def is_copy(self) -> bool:
        """Is ``True`` if the entity is a copy."""
        return self.source_of_copy is not None

    @property
    def source_of_copy(self) -> Optional[DXFEntity]:
        """The immediate source entity if this entity is a copy else
        ``None``. Never references a destroyed entity.
        """
        # attribute only exist in copies:
        source = getattr(self, DYN_SOURCE_OF_COPY_ATTRIBUTE, None)
        if isinstance(source, DXFEntity) and not source.is_alive:
            return None
        return source

    @property
    def origin_of_copy(self) -> Optional[DXFEntity]:
        """The origin source entity if this entity is a copy else
        ``None``. References the first non-virtual source entity and never
        references a destroyed entity.
        """
        source = self.source_of_copy
        # follow source entities references until the first non-virtual entity:
        while isinstance(source, DXFEntity) and source.is_alive and source.is_virtual:
            source = source.source_of_copy
        return source

    def update_dxf_attribs(self, dxfattribs: dict) -> None:
        """Set DXF attributes by a ``dict`` like :code:`{'layer': 'test',
        'color': 4}`.
        """
        setter = self.dxf.set
        for key, value in dxfattribs.items():
            setter(key, value)

    def setup_app_data(self, appdata: list[Tags]) -> None:
        """Setup data structures from APP data. (internal API)"""
        for data in appdata:
            code, appid = data[0]
            if appid == const.ACAD_REACTORS:
                self.reactors = Reactors.from_tags(data)
            elif appid == const.ACAD_XDICTIONARY:
                self.extension_dict = ExtensionDict.from_tags(data)
            else:
                self.set_app_data(appid, data)

    def update_handle(self, handle: str) -> None:
        """Update entity handle. (internal API)"""
        self.dxf.handle = handle
        if self.extension_dict:
            self.extension_dict.update_owner(handle)

    @property
    def is_alive(self) -> bool:
        """Is ``False`` if entity has been deleted."""
        return hasattr(self, "dxf")

    @property
    def is_virtual(self) -> bool:
        """Is ``True`` if entity is a virtual entity."""
        return self.doc is None or self.dxf.handle is None

    @property
    def is_bound(self) -> bool:
        """Is ``True`` if entity is bound to DXF document."""
        if self.is_alive and not self.is_virtual:
            return factory.is_bound(self, self.doc)  # type: ignore
        return False

    @property
    def has_source_block_reference(self) -> bool:
        """Is ``True`` if this virtual entity was created by a block reference."""
        return hasattr(self, DYN_SOURCE_BLOCK_REFERENCE_ATTRIBUTE)

    @property
    def source_block_reference(self) -> Optional[Insert]:
        """The source block reference (INSERT) which created
        this virtual entity. The property is ``None`` if this entity was not
        created by a block reference.
        """
        blockref = getattr(self, DYN_SOURCE_BLOCK_REFERENCE_ATTRIBUTE, None)
        if blockref is not None and blockref.is_alive:
            return blockref  # type: ignore
        return None

    def set_source_block_reference(self, blockref: Insert) -> None:
        """Set the immediate source block reference which created this virtual
        entity.

        The source block reference can only be set once by the immediate INSERT
        entity and does not change if the entity is passed through multiple
        nested INSERT entities.

        (Internal API)
        """
        assert self.is_virtual, "instance has to be a virtual entity"
        if self.has_source_block_reference:
            return
        setattr(self, DYN_SOURCE_BLOCK_REFERENCE_ATTRIBUTE, blockref)

    def del_source_block_reference(self) -> None:
        """Delete source block reference.

        (Internal API)
        """
        if hasattr(self, DYN_SOURCE_BLOCK_REFERENCE_ATTRIBUTE):
            delattr(self, DYN_SOURCE_BLOCK_REFERENCE_ATTRIBUTE)

    def get_dxf_attrib(self, key: str, default: Any = None) -> Any:
        """Get DXF attribute `key`, returns `default` if key doesn't exist, or
        raise :class:`DXFValueError` if `default` is :class:`DXFValueError`
        and no DXF default value is defined::

            layer = entity.get_dxf_attrib("layer")
            # same as
            layer = entity.dxf.layer

        Raises :class:`DXFAttributeError` if `key` is not an supported DXF
        attribute.

        """
        return self.dxf.get(key, default)

    def set_dxf_attrib(self, key: str, value: Any) -> None:
        """Set new `value` for DXF attribute `key`::

           entity.set_dxf_attrib("layer", "MyLayer")
           # same as
           entity.dxf.layer = "MyLayer"

        Raises :class:`DXFAttributeError` if `key` is not an supported DXF
        attribute.

        """
        self.dxf.set(key, value)

    def del_dxf_attrib(self, key: str) -> None:
        """Delete DXF attribute `key`, does not raise an error if attribute is
        supported but not present.

        Raises :class:`DXFAttributeError` if `key` is not an supported DXF
        attribute.

        """
        self.dxf.discard(key)

    def has_dxf_attrib(self, key: str) -> bool:
        """Returns ``True`` if DXF attribute `key` really exist.

        Raises :class:`DXFAttributeError` if `key` is not an supported DXF
        attribute.

        """
        return self.dxf.hasattr(key)

    dxf_attrib_exists = has_dxf_attrib

    def is_supported_dxf_attrib(self, key: str) -> bool:
        """Returns ``True`` if DXF attrib `key` is supported by this entity.
        Does not grant that attribute `key` really exist.

        """
        if key in self.DXFATTRIBS:
            if self.doc:
                return (
                    self.doc.dxfversion
                    >= self.DXFATTRIBS.get(key).dxfversion  # type: ignore
                )
            else:
                return True
        else:
            return False

    def dxftype(self) -> str:
        """Get DXF type as string, like ``LINE`` for the line entity."""
        return self.DXFTYPE

    def __str__(self) -> str:
        """Returns a simple string representation."""
        return "{}(#{})".format(self.dxftype(), self.dxf.handle)

    def __repr__(self) -> str:
        """Returns a simple string representation including the class."""
        return str(self.__class__) + " " + str(self)

    def dxfattribs(self, drop: Optional[set[str]] = None) -> dict:
        """Returns a ``dict`` with all existing DXF attributes and their
        values and exclude all DXF attributes listed in set `drop`.

        """
        all_attribs = self.dxf.all_existing_dxf_attribs()
        if drop:
            return {k: v for k, v in all_attribs.items() if k not in drop}
        else:
            return all_attribs

    def set_flag_state(
        self, flag: int, state: bool = True, name: str = "flags"
    ) -> None:
        """Set binary coded `flag` of DXF attribute `name` to 1 (on)
        if `state` is ``True``, set `flag` to 0 (off)
        if `state` is ``False``.
        """
        flags = self.dxf.get(name, 0)
        self.dxf.set(name, set_flag_state(flags, flag, state=state))

    def get_flag_state(self, flag: int, name: str = "flags") -> bool:
        """Returns ``True`` if any `flag` of DXF attribute is 1 (on), else
        ``False``. Always check only one flag state at the time.
        """
        return bool(self.dxf.get(name, 0) & flag)

    def remove_dependencies(self, other: Optional[Drawing] = None):
        """Remove all dependencies from current document.

        Intended usage is to remove dependencies from the current document to
        move or copy the entity to `other` DXF document.

        An error free call of this method does NOT guarantee that this entity
        can be moved/copied to the `other` document, some entities like
        DIMENSION have too many dependencies to a document to move or copy
        them, but to check this is not the domain of this method!

        (internal API)
        """
        if self.is_alive:
            self.dxf.owner = None
            self.dxf.handle = None
            self.reactors = None
            self.extension_dict = None
            self.appdata = None
            self.xdata = None
            # remove dynamic attributes if exist:
            self.del_source_of_copy()
            self.del_source_block_reference()

    def destroy(self) -> None:
        """Delete all data and references. Does not delete entity from
        structures like layouts or groups.

        (internal API)
        """
        if not self.is_alive:
            return

        if self.extension_dict is not None:
            self.extension_dict.destroy()
            del self.extension_dict
        del self.appdata
        del self.reactors
        del self.xdata
        del self.doc
        del self.dxf  # check mark for is_alive
        # Remove dynamic attributes, which reference other entities:
        self.del_source_of_copy()
        self.del_source_block_reference()

    def _silent_kill(self):  # final - do not override this method!
        """Delete entity but not the referenced content!

        DANGER! DON'T USE THIS METHOD!

        (internal API)
        """
        if not self.is_alive:
            return
        del self.extension_dict
        del self.appdata
        del self.reactors
        del self.xdata
        del self.doc
        del self.dxf  # check mark for is_alive

    def preprocess_export(self, tagwriter: AbstractTagWriter) -> bool:
        """Pre requirement check and pre-processing for export.

        Returns ``False``  if entity should not be exported at all.

        (internal API)
        """
        return True

    def export_dxf(self, tagwriter: AbstractTagWriter) -> None:
        """Export DXF entity by `tagwriter`.

        This is the first key method for exporting DXF entities:

            - has to know the group codes for each attribute
            - has to add subclass tags in correct order
            - has to integrate extended data: ExtensionDict, Reactors, AppData
            - has to maintain the correct tag order (because sometimes order matters)

        (internal API)

        """
        if tagwriter.dxfversion < self.MIN_DXF_VERSION_FOR_EXPORT:
            return
        if not self.preprocess_export(tagwriter):
            return
        # write handle, AppData, Reactors, ExtensionDict, owner
        self.export_base_class(tagwriter)

        # this is the entity specific part
        self.export_entity(tagwriter)

        # write xdata at the end of the entity
        self.export_xdata(tagwriter)

    def export_base_class(self, tagwriter: AbstractTagWriter) -> None:
        """Export base class DXF attributes and structures. (internal API)"""
        dxftype = self.DXFTYPE
        _handle_code = 105 if dxftype == "DIMSTYLE" else 5
        # 1. tag: (0, DXFTYPE)
        tagwriter.write_tag2(const.STRUCTURE_MARKER, dxftype)

        if tagwriter.dxfversion >= const.DXF2000:
            tagwriter.write_tag2(_handle_code, self.dxf.handle)
            if self.appdata:
                self.appdata.export_dxf(tagwriter)  # type: ignore
            if self.has_extension_dict:
                self.extension_dict.export_dxf(tagwriter)  # type: ignore
            if self.reactors:
                self.reactors.export_dxf(tagwriter)  # type: ignore
            tagwriter.write_tag2(const.OWNER_CODE, self.dxf.owner)
        else:  # DXF R12
            if tagwriter.write_handles:
                tagwriter.write_tag2(_handle_code, self.dxf.handle)
                # do not write owner handle - not supported by DXF R12

    def export_entity(self, tagwriter: AbstractTagWriter) -> None:
        """Export DXF entity specific data by `tagwriter`.

        This is the second key method for exporting DXF entities:

            - has to know the group codes for each attribute
            - has to add subclass tags in correct order
            - has to maintain the correct tag order (because sometimes order matters)

        (internal API)
        """
        # base class (handle, appid, reactors, xdict, owner) export is done by parent class
        pass
        # xdata and embedded objects  export is also done by parent

    def export_xdata(self, tagwriter: AbstractTagWriter) -> None:
        """Export DXF XDATA by `tagwriter`. (internal API)"""
        if self.xdata:
            self.xdata.export_dxf(tagwriter)

    def audit(self, auditor: Auditor) -> None:
        """Validity check. (internal API)"""
        # Important: do not check owner handle! -> DXFGraphic(), DXFObject()
        # check app data
        # check reactors
        # check extension dict
        # check XDATA

    @property
    def has_extension_dict(self) -> bool:
        """Returns ``True`` if entity has an attached
        :class:`~ezdxf.entities.xdict.ExtensionDict` instance.
        """
        xdict = self.extension_dict
        # Don't use None check: bool(xdict) for an empty extension dict is False
        if xdict is not None and xdict.is_alive:
            return xdict.dictionary.is_alive
        return False

    def get_extension_dict(self) -> ExtensionDict:
        """Returns the existing :class:`~ezdxf.entities.xdict.ExtensionDict`
        instance.

        Raises:
            AttributeError: extension dict does not exist

        """
        if self.has_extension_dict:
            return self.extension_dict  # type: ignore
        else:
            raise AttributeError("Entity has no extension dictionary.")

    def new_extension_dict(self) -> ExtensionDict:
        """Create a new :class:`~ezdxf.entities.xdict.ExtensionDict` instance ."""
        assert self.doc is not None
        xdict = ExtensionDict.new(self.dxf.handle, self.doc)
        self.extension_dict = xdict
        return xdict

    def discard_extension_dict(self) -> None:
        """Delete :class:`~ezdxf.entities.xdict.ExtensionDict` instance ."""
        if isinstance(self.extension_dict, ExtensionDict):
            self.extension_dict.destroy()
        self.extension_dict = None

    def has_app_data(self, appid: str) -> bool:
        """Returns ``True`` if application defined data for `appid` exist."""
        if self.appdata:
            return appid in self.appdata
        else:
            return False

    def get_app_data(self, appid: str) -> Tags:
        """Returns application defined data for `appid`.

        Args:
            appid: application name as defined in the APPID table.

        Raises:
            DXFValueError: no data for `appid` found

        """
        if self.appdata:
            return Tags(self.appdata.get(appid)[1:-1])
        else:
            raise const.DXFValueError(appid)

    def set_app_data(self, appid: str, tags: Iterable) -> None:
        """Set application defined data for `appid` as iterable of tags.

        Args:
             appid: application name as defined in the APPID table.
             tags: iterable of (code, value) tuples or :class:`~ezdxf.lldxf.types.DXFTag`

        """
        if self.appdata is None:
            self.appdata = AppData()
        self.appdata.add(appid, tags)

    def discard_app_data(self, appid: str):
        """Discard application defined data for `appid`. Does not raise an
        exception if no data for `appid` exist.
        """
        if self.appdata:
            self.appdata.discard(appid)

    def has_xdata(self, appid: str) -> bool:
        """Returns ``True`` if extended data for `appid` exist."""
        if self.xdata:
            return appid in self.xdata
        else:
            return False

    def get_xdata(self, appid: str) -> Tags:
        """Returns extended data for `appid`.

        Args:
            appid: application name as defined in the APPID table.

        Raises:
            DXFValueError: no extended data for `appid` found

        """
        if self.xdata:
            return Tags(self.xdata.get(appid)[1:])
        else:
            raise const.DXFValueError(appid)

    def set_xdata(self, appid: str, tags: Iterable) -> None:
        """Set extended data for `appid` as iterable of tags.

        Args:
             appid: application name as defined in the APPID table.
             tags: iterable of (code, value) tuples or :class:`~ezdxf.lldxf.types.DXFTag`

        """
        if self.xdata is None:
            self.xdata = XData()
        self.xdata.add(appid, tags)

    def discard_xdata(self, appid: str) -> None:
        """Discard extended data for `appid`. Does not raise an exception if
        no extended data for `appid` exist.
        """
        if self.xdata:
            self.xdata.discard(appid)

    def has_xdata_list(self, appid: str, name: str) -> bool:
        """Returns ``True`` if a tag list `name` for extended data `appid`
        exist.
        """
        if self.has_xdata(appid):
            return self.xdata.has_xlist(appid, name)  # type: ignore
        else:
            return False

    def get_xdata_list(self, appid: str, name: str) -> Tags:
        """Returns tag list `name` for extended data `appid`.

        Args:
            appid: application name as defined in the APPID table.
            name: extended data list name

        Raises:
            DXFValueError: no extended data for `appid` found or no data list `name` not found

        """
        if self.xdata:
            return Tags(self.xdata.get_xlist(appid, name))
        else:
            raise const.DXFValueError(appid)

    def set_xdata_list(self, appid: str, name: str, tags: Iterable) -> None:
        """Set tag list `name` for extended data `appid` as iterable of tags.

        Args:
             appid: application name as defined in the APPID table.
             name: extended data list name
             tags: iterable of (code, value) tuples or :class:`~ezdxf.lldxf.types.DXFTag`

        """
        if self.xdata is None:
            self.xdata = XData()
        self.xdata.set_xlist(appid, name, tags)

    def discard_xdata_list(self, appid: str, name: str) -> None:
        """Discard tag list `name` for extended data `appid`. Does not raise
        an exception if no extended data for `appid` or no tag list `name`
        exist.
        """
        if self.xdata:
            self.xdata.discard_xlist(appid, name)

    def replace_xdata_list(self, appid: str, name: str, tags: Iterable) -> None:
        """
        Replaces tag list `name` for existing extended data `appid` by `tags`.
        Appends new list if tag list `name` do not exist, but raises
        :class:`DXFValueError` if extended data `appid` do not exist.

        Args:
             appid: application name as defined in the APPID table.
             name: extended data list name
             tags: iterable of (code, value) tuples or :class:`~ezdxf.lldxf.types.DXFTag`

        Raises:
            DXFValueError: no extended data for `appid` found

        """
        assert self.xdata is not None
        self.xdata.replace_xlist(appid, name, tags)

    def has_reactors(self) -> bool:
        """Returns ``True`` if entity has reactors."""
        return bool(self.reactors)

    def get_reactors(self) -> list[str]:
        """Returns associated reactors as list of handles."""
        return self.reactors.get() if self.reactors else []

    def set_reactors(self, handles: Iterable[str]) -> None:
        """Set reactors as list of handles."""
        if self.reactors is None:
            self.reactors = Reactors()
        self.reactors.set(handles)

    def append_reactor_handle(self, handle: str) -> None:
        """Append `handle` to reactors."""
        if self.reactors is None:
            self.reactors = Reactors()
        self.reactors.add(handle)

    def discard_reactor_handle(self, handle: str) -> None:
        """Discard `handle` from reactors. Does not raise an exception if
        `handle` does not exist.
        """
        if self.reactors:
            self.reactors.discard(handle)

    def register_resources(self, registry: xref.Registry) -> None:
        """Register required resources to the resource registry."""
        if self.xdata:
            for name in self.xdata.data.keys():
                registry.add_appid(name)
        if self.appdata:  # add hard owned entities
            for tags in self.appdata.tags():
                for tag in tags.get_hard_owner_handles():
                    registry.add_handle(tag.value)

    def map_resources(self, clone: DXFEntity, mapping: xref.ResourceMapper) -> None:
        """Translate resources from self to the copied entity."""

        def map_xdata_resources():
            for index, (code, value) in enumerate(tags):
                if code == 1005:  # map soft-pointer handles
                    tags[index] = DXFTag(code, mapping.get_handle(value))
                elif code == 1003:  # map layer name
                    tags[index] = DXFTag(code, mapping.get_layer(value))

        if clone.xdata:
            for tags in clone.xdata.data.values():
                map_xdata_resources()

        if clone.appdata:
            for tags in clone.appdata.tags():
                mapping.map_pointers(tags, new_owner_handle=clone.dxf.handle)

        if clone.extension_dict:
            assert self.extension_dict is not None
            self.extension_dict.dictionary.map_resources(
                clone.extension_dict.dictionary, mapping
            )
        # reactors are not copied automatically, clone.reactors is always None:
        if self.reactors:
            # reactors are soft-pointers (group code 330)
            mapped_handles = [mapping.get_handle(h) for h in self.reactors.reactors]
            mapped_handles = [h for h in mapped_handles if h != "0"]
            if mapped_handles:
                clone.set_reactors(mapped_handles)


@factory.set_default_class
class DXFTagStorage(DXFEntity):
    """Just store all the tags as they are. (internal class)"""

    def __init__(self) -> None:
        """Default constructor"""
        super().__init__()
        self.xtags = ExtendedTags()
        self.embedded_objects: Optional[list[Tags]] = None

    def raw_copy(self: T) -> T:
        raise const.DXFTypeError(
            f"Cloning of tag storage {self.dxftype()} not supported."
        )

    def transform(self, m: Matrix44) -> DXFGraphic:
        raise NotImplementedError("cannot transform DXF tag storage")

    @property
    def base_class(self):
        return self.xtags.subclasses[0]

    @property
    def is_graphic_entity(self) -> bool:
        """Returns ``True`` if the entity has a graphical representations and
        can reside in the model space, a paper space or a block layout,
        otherwise the entity is a table or class entry or a DXF object from the
        OBJECTS section.
        """
        return self.xtags.has_subclass("AcDbEntity")

    def graphic_properties(self) -> dict[str, Any]:
        """Returns the graphical properties like layer, color, linetype, ... as
        `dxfattribs` dict if the :class:`TagStorage` object represents a graphical
        entity otherwise returns an empty dictionary.

        These are all the DXF attributes which are stored in the ``AcDbEntity``
        subclass.

        """
        from ezdxf.entities.dxfgfx import acdb_entity_group_codes

        attribs: dict[str, Any] = dict()
        try:
            tags = self.xtags.get_subclass("AcDbEntity")
        except const.DXFKeyError:
            return attribs

        for code, value in tags:
            attrib_name = acdb_entity_group_codes.get(code)
            if isinstance(attrib_name, str):
                attribs[attrib_name] = value
        return attribs

    @classmethod
    def load(cls, tags: ExtendedTags, doc: Optional[Drawing] = None) -> DXFTagStorage:
        assert isinstance(tags, ExtendedTags)
        entity = cls.new(doc=doc)
        dxfversion = doc.dxfversion if doc else None
        entity.load_tags(tags, dxfversion=dxfversion)
        entity.store_tags(tags)
        entity.store_embedded_objects(tags)
        if options.load_proxy_graphics:
            entity.load_proxy_graphic()
        return entity

    def load_proxy_graphic(self) -> None:
        try:
            tags = self.xtags.get_subclass("AcDbEntity")
        except const.DXFKeyError:
            return
        binary_data = [tag.value for tag in tags.find_all(310)]
        if len(binary_data):
            self.proxy_graphic = b"".join(binary_data)

    def store_tags(self, tags: ExtendedTags) -> None:
        # store DXFTYPE, overrides class member
        # 1. tag of 1. subclass is the structure tag (0, DXFTYPE)
        self.xtags = tags
        self.DXFTYPE = self.base_class[0].value
        try:
            acdb_entity = tags.get_subclass("AcDbEntity")
            self.dxf.__dict__["paperspace"] = acdb_entity.get_first_value(67, 0)
        except const.DXFKeyError:
            # just fake it
            self.dxf.__dict__["paperspace"] = 0

    def store_embedded_objects(self, tags: ExtendedTags) -> None:
        self.embedded_objects = tags.embedded_objects

    def export_entity(self, tagwriter: AbstractTagWriter) -> None:
        """Write subclass tags as they are."""
        for subclass in self.xtags.subclasses[1:]:
            tagwriter.write_tags(subclass)

        if self.embedded_objects:
            for tags in self.embedded_objects:
                tagwriter.write_tags(tags)

    def destroy(self) -> None:
        if not self.is_alive:
            return

        del self.xtags
        super().destroy()

    def __virtual_entities__(self) -> Iterator[DXFGraphic]:
        """Implements the SupportsVirtualEntities protocol."""
        from ezdxf.proxygraphic import ProxyGraphic

        if self.proxy_graphic:
            for e in ProxyGraphic(self.proxy_graphic, self.doc).virtual_entities():
                e.set_source_of_copy(self)
                yield e
        return []

    def virtual_entities(self) -> Iterator[DXFGraphic]:
        """Yields proxy graphic as "virtual" entities."""
        return self.__virtual_entities__()
