#  Copyright (c) 2021, Manfred Moitzi
#  License: MIT License
"""
UserRecord(): store user data in a XRECORD entity.

The group code 302 is used as a structure tag.

All supported data types have a fixed group code:
    - str: 1
    - int: 90 - 32-bit values
    - float: 40 - doubles
    - Vec3, Vec2: 10, 20, 30 - Vec2 is stored as Vec3
    - list, tuple: starts with tag (302, "[") and ends with tag (302, "]")
    - dict: starts with tag (302, "{") and ends with tag (302, "}")

The str type can have a max. length of 2049 characters and cannot contain "\n"
or "\r".

This is an advanced feature for experienced programmers, handle with care!
The attribute UserRecord.data is a simple Python list with read/write access.

The UserRecord can store nested list and dict objects.

BinaryData(): store arbitrary binary data in a XRECORD entity

"""

from typing import (
    TYPE_CHECKING,
    Optional,
    Iterable,
    Sequence,
    Mapping,
    MutableSequence,
    List,
    cast,
)
from ezdxf.lldxf import const
from ezdxf.entities import XRecord
from ezdxf.lldxf.tags import Tags, binary_data_to_dxf_tags
from ezdxf.lldxf.types import dxftag
from ezdxf.math import Vec3, Vec2
from ezdxf.tools import take2
from ezdxf.tools.binarydata import bytes_to_hexstr

if TYPE_CHECKING:
    from ezdxf.eztypes import Drawing

TYPE_GROUP_CODE = 2
STR_GROUP_CODE = 1
INT_GROUP_CODE = 90
FLOAT_GROUP_CODE = 40
VEC3_GROUP_CODE = 10
COLLECTION_GROUP_CODE = 302
START_LIST = "["
END_LIST = "]"
START_DICT = "{"
END_DICT = "}"
DEFAULT_NAME = "UserRecord"

__all__ = ["UserRecord", "BinaryRecord"]


class UserRecord:
    def __init__(
        self,
        xrecord: XRecord = None,
        *,
        name: str = DEFAULT_NAME,
        doc: "Drawing" = None,
    ):
        """Setup a :class:`UserRecord` with the given `name`.

        The data is stored in the given `xrecord` object, or in a new created
        :class:`~ezdxf.entities.XRecord` instance if ``None``. If `doc` is not
        ``None`` the new xrecord is added to the OBJECTS section of the DXF
        document.

        Changes of the content has to be committed at the end to be stored in
        the underlying :attr:`xrecord` object.

        Args:
            xrecord (XRecord): underlying :class:`~ezdxf.entities.XRecord` instance,
                if ``None`` a new one will be created
            name (str): name of the user list
            doc (Drawing): DXF document or ``None``

        """
        if xrecord is None:
            if doc is None:
                xrecord = XRecord.new()
            else:
                xrecord = cast(XRecord, doc.objects.new_entity("XRECORD", {}))

        self.xrecord = xrecord
        self.name = str(name)
        self.data: MutableSequence = parse_xrecord(self.xrecord, self.name)

    @property
    def handle(self) -> Optional[str]:
        """DXF handle of the underlying :class:`~ezdxf.entities.XRecord` instance."""
        return self.xrecord.dxf.get("handle")

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.commit()

    def __str__(self):
        """Return str(self)."""
        return str(self.data)

    def commit(self) -> XRecord:
        """Store :attr:`data` in the underlying :class:`~ezdxf.entities.XRecord`
        instance. This call is not required if using the class by the ``with``
        statement.

        Raises:
            DXFValueError: invalid chars ``"\\n"`` or ``"\\r"`` in a string
            DXFTypeError: invalid data type

        """
        self.xrecord.tags = compile_user_record(self.name, self.data)
        return self.xrecord


def parse_xrecord(xrecord: XRecord, name: str) -> List:
    data: List = []
    tags = xrecord.tags
    if tags:
        code, value = tags[0]
        if code != TYPE_GROUP_CODE and value != name:
            raise const.DXFTypeError(
                f"{str(xrecord)} is not an user record of type {name}"
            )
        data.extend(item for item in parse_items(tags[1:]))
    return data


def parse_items(tags: Tags) -> List:
    stack: List = []
    items: List = []
    for tag in tags:
        code, value = tag
        if code == STR_GROUP_CODE:
            items.append(str(value))
        elif code == INT_GROUP_CODE:
            items.append(int(value))
        elif code == FLOAT_GROUP_CODE:
            items.append(float(value))
        elif code == VEC3_GROUP_CODE:
            items.append(Vec3(value))
        elif code == COLLECTION_GROUP_CODE and (
            value == START_LIST or value == START_DICT
        ):
            stack.append(items)
            items = []
        elif code == COLLECTION_GROUP_CODE and (
            value == END_LIST or value == END_DICT
        ):
            try:
                prev_level = stack.pop()
            except IndexError:
                raise const.DXFStructureError(
                    f"invalid nested structure, mismatch of structure tags"
                    f" ({COLLECTION_GROUP_CODE}, ...)"
                )
            if value == END_DICT:
                prev_level.append(dict(take2(items)))
            else:
                prev_level.append(items)
            items = prev_level
        else:
            raise const.DXFValueError(
                f"invalid group code in tag: ({code}, {value})"
            )
    if stack: