# Copyright (c) 2019-2024 Manfred Moitzi
# License: MIT License
from __future__ import annotations
from typing import TYPE_CHECKING, Iterable, Union, Optional, Sequence, Any
from typing_extensions import Self, override
import logging

from ezdxf.lldxf.attributes import (
    DXFAttr,
    DXFAttributes,
    DefSubclass,
    XType,
    group_code_mapping,
)
from ezdxf.lldxf import const
from ezdxf.lldxf.tags import Tags, DXFTag
from ezdxf.math import Matrix44
from ezdxf.tools import crypt, guid
from ezdxf import msgtypes

from .dxfentity import base_class, SubclassProcessor
from .dxfgfx import DXFGraphic, acdb_entity
from .factory import register_entity
from .copy import default_copy
from .temporary_transform import TransformByBlockReference

if TYPE_CHECKING:
    from ezdxf.entities import DXFNamespace
    from ezdxf.lldxf.tagwriter import AbstractTagWriter
    from ezdxf import xref


__all__ = [
    "Body",
    "Solid3d",
    "Region",
    "Surface",
    "ExtrudedSurface",
    "LoftedSurface",
    "RevolvedSurface",
    "SweptSurface",
]

logger = logging.getLogger("ezdxf")
acdb_modeler_geometry = DefSubclass(
    "AcDbModelerGeometry",
    {
        "version": DXFAttr(70, default=1),
        "flags": DXFAttr(290, dxfversion=const.DXF2013),
        "uid": DXFAttr(2, dxfversion=const.DXF2013),
    },
)
acdb_modeler_geometry_group_codes = group_code_mapping(acdb_modeler_geometry)

# with R2013/AC1027 Modeler Geometry of ACIS data is stored in the ACDSDATA
# section as binary encoded information detection:
# group code 70, 1, 3 is missing
# group code 290, 2 present
#
#   0
# ACDSRECORD
#  90
# 1
#   2
# AcDbDs::ID
# 280
# 10
# 320
# 19B   <<< handle of associated 3DSOLID entity in model space
#   2
# ASM_Data
# 280
# 15
#  94
# 7197  <<< size in bytes ???
# 310
# 414349532042696E61727946696C6...


@register_entity
class Body(DXFGraphic):
    """DXF BODY entity - container entity for embedded ACIS data."""

    DXFTYPE = "BODY"
    DXFATTRIBS = DXFAttributes(base_class, acdb_entity, acdb_modeler_geometry)
    MIN_DXF_VERSION_FOR_EXPORT = const.DXF2000

    def __init__(self) -> None:
        super().__init__()
        # Store SAT data as immutable sequence of strings, so the data can be shared
        # across multiple copies of an ACIS entity.
        self._sat: Sequence[str] = tuple()
        self._sab: bytes = b""
        self._update = False
        self._temporary_transformation = TransformByBlockReference()

    @property
    def acis_data(self) -> Union[bytes, Sequence[str]]:
        """Returns :term:`SAT` data  for DXF R2000 up to R2010 and :term:`SAB`
        data for DXF R2013 and later
        """
        if self.has_binary_data:
            return self.sab
        return self.sat

    @property
    def sat(self) -> Sequence[str]:
        """Get/Set :term:`SAT` data as sequence of strings."""
        return self._sat

    @sat.setter
    def sat(self, data: Sequence[str]) -> None:
        """Set :term:`SAT` data as sequence of strings."""
        self._sat = tuple(data)

    @property
    def sab(self) -> bytes:
        """Get/Set :term:`SAB` data as bytes."""
        if (  # load SAB data on demand
            self.doc is not None and self.has_binary_data and len(self._sab) == 0
        ):
            self._sab = self.doc.acdsdata.get_acis_data(self.dxf.handle)
        return self._sab

    @sab.setter
    def sab(self, data: bytes) -> None:
        """Set :term:`SAB` data as bytes."""
        self._update = True
        self._sab = data

    @property
    def has_binary_data(self):
        """Returns ``True`` if the entity contains :term:`SAB` data and
        ``False`` if the entity contains :term:`SAT` data.
        """
        if self.doc:
            return self.doc.dxfversion >= const.DXF2013
        else:
            return False

    @override
    def copy_data(self, entity: Self, copy_strategy=default_copy) -> None:
        assert isinstance(entity, Body)
        entity.sat = self.sat
        entity.sab = self.sab  # load SAB on demand
        entity.dxf.uid = guid()
        entity._temporary_transformation = self._temporary_transformation.copy()

    @override
    def map_resources(self, clone: Self, mapping: xref.ResourceMapper) -> None:
        """Translate resources from self to the copied entity."""
        super().map_resources(clone, mapping)
        clone.convert_acis_data()

    def convert_acis_data(self) -> None:
        if self.doc is None:
            return
        msg = ""
        dxfversion = self.doc.dxfversion
        if dxfversion < const.DXF2013:
            if self._sab:
                self._sab = b""
                msg = "DXF version mismatch, can't convert ACIS data from SAB to SAT, SAB data removed."
        else:
            if self._sat:
                self._sat = tuple()
                msg = "DXF version mismatch, can't convert ACIS data from SAT to SAB, SAT data removed."
        if msg:
            logger.info(msg)

    @override
    def notify(self, message_type: int, data: Any = None) -> None:
        if message_type == msgtypes.COMMIT_PENDING_CHANGES:
            self._temporary_transformation.apply_transformation(self)

    @override
    def preprocess_export(self, tagwriter: AbstractTagWriter) -> bool:
        msg = ""
        if tagwriter.dxfversion < const.DXF2013:
            valid = len(self.sat) > 0
            if not valid:
                msg = f"{str(self)} doesn't have SAT data, skipping DXF export"
        else:
            valid = len(self.sab) > 0
            if not valid:
                msg = f"{str(self)} doesn't have SAB data, skipping DXF export"
        if not valid:
            logger.info(msg)
        if valid and self._temporary_transformation.get_matrix() is not None:
            logger.warning(f"{str(self)} has unapplied temporary transformations.")
        return valid

    @override
    def load_dxf_attribs(
        self, processor: Optional[SubclassProcessor] = None
    ) -> DXFNamespace:
        """Loading interface. (internal API)"""
        dxf = super().load_dxf_attribs(processor)
        if processor:
            processor.fast_load_dxfattribs(
                dxf, acdb_modeler_geometry_group_codes, 2, log=False
            )
            if not self.has_binary_data:
                self.load_sat_data(processor.subclasses[2])
        return dxf

    def load_sat_data(self, tags: Tags):
        """Loading interface. (internal API)"""
        text_lines = tags2textlines(tag for tag in tags if tag.code in (1, 3))
        self._sat = tuple(crypt.decode(text_lines))

    @override
    def export_entity(self, tagwriter: AbstractTagWriter) -> None:
        """Export entity specific data as DXF tags. (internal API)"""
        super().export_entity(tagwriter)
        tagwriter.write_tag2(const.SUBCLASS_MARKER, acdb_modeler_geometry.name)
        if tagwriter.dxfversion >= const.DXF2013:
            # ACIS data is stored in the ACDSDATA section as SAB
            if self.doc and self._update:
                # write back changed SAB data into AcDsDataSection or create
                # a new ACIS record:
                self.doc.acdsdata.set_acis_data(self.dxf.handle, self.sab)
            if self.dxf.hasattr("version"):
                tagwriter.write_tag2(70, self.dxf.version)
            self.dxf.export_dxf_attribs(tagwriter, ["flags", "uid"])
        else:
            # DXF R2000 - R2010 stores the ACIS data as SAT in the entity
            self.dxf.export_dxf_attribs(tagwriter, "version")
            self.export_sat_data(tagwriter)

    def export_sat_data(self, tagwriter: AbstractTagWriter) -> None:
        """Export ACIS data as DXF tags. (internal API)"""

        def cleanup(lines):
            for line in lines:
                yield line.rstrip().replace("\n", "")

        tags = Tags(textlines2tags(crypt.encode(cleanup(self.sat))))
        tagwriter.write_tags(tags)

    def tostring(self) -> str:
        """Returns ACIS :term:`SAT` data as a single string if the entity has
        SAT data.
        """
        if self.has_binary_data:
            return ""
        else:
            return "\n".join(self.sat)

    @override
    def destroy(self) -> None:
        if self.has_binary_data:
            self.doc.acdsdata.del_acis_data(self.dxf.handle)  # type: ignore
        super().destroy()

    @override
    def transform(self, m: Matrix44) -> Self:
        self._temporary_transformation.add_matrix(m)
        return self

    def temporary_transformation(self) -> TransformByBlockReference:
        return self._temporary_transformation


def tags2textlines(tags: Iterable) -> Iterable[str]:
    """Yields text lines from code 1 and 3 tags, code 1 starts a line following
    code 3 tags are appended to the line.
    """
    line = None
    for code, value in tags:
        if code == 1:
            if line is not None:
                yield line
            line = value
        elif code == 3:
            line += value
    if line is not None:
        yield line


def textlines2tags(lines: Iterable[str]) -> Iterable[DXFTag]:
    """Yields text lines as DXFTags, splitting long lines (>255) int code 1
    and code 3 tags.
    """
    for line in lines:
        text = line[:255]
        tail = line[255:]
        yield DXFTag(1, text)
        while len(tail):
            text = tail[:255]
            tail = tail[255:]
            yield DXFTag(3, text)


@register_entity
class Region(Body):
    """DXF REGION entity - container entity for embedded ACIS data."""

    DXFTYPE = "REGION"


acdb_3dsolid = DefSubclass(
    "AcDb3dSolid",
    {
        "history_handle": DXFAttr(350, default="0"),
    },
)
acdb_3dsolid_group_codes = group_code_mapping(acdb_3dsolid)


@register_entity
class Solid3d(Body):
    """DXF 3DSOLID entity - container entity for embedded ACIS data."""

    DXFTYPE = "3DSOLID"
    DXFATTRIBS = DXFAttributes(
        base_class, acdb_entity, acdb_modeler_geometry, acdb_3dsolid
    )

    @override
    def load_dxf_attribs(
        self, processor: Optional[SubclassProcessor] = None
    ) -> DXFNamespace:
        dxf = super().load_dxf_attribs(processor)
        if processor:
            processor.fast_load_dxfattribs(dxf, acdb_3dsolid_group_codes, 3)
        return dxf

    @override
    def export_entity(self, tagwriter: AbstractTagWriter) -> None:
        """Export entity specific data as DXF tags."""
        # base class export is done by parent class
        super().export_entity(tagwriter)
        # AcDbEntity export is done by parent class
        # AcDbModelerGeometry export is done by parent class
        if tagwriter.dxfversion > const.DXF2004:
            tagwriter.write_tag2(const.SUBCLASS_MARKER, acdb_3dsolid.name)
            self.dxf.export_dxf_attribs(tagwriter, "history_handle")


def load_matrix(subclass: Tags, code: int) -> Matrix44:
    values = [tag.value for tag in subclass.find_all(code)]
    if len(values) != 16:
        raise const.DXFStructureError("Invalid transformation matrix.")
    return Matrix44(values)


def export_matrix(tagwriter: AbstractTagWriter, code: int, matrix: Matrix44) -> None:
    for value in list(matrix):
        tagwriter.write_tag2(code, value)


acdb_surface = DefSubclass(
    "AcDbSurface",
    {
        "u_count": DXFAttr(71),
        "v_count": DXFAttr(72),
    },
)
acdb_surface_group_codes = group_code_mapping(acdb_surface)


@register_entity
class Surface(Body):
    """DXF SURFACE entity - container entity for embedded ACIS data."""

    DXFTYPE = "SURFACE"
    DXFATTRIBS = DXFAttributes(
        base_class, acdb_entity, acdb_modeler_geometry, acdb_surface
    )

    @override
    def load_dxf_attribs(
        self, processor: Optional[SubclassProcessor] = None
    ) -> DXFNamespace:
        dxf = super().load_dxf_attribs(processor)
        if processor:
            processor.fast_load_dxfattribs(dxf, acdb_surface_group_codes, 3)
        return dxf

    @override
    def export_entity(self, tagwriter: AbstractTagWriter) -> None:
        """Export entity specific data as DXF tags."""
        # base class export is done by parent class
        super().export_entity(tagwriter)
        # AcDbEntity export is done by parent class
        # AcDbModelerGeometry export is done by parent class
        tagwriter.write_tag2(const.SUBCLASS_MARKER, acdb_surface.name)
        self.dxf.export_dxf_attribs(tagwriter, ["u_count", "v_count"])


acdb_extruded_surface = DefSubclass(
    "AcDbExtrudedSurface",
    {
        "class_id": DXFAttr(90),
        "sweep_vector": DXFAttr(10, xtype=XType.point3d),
        # 16x group code 40: Transform matrix of extruded entity (16 floats;
        # row major format; default = identity matrix)
        "draft_angle": DXFAttr(42, default=0.0),  # in radians
        "draft_start_distance": DXFAttr(43, default=0.0),
        "draft_end_distance": DXFAttr(44, default=0.0),
        "twist_angle": DXFAttr(45, default=0.0),  # in radians?
        "scale_factor": DXFAttr(48, default=0.0),
        "align_angle": DXFAttr(49, default=0.0),  # in radians
        # 16x group code 46: Transform matrix of sweep entity (16 floats;
        # row major format; default = identity matrix)
        # 16x group code 47: Transform matrix of path entity (16 floats;
        # row major format; default = identity matrix)
        "solid": DXFAttr(290, default=0),  # bool
        # 0=No alignment; 1=Align sweep entity to path:
        "sweep_alignment_flags": DXFAttr(70, default=0),
        "unknown1": DXFAttr(71, default=0),
        # 2=Translate sweep entity to path; 3=Translate path to sweep entity:
        "align_start": DXFAttr(292, default=0),  # bool
        "bank": DXFAttr(293, default=0),  # bool
        "base_point_set": DXFAttr(294, default=0),  # bool
        "sweep_entity_transform_computed": DXFAttr(295, default=0),  # bool
        "path_entity_transform_computed": DXFAttr(296, default=0),  # bool
        "reference_vector_for_controlling_twist": DXFAttr(11, xtype=XType.point3d),
    },
)
acdb_extruded_surface_group_codes = group_code_mapping(acdb_extruded_surface)


@register_entity
class ExtrudedSurface(Surface):
    """DXF EXTRUDEDSURFACE entity - container entity for embedded ACIS data."""

    DXFTYPE = "EXTRUDEDSURFACE"
    DXFATTRIBS = DXFAttributes(
        base_class,
        acdb_entity,
        acdb_modeler_geometry,
        acdb_surface,
        acdb_extruded_surface,
    )

    def __init__(self):
        super().__init__()
        self.transformation_matrix_extruded_entity = Matrix44()
        self.sweep_entity_transformation_matrix = Matrix44()
        self.path_entity_transformation_matrix = Matrix44()

    @override
    def copy_data(self, entity: Self, copy_strategy=default_copy) -> None:
        assert isinstance(entity, ExtrudedSurface)
        super().copy_data(entity, copy_strategy)
        entity.transformation_matrix_extruded_entity = (
            self.transformation_matrix_extruded_entity.copy()
        )
        entity.sweep_entity_transformation_matrix = (
            self.sweep_entity_transformation_matrix.copy()
        )
        entity.path_entity_transformation_matrix = (
            self.path_entity_transformation_matrix.copy()
        )

    @override
    def load_dxf_attribs(
        self, processor: Optional[SubclassProcessor] = None
    ) -> DXFNamespace:
        dxf = super().load_dxf_attribs(processor)
        if processor:
            processor.fast_load_dxfattribs(
                dxf, acdb_extruded_surface_group_codes, 4, log=False
            )
            self.load_matrices(processor.subclasses[4])
        return dxf

    def load_matrices(self, tags: Tags):
        self.transformation_matrix_extruded_entity = load_matrix(tags, code=40)
        self.sweep_entity_transformation_matrix = load_matrix(tags, code=46)
        self.path_entity_transformation_matrix = load_matrix(tags, code=47)

    @override
    def export_entity(self, tagwriter: AbstractTagWriter) -> None:
        """Export entity specific data as DXF tags."""
        # base class export is done by parent class
        super().export_entity(tagwriter)
        # AcDbEntity export is done by parent class
        # AcDbModelerGeometry export is done by parent class
        tagwriter.write_tag2(const.SUBCLASS_MARKER, acdb_extruded_surface.name)
        self.dxf.export_dxf_attribs(tagwriter, ["class_id", "sweep_vector"])
        export_matrix(
            tagwriter,
            code=40,
            matrix=self.transformation_matrix_extruded_entity,
        )
        self.dxf.export_dxf_attribs(
            tagwriter,
            [
                "draft_angle",
                "draft_start_distance",
                "draft_end_distance",
                "twist_angle",
                "scale_factor",
                "align_angle",
            ],
        )
        export_matrix(
            tagwriter, code=46, matrix=self.sweep_entity_transformation_matrix
        )
        export_matrix(tagwriter, code=47, matrix=self.path_entity_transformation_matrix)
        self.dxf.export_dxf_attribs(
            tagwriter,
            [
                "solid",
                "sweep_alignment_flags",
                "unknown1",
                "align_start",
                "bank",
                "base_point_set",
                "sweep_entity_transform_computed",
                "path_entity_transform_computed",
                "reference_vector_for_controlling_twist",
            ],
        )


acdb_lofted_surface = DefSubclass(
    "AcDbLoftedSurface",
    {
        # 16x group code 40: Transform matrix of loft entity (16 floats;
        # row major format; default = identity matrix)
        "plane_normal_lofting_type": DXFAttr(70),
        "start_draft_angle": DXFAttr(41, default=0.0),  # in radians
        "end_draft_angle": DXFAttr(42, default=0.0),  # in radians
        "start_draft_magnitude": DXFAttr(43, default=0.0),
        "end_draft_magnitude": DXFAttr(44, default=0.0),
        "arc_length_parameterization": DXFAttr(290, default=0),  # bool
        "no_twist": DXFAttr(291, default=1),  # true/false
        "align_direction": DXFAttr(292, default=1),  # bool
        "simple_surfaces": DXFAttr(293, default=1),  # bool
        "closed_surfaces": DXFAttr(294, default=0),  # bool
        "solid": DXFAttr(295, default=0),  # true/false
        "ruled_surface": DXFAttr(296, default=0),  # bool
        "virtual_guide": DXFAttr(297, default=0),  # bool
    },
)
acdb_lofted_surface_group_codes = group_code_mapping(acdb_lofted_surface)


@register_entity
class LoftedSurface(Surface):
    """DXF LOFTEDSURFACE entity - container entity for embedded ACIS data."""

    DXFTYPE = "LOFTEDSURFACE"
    DXFATTRIBS = DXFAttributes(
        base_class,
        acdb_entity,
        acdb_modeler_geometry,
        acdb_surface,
        acdb_lofted_surface,
    )

    def __init__(self):
        super().__init__()
        self.transformation_matrix_lofted_entity = Matrix44()

    @override
    def copy_data(self, entity: Self, copy_strategy=default_copy) -> None:
        assert isinstance(entity, LoftedSurface)
        super().copy_data(entity, copy_strategy)
        entity.transformation_matrix_lofted_entity = (
            self.transformation_matrix_lofted_entity.copy()
        )

    @override
    def load_dxf_attribs(
        self, processor: Optional[SubclassProcessor] = None
    ) -> DXFNamespace:
        dxf = super().load_dxf_attribs(processor)
        if processor:
            processor.fast_load_dxfattribs(
                dxf, acdb_lofted_surface_group_codes, 4, log=False
            )
            self.load_matrices(processor.subclasses[4])
        return dxf

    def load_matrices(self, tags: Tags):
        self.transformation_matrix_lofted_entity = load_matrix(tags, code=40)

    @override
    def export_entity(self, tagwriter: AbstractTagWriter) -> None:
        """Export entity specific data as DXF tags."""
        # base class export is done by parent class
        super().export_entity(tagwriter)
        # AcDbEntity export is done by parent class
        # AcDbModelerGeometry export is done by parent class
        tagwriter.write_tag2(const.SUBCLASS_MARKER, acdb_lofted_surface.name)
        export_matrix(
            tagwriter, code=40, matrix=self.transformation_matrix_lofted_entity
        )
        self.dxf.export_dxf_attribs(tagwriter, acdb_lofted_surface.attribs.keys())


acdb_revolved_surface = DefSubclass(
    "AcDbRevolvedSurface",
    {
        "class_id": DXFAttr(90, default=0.0),
        "axis_point": DXFAttr(10, xtype=XType.point3d),
        "axis_vector": DXFAttr(11, xtype=XType.point3d),
        "revolve_angle": DXFAttr(40),  # in radians
        "start_angle": DXFAttr(41),  # in radians
        # 16x group code 42: Transform matrix of revolved entity (16 floats;
        # row major format; default = identity matrix)
        "draft_angle": DXFAttr(43),  # in radians
        "start_draft_distance": DXFAttr(44, default=0),
        "end_draft_distance": DXFAttr(45, default=0),
        "twist_angle": DXFAttr(46, default=0),  # in radians
        "solid": DXFAttr(290, default=0),  # bool
        "close_to_axis": DXFAttr(291, default=0),  # bool
    },
)
acdb_revolved_surface_group_codes = group_code_mapping(acdb_revolved_surface)


@register_entity
class RevolvedSurface(Surface):
    """DXF REVOLVEDSURFACE entity - container entity for embedded ACIS data."""

    DXFTYPE = "REVOLVEDSURFACE"
    DXFATTRIBS = DXFAttributes(
        base_class,
        acdb_entity,
        acdb_modeler_geometry,
        acdb_surface,
        acdb_revolved_surface,
    )

    def __init__(self):
        super().__init__()
        self.transformation_matrix_revolved_entity = Matrix44()

    @override
    def copy_data(self, entity: Self, copy_strategy=default_copy) -> None:
        assert isinstance(entity, RevolvedSurface)
        super().copy_data(entity, copy_strategy)
        entity.transformation_matrix_revolved_entity = (
            self.transformation_matrix_revolved_entity.copy()
        )

    @override
    def load_dxf_attribs(
        self, processor: Optional[SubclassProcessor] = None
    ) -> DXFNamespace:
        dxf = super().load_dxf_attribs(processor)
        if processor:
            processor.fast_load_dxfattribs(
                dxf, acdb_revolved_surface_group_codes, 4, log=False
            )
            self.load_matrices(processor.subclasses[4])
        return dxf

    def load_matrices(self, tags: Tags):
        self.transformation_matrix_revolved_entity = load_matrix(tags, code=42)

    @override
    def export_entity(self, tagwriter: AbstractTagWriter) -> None:
        """Export entity specific data as DXF tags."""
        # base class export is done by parent class
        super().export_entity(tagwriter)
        # AcDbEntity export is done by parent class
        # AcDbModelerGeometry export is done by parent class
        tagwriter.write_tag2(const.SUBCLASS_MARKER, acdb_revolved_surface.name)
        self.dxf.export_dxf_attribs(
            tagwriter,
            [
                "class_id",
                "axis_point",
                "axis_vector",
                "revolve_angle",
                "start_angle",
            ],
        )
        export_matrix(
            tagwriter,
            code=42,
            matrix=self.transformation_matrix_revolved_entity,
        )
        self.dxf.export_dxf_attribs(
            tagwriter,
            [
                "draft_angle",
                "start_draft_distance",
                "end_draft_distance",
                "twist_angle",
                "solid",
                "close_to_axis",
            ],
        )


acdb_swept_surface = DefSubclass(
    "AcDbSweptSurface",
    {
        "swept_entity_id": DXFAttr(90),
        # 90: size of binary data (lost on saving)
        # 310: binary data  (lost on saving)
        "path_entity_id": DXFAttr(91),
        # 90: size of binary data  (lost on saving)
        # 310: binary data  (lost on saving)
        # 16x group code 40: Transform matrix of sweep entity (16 floats;
        # row major format; default = identity matrix)
        # 16x group code 41: Transform matrix of path entity (16 floats;
        # row major format; default = identity matrix)
        "draft_angle": DXFAttr(42),  # in radians
        "draft_start_distance": DXFAttr(43, default=0),
        "draft_end_distance": DXFAttr(44, default=0),
        "twist_angle": DXFAttr(45, default=0),  # in radians
        "scale_factor": DXFAttr(48, default=1),
        "align_angle": DXFAttr(49, default=0),  # in radians
        # don't know the meaning of this matrices
        # 16x group code 46: Transform matrix of sweep entity (16 floats;
        # row major format; default = identity matrix)
        # 16x group code 47: Transform matrix of path entity (16 floats;
        # row major format; default = identity matrix)
        "solid": DXFAttr(290, default=0),  # in radians
        # 0=No alignment; 1= align sweep entity to path:
        "sweep_alignment": DXFAttr(70, default=0),
        "unknown1": DXFAttr(71, default=0),
        # 2=Translate sweep entity to path; 3=Translate path to sweep entity:
        "align_start": DXFAttr(292, default=0),  # bool
        "bank": DXFAttr(293, default=0),  # bool
        "base_point_set": DXFAttr(294, default=0),  # bool
        "sweep_entity_transform_computed": DXFAttr(295, default=0),  # bool
        "path_entity_transform_computed": DXFAttr(296, default=0),  # bool
        "reference_vector_for_controlling_twist": DXFAttr(11, xtype=XType.point3d),
    },
)
acdb_swept_surface_group_codes = group_code_mapping(acdb_swept_surface)


@register_entity
class SweptSurface(Surface):
    """DXF SWEPTSURFACE entity - container entity for embedded ACIS data."""

    DXFTYPE = "SWEPTSURFACE"
    DXFATTRIBS = DXFAttributes(
        base_class,
        acdb_entity,
        acdb_modeler_geometry,
        acdb_surface,
        acdb_swept_surface,
    )

    def __init__(self):
        super().__init__()
        self.transformation_matrix_sweep_entity = Matrix44()
        self.transformation_matrix_path_entity = Matrix44()
        self.sweep_entity_transformation_matrix = Matrix44()
        self.path_entity_transformation_matrix = Matrix44()

    @override
    def copy_data(self, entity: Self, copy_strategy=default_copy) -> None:
        assert isinstance(entity, SweptSurface)
        super().copy_data(entity, copy_strategy)
        entity.transformation_matrix_sweep_entity = (
            self.transformation_matrix_sweep_entity.copy()
        )
        entity.transformation_matrix_path_entity = (
            self.transformation_matrix_path_entity.copy()
        )
        entity.sweep_entity_transformation_matrix = (
            self.sweep_entity_transformation_matrix.copy()
        )
        entity.path_entity_transformation_matrix = (
            self.path_entity_transformation_matrix.copy()
        )

    @override
    def load_dxf_attribs(
        self, processor: Optional[SubclassProcessor] = None
    ) -> DXFNamespace:
        dxf = super().load_dxf_attribs(processor)
        if processor:
            processor.fast_load_dxfattribs(
                dxf, acdb_swept_surface_group_codes, 4, log=False
            )
            self.load_matrices(processor.subclasses[4])
        return dxf

    def load_matrices(self, tags: Tags):
        self.transformation_matrix_sweep_entity = load_matrix(tags, code=40)
        self.transformation_matrix_path_entity = load_matrix(tags, code=41)
        self.sweep_entity_transformation_matrix = load_matrix(tags, code=46)
        self.path_entity_transformation_matrix = load_matrix(tags, code=47)

    @override
    def export_entity(self, tagwriter: AbstractTagWriter) -> None:
        """Export entity specific data as DXF tags."""
        # base class export is done by parent class
        super().export_entity(tagwriter)
        # AcDbEntity export is done by parent class
        # AcDbModelerGeometry export is done by parent class
        tagwriter.write_tag2(const.SUBCLASS_MARKER, acdb_swept_surface.name)
        self.dxf.export_dxf_attribs(
            tagwriter,
            [
                "swept_entity_id",
                "path_entity_id",
            ],
        )
        export_matrix(
            tagwriter, code=40, matrix=self.transformation_matrix_sweep_entity
        )
        export_matrix(tagwriter, code=41, matrix=self.transformation_matrix_path_entity)
        self.dxf.export_dxf_attribs(
            tagwriter,
            [
                "draft_angle",
                "draft_start_distance",
                "draft_end_distance",
                "twist_angle",
                "scale_factor",
                "align_angle",
            ],
        )

        export_matrix(
            tagwriter, code=46, matrix=self.sweep_entity_transformation_matrix
        )
        export_matrix(tagwriter, code=47, matrix=self.path_entity_transformation_matrix)
        self.dxf.export_dxf_attribs(
            tagwriter,
            [
                "solid",
                "sweep_alignment",
                "unknown1",
                "align_start",
                "bank",
                "base_point_set",
                "sweep_entity_transform_computed",
                "path_entity_transform_computed",
                "reference_vector_for_controlling_twist",
            ],
        )
