# Copyright (c) 2018-2022, Manfred Moitzi
# License: MIT License
from __future__ import annotations
from typing import TYPE_CHECKING, Union, Optional, Iterable, Any, Iterator
import copy
import logging
from collections import namedtuple

from ezdxf.lldxf import const
from ezdxf.lldxf.types import cast_value
from ezdxf.lldxf.attributes import (
    DXFAttr,
    DXFAttributes,
    DefSubclass,
    XType,
    group_code_mapping,
)
from ezdxf.lldxf.tags import Tags
from ezdxf.lldxf.const import EXT_MAX_STR_LEN
from ezdxf.math import (
    Vec3,
    NULLVEC,
    X_AXIS,
    Y_AXIS,
    Z_AXIS,
    Matrix44,
    WCSTransform,
    OCSTransform,
    NonUniformScalingError,
)
from ezdxf import colors
from ezdxf.proxygraphic import ProxyGraphicError
from ezdxf.tools.text import safe_string
from ezdxf.tools.handle import safe_handle

from .dxfentity import base_class, SubclassProcessor
from .dxfobj import DXFObject
from .dxfgfx import DXFGraphic, acdb_entity

from .factory import register_entity
from .objectcollection import ObjectCollection

if TYPE_CHECKING:
    from ezdxf.audit import Auditor
    from ezdxf.document import Drawing
    from ezdxf.entities import DXFNamespace, DXFEntity
    from ezdxf.layouts import BaseLayout
    from ezdxf.lldxf.tagwriter import AbstractTagWriter
    from ezdxf.lldxf.types import DXFTag
    from ezdxf.query import EntityQuery

__all__ = [
    "MultiLeader",
    "MLeader",
    "MLeaderStyle",
    "MLeaderStyleCollection",
    "MLeaderContext",
    "MTextData",
    "BlockData",
    "LeaderData",
    "LeaderLine",
    "ArrowHeadData",
    "AttribData",
]
logger = logging.getLogger("ezdxf")


# DXF Examples:
# "D:\source\dxftest\CADKitSamples\house design for two family with common staircasedwg.dxf"
# "D:\source\dxftest\CADKitSamples\house design.dxf"

# How to render MLEADER: https://atlight.github.io/formats/dxf-leader.html
# DXF reference:
# http://help.autodesk.com/view/OARX/2018/ENU/?guid=GUID-72D20B8C-0F5E-4993-BEB7-0FCF94F32BE0

acdb_mleader = DefSubclass(
    "AcDbMLeader",
    {
        "version": DXFAttr(270, default=2),
        "style_handle": DXFAttr(340),
        # Theory: Take properties from MLEADERSTYLE,
        # except explicit overridden here:
        "property_override_flags": DXFAttr(90),
        # Bit coded flags from ODA DWG specs pg.158:
        # 1 << 0 = leader_type
        # 1 << 1 = leader_line_color
        # 1 << 2 = leader_linetype_handle
        # 1 << 3 = leader_lineweight
        # 1 << 4 = has_landing
        # 1 << 5 = landing_gap_size ???
        # 1 << 6 = has_dogleg
        # 1 << 7 = dogleg_length
        # 1 << 8 = arrow_head_handle
        # 1 << 9 = arrow_head_size
        # 1 << 10 = content_type
        # 1 << 11 = text_style_handle
        # 1 << 12 = text_left_attachment_type (of MTEXT)
        # 1 << 13 = text_angle_type (of MTEXT)
        # 1 << 14 = text_alignment_type (of MTEXT)
        # 1 << 15 = text_color (of MTEXT)
        # 1 << 16 = ??? Text height (of MTEXT) ???
        # 1 << 17 = has_text_frame
        # 1 << 18 = ??? Enable use of default MTEXT (from MLEADERSTYLE)
        # 1 << 19 = block_record_handle
        # 1 << 20 = block_color
        # 1 << 21 = block_scale_vector
        # 1 << 22 = block_rotation
        # 1 << 23 = block_connection_type
        # 1 << 24 = ??? Scale ???
        # 1 << 25 = text_right_attachment_type (of MTEXT)
        # 1 << 26 = ??? Text switch alignment type (of MTEXT) ???
        # 1 << 27 = text_attachment_direction (of MTEXT)
        # 1 << 28 = text_top_attachment_type (of MTEXT)
        # 1 << 29 = Text_bottom_attachment_type (of MTEXT)
        # leader_type:
        # 0 = invisible
        # 1 = straight line leader
        # 2 = spline leader
        "leader_type": DXFAttr(170, default=1),
        "leader_line_color": DXFAttr(91, default=colors.BY_BLOCK_RAW_VALUE),
        "leader_linetype_handle": DXFAttr(341),
        "leader_lineweight": DXFAttr(171, default=const.LINEWEIGHT_BYBLOCK),
        "has_landing": DXFAttr(290, default=1),
        "has_dogleg": DXFAttr(291, default=1),
        "dogleg_length": DXFAttr(41, default=8),  # depend on $MEASUREMENT?
        # no handle is default arrow 'closed filled':
        "arrow_head_handle": DXFAttr(342),
        # unscaled arrow head size:
        "arrow_head_size": DXFAttr(42, default=4),  # depend on $MEASUREMENT?
        "content_type": DXFAttr(172, default=2),
        # 0 = None
        # 1 = Block content
        # 2 = MTEXT content
        # 3 = TOLERANCE content
        # Text Content:
        "text_style_handle": DXFAttr(343),
        "text_left_attachment_type": DXFAttr(173, default=1),
        # Values 0-8 are used for the left/right attachment
        # point (attachment direction is horizontal)
        # Attachment point is:
        # 0 = top of top text line
        # 1 = middle of top text line
        # 2 = middle of whole text
        # 3 = middle of bottom text line
        # 4 = bottom of bottom text line
        # 5 = bottom of bottom text line & underline bottom text line
        # 6 = bottom of top text line & underline top text line
        # 7 = bottom of top text line
        # 8 = bottom of top text line & underline all text lines
        "text_right_attachment_type": DXFAttr(95, default=1),  # like 173
        "text_angle_type": DXFAttr(174, default=1),
        # 0 = text angle is equal to last leader line segment angle
        # 1 = text is horizontal
        # 2 = text angle is equal to last leader line segment angle, but potentially
        #     rotated by 180 degrees so the right side is up for readability.
        "text_alignment_type": DXFAttr(175, default=2),
        "text_color": DXFAttr(92, default=colors.BY_BLOCK_RAW_VALUE),
        "has_text_frame": DXFAttr(292, default=0),
        # Block Content:
        "block_record_handle": DXFAttr(344),
        "block_color": DXFAttr(
            93, default=colors.BY_BLOCK_RAW_VALUE
        ),  # raw color
        "block_scale_vector": DXFAttr(
            10, xtype=XType.point3d, default=Vec3(1, 1, 1)
        ),
        "block_rotation": DXFAttr(43, default=0),  # in radians!!!
        "block_connection_type": DXFAttr(176, default=0),
        # 0 = center extents
        # 1 = insertion point
        "is_annotative": DXFAttr(293, default=0),
        # REPEAT "arrow_heads": DXF R2007+
        # arrow_head_index: 94, ???
        # arrow_head_handle: 345
        # END "arrow heads"
        # REPEAT "block attribs" (ATTDEF): DXF R2007+
        # attrib_handle: 330
        # attrib_index: 177, sequential index of the label in the collection
        # attrib_width: 44
        # attrib_text: 302, collision with group code (302, "LEADER{") in context data
        # END "block attribs"
        # Text Content:
        "is_text_direction_negative": DXFAttr(
            294, default=0, dxfversion=const.DXF2007
        ),
        "text_IPE_align": DXFAttr(178, default=0, dxfversion=const.DXF2007),
        "text_attachment_point": DXFAttr(
            179, default=1, dxfversion=const.DXF2007
        ),
        # 1 = left
        # 2 = center
        # 3 = right
        "scale": DXFAttr(45, default=1, dxfversion=const.DXF2007),
        # This defines whether the leaders attach to the left/right of the content
        # block/text, or attach to the top/bottom:
        # 0 = horizontal
        # 1 = vertical
        "text_attachment_direction": DXFAttr(
            271, default=0, dxfversion=const.DXF2010
        ),
        # like 173, but
        # 9 = center
        # 10= underline and center
        "text_bottom_attachment_type": DXFAttr(
            272, default=9, dxfversion=const.DXF2010
        ),
        # like 173, but
        # 9 = center
        # 10= overline and center
        "text_top_attachment_type": DXFAttr(
            273, default=9, dxfversion=const.DXF2010
        ),
        "leader_extend_to_text": DXFAttr(
            295, default=0, dxfversion=const.DXF2013
        ),
    },
)
# The text frame shape is stored in XDATA except for the default rectangle:
# 1001 ACAD
# 1070 <type>
# 2 = rounded rectangle
# 3 = parallelogram
# 4 = triangle
# 5 = square
# 6 = pentagon
# 7 = hexagon
# 8 = octagon
# 9 = circle
# 10= ellipse

acdb_mleader_group_codes = group_code_mapping(acdb_mleader)
CONTEXT_STR = "CONTEXT_DATA{"
LEADER_STR = "LEADER{"
LEADER_LINE_STR = "LEADER_LINE{"
START_CONTEXT_DATA = 300
END_CONTEXT_DATA = 301
START_LEADER = 302
END_LEADER = 303
START_LEADER_LINE = 304
END_LEADER_LINE = 305


def compile_context_tags(
    data: list[DXFTag], stop_code: int
) -> list[Union[DXFTag, list]]:
    def build_structure(tag: DXFTag, stop: int) -> list[Union[DXFTag, list]]:
        collector = [tag]
        tag = next(tags)
        while tag.code != stop:
            if tag.code == START_LEADER:
                collector.append(build_structure(tag, END_LEADER))  # type: ignore
            # Group code 304 is used also for MTEXT content, therefore always
            # test for group code AND and value string:
            elif tag.code == START_LEADER_LINE and tag.value == LEADER_LINE_STR:
                collector.append(build_structure(tag, END_LEADER_LINE))  # type: ignore
            else:
                collector.append(tag)
            tag = next(tags)
        return collector  # type: ignore

    tags = iter(data)
    return build_structure(next(tags), stop_code)


ArrowHeadData = namedtuple("ArrowHeadData", "index, handle")
AttribData = namedtuple("AttribData", "handle, index, width, text")


@register_entity
class MultiLeader(DXFGraphic):
    DXFTYPE = "MULTILEADER"
    DXFATTRIBS = DXFAttributes(base_class, acdb_entity, acdb_mleader)
    MIN_DXF_VERSION_FOR_EXPORT = const.DXF2000

    def __init__(self) -> None:
        super().__init__()
        self.context = MLeaderContext()
        self.arrow_heads: list[ArrowHeadData] = []
        self.block_attribs: list[AttribData] = []

    @property
    def has_mtext_content(self) -> bool:
        return self.context.mtext is not None

    def get_mtext_content(self) -> str:
        """Get MTEXT content as string, return "" if MULTILEADER has
        BLOCK content.
        """
        mtext = self.context.mtext
        if mtext is not None:
            return mtext.default_content
        return ""

    def set_mtext_content(self, text: str):
        """Set MTEXT content as string, does nothing if MULTILEADER has
        BLOCK content.
        """
        mtext = self.context.mtext
        if mtext is not None:
            mtext.default_content = safe_string(text, EXT_MAX_STR_LEN)

    @property
    def has_block_content(self) -> bool:
        return self.context.block is not None

    def get_block_content(self) -> dict[str, str]:
        """Get BLOCK attributes as dictionary of (tag, value) pairs.
        Returns an empty dictionary if MULTILEADER has MTEXT content.
        """
        assert self.doc is not None, "valid DXF document required"
        entitydb = self.doc.entitydb
        tags: dict[str, str] = dict()
        for attr in self.block_attribs:
            attdef = entitydb.get(attr.handle)
            if attdef is not None:
                tags[attdef.dxf.tag] = attr.text
        return tags

    def set_block_content(self, content: dict[str, str]):
        """Set BLOCK attributes by a dictionary of (tag, value) pairs.
        Does nothing if MULTILEADER has MTEXT content.
        """
        assert self.doc is not None, "valid DXF document required"
        entitydb = self.doc.entitydb
        tags: dict[str, str] = dict()
        block_attribs = self.block_attribs
        for index, attr in enumerate(block_attribs):
            attdef = entitydb.get(attr.handle)
            if attdef is not None:
                tag = attdef.dxf.tag
                new_text = safe_string(content.get(tag), EXT_MAX_STR_LEN)
                if new_text is not None:
                    block_attribs[index] = attr._replace(text=new_text)
        return tags

    def _copy_data(self, entity: DXFEntity) -> None:
        """Copy leaders"""
        assert isinstance(entity, MultiLeader)
        entity.context = copy.deepcopy(self.context)
        entity.arrow_heads = copy.deepcopy(self.arrow_heads)
        entity.block_attribs = copy.deepcopy(self.block_attribs)

    def load_dxf_attribs(
        self, processor: Optional[SubclassProcessor] = None
    ) -> DXFNamespace:
        dxf = super().load_dxf_attribs(processor)
        if processor is None:
            return dxf
        tags = processor.subclass_by_index(2)
        if tags:
            context = self.extract_context_data(tags)
            if context:
                try:
                    self.context = self.load_context(context)
                except const.DXFStructureError:
                    logger.info(
                        f"Context structure error in entity MULTILEADER(#{dxf.handle})"
                    )
        else:
            raise const.DXFStructureError(
                f"missing 'AcDbMLeader' subclass in MULTILEADER(#{dxf.handle})"
            )

        self.arrow_heads = self.extract_arrow_heads(tags)
        self.block_attribs = self.extract_block_attribs(tags)

        processor.fast_load_dxfattribs(
            dxf, acdb_mleader_group_codes, subclass=tags, recover=True
        )
        return dxf

    @staticmethod
    def extract_context_data(tags: Tags) -> list[DXFTag]:
        start, end = None, None
        context_data = []
        for index, tag in enumerate(tags):
            if tag.code == START_CONTEXT_DATA:
                start = index
            elif tag.code == END_CONTEXT_DATA:
                end = index + 1

        if start and end:
            context_data = tags[start:end]
            # Remove context data!
            del tags[start:end]
        return context_data

    @staticmethod
    def load_context(data: list[DXFTag]) -> MLeaderContext:
        try:
            context = compile_context_tags(data, END_CONTEXT_DATA)
        except StopIteration:
            raise const.DXFStructureError
        else:
            return MLeaderContext.load(context)

    @staticmethod
    def extract_arrow_heads(data: Tags) -> list[ArrowHeadData]:
        def store_head():
            heads.append(
                ArrowHeadData(
                    collector.get(94, 0),  # arrow head index
                    collector.get(345, "0"),  # arrow head handle
                )
            )
            collector.clear()

        heads: list[ArrowHeadData] = []
        try:
            start = data.tag_index(94)
        except const.DXFValueError:
            return heads

        end = start
        collector = dict()
        for code, value in data.collect_consecutive_tags({94, 345}, start):
            end += 1
            collector[code] = value
            if code == 345:
                store_head()

        # Remove processed tags:
        del data[start:end]
        return heads

    @staticmethod
    def extract_block_attribs(data: Tags) -> list[AttribData]:
        def store_attrib():
            attribs.append(
                AttribData(
                    collector.get(330, "0"),  # ATTDEF handle
                    collector.get(177, 0),  # ATTDEF index
                    collector.get(44, 1.0),  # ATTDEF width
                    collector.get(302, ""),  # ATTDEF text (content)
                )
            )
            collector.clear()

        attribs: list[AttribData] = []
        try:
            start = data.tag_index(330)
        except const.DXFValueError:
            return attribs

        end = start
        collector: dict[int, Any] = dict()
        for code, value in data.collect_consecutive_tags(
            {330, 177, 44, 302}, start
        ):
            end += 1
            if code == 330 and len(collector):
                store_attrib()
            collector[code] = value
        if len(collector):
            store_attrib()

        # Remove processed tags:
        del data[start:end]
        return attribs

    def preprocess_export(self, tagwriter: AbstractTagWriter) -> bool:
        if self.context.is_valid:
            return True
        else:
            logger.debug(
                f"Ignore {str(self)} at DXF export, invalid context data."
            )
            return False

    def export_entity(self, tagwriter: AbstractTagWriter) -> None:
        def write_handle_if_exist(code: int, name: str):
            handle = dxf.get(name)
            if handle is not None:
                write_tag2(code, handle)

        super().export_entity(tagwriter)
        dxf = self.dxf
        version = tagwriter.dxfversion
        write_tag2 = tagwriter.write_tag2

        write_tag2(100, acdb_mleader.name)
        write_tag2(270, dxf.version)
        self.context.export_dxf(tagwriter)

        # Export common MLEADER tags:
        # Don't use dxf.export_dxf_attribs() - all attributes should be written
        # even if equal to the default value:
        write_tag2(340, dxf.style_handle)
        write_tag2(90, dxf.property_override_flags)
        write_tag2(170, dxf.leader_type)
        write_tag2(91, dxf.leader_line_color)
        write_tag2(341, dxf.leader_linetype_handle)
        write_tag2(171, dxf.leader_lineweight)
        write_tag2(290, dxf.has_landing)
        write_tag2(291, dxf.has_dogleg)
        write_tag2(41, dxf.dogleg_length)
        # arrow_head_handle is None for default arrow 'closed filled':
        write_handle_if_exist(342, "arrow_head_handle")
        write_tag2(42, dxf.arrow_head_size)
        write_tag2(172, dxf.content_type)
        write_tag2(343, dxf.text_style_handle)  # mandatory!
        write_tag2(173, dxf.text_left_attachment_type)
        write_tag2(95, dxf.text_right_attachment_type)
        write_tag2(174, dxf.text_angle_type)
        write_tag2(175, dxf.text_alignment_type)
        write_tag2(92, dxf.text_color)
        write_tag2(292, dxf.has_text_frame)

        write_handle_if_exist(344, "block_record_handle")
        write_tag2(93, dxf.block_color)
        tagwriter.write_vertex(10, dxf.block_scale_vector)
        write_tag2(43, dxf.block_rotation)
        write_tag2(176, dxf.block_connection_type)
        write_tag2(293, dxf.is_annotative)
        if version >= const.DXF2007:
            self.export_arrow_heads(tagwriter)
            self.export_block_attribs(tagwriter)
            write_tag2(294, dxf.is_text_direction_negative)
            write_tag2(178, dxf.text_IPE_align)
            write_tag2(179, dxf.text_attachment_point)
            write_tag2(45, dxf.scale)

        if version >= const.DXF2010:
            write_tag2(271, dxf.text_attachment_direction)
            write_tag2(272, dxf.text_bottom_attachment_type)
            write_tag2(273, dxf.text_top_attachment_type)

        if version >= const.DXF2013:
            write_tag2(295, dxf.leader_extend_to_text)

    def export_arrow_heads(self, tagwriter: AbstractTagWriter) -> None:
        for index, handle in self.arrow_heads:
            tagwriter.write_tag2(94, index)
            tagwriter.write_tag2(345, handle)

    def export_block_attribs(self, tagwriter: AbstractTagWriter) -> None:
        for attrib in self.block_attribs:
            tagwriter.write_tag2(330, attrib.handle)
            tagwriter.write_tag2(177, attrib.index)
            tagwriter.write_tag2(44, attrib.width)
            tagwriter.write_tag2(302, safe_string(attrib.text, EXT_MAX_STR_LEN))

    def virtual_entities(self) -> Iterator[DXFGraphic]:
        """Yields the graphical representation of MULTILEADER as virtual DXF
        primitives.

        These entities are located at the original location, but are not stored
        in the entity database, have no handle and are not assigned to any
        layout.

        """
        return self.__virtual_entities__()

    def explode(
        self, target_layout: Optional[BaseLayout] = None
    ) -> EntityQuery:
        """Explode MULTILEADER as DXF primitives into target layout,
        if target layout is ``None``, the target layout is the layout of the
        source entity.

        Returns an :class:`~ezdxf.query.EntityQuery` container with all DXF
        primitives.

        Args:
            target_layout: target layout for the DXF primitives, ``None`` for
                same layout as the source entity.

        """
        from ezdxf.explode import explode_entity

        return explode_entity(self, target_layout)

    def __virtual_entities__(self) -> Iterator[DXFGraphic]:
        """Support for "VirtualEntities" protocol."""
        from ezdxf.render import mleader

        try:
            return mleader.virtual_entities(self, proxy_graphic=True)
        except ProxyGraphicError:
            return mleader.virtual_entities(self, proxy_graphic=False)

    def __referenced_blocks__(self) -> Iterable[str]:
        """Support for "ReferencedBlocks" protocol."""
        # 1. overridden MLEADERSTYLE attributes
        for name in ("block_record_handle", "arrow_head_handle"):
            handle = self.dxf.get(name, None)
            if handle is not None:
                yield handle

        # 2. arrows heads
        for arrow_head in self.arrow_heads:
            yield arrow_head.handle

        # 3. MLeader context
        if self.context.block is not None:
            handle = self.context.block.block_record_handle
            if handle is not None:
                yield handle

    def transform(self, m: Matrix44) -> MultiLeader:
        """Transform the MULTILEADER entity by transformation matrix `m` inplace.

        Non-uniform scaling is not supported.

        Args:
            m: transformation :class:`~ezdxf.math.Matrix44`

        Raises:
            NonUniformScalingError: for non uniform scaling

        """
        dxf = self.dxf
        context = self.context

        wcs = WCSTransform(m)
        if not wcs.has_uniform_xy_scaling:
            # caller has to catch this exception and explode the MULTILEADER
            raise NonUniformScalingError(
                "MULTILEADER does not support non uniform scaling"
            )
        if (
            abs(context.plane_x_axis.z) > 1e-12
            or abs(context.plane_y_axis.z) > 1e-12
        ):
            # check only if not parallel to xy-plane
            if not wcs.has_uniform_xyz_scaling:
                # caller has to catch this exception and explode the MULTILEADER
                raise NonUniformScalingError(
                    "MULTILEADER does not support non uniform scaling"
                )
        context.transform(wcs)
        # copy redundant attributes from sub-structures:
        dxf.arrow_head_size = context.arrow_head_size
        dxf.scale = context.scale
        dxf.dogleg_length *= wcs.uniform_scale
        if context.block:
            dxf.block_rotation = context.block.rotation
            dxf.block_scale_vector = context.block.scale

        # ArrowHeadData: no transformation needed
        # AttribData: no transformation needed
        self.post_transform(m)

        # The proxy graphic is stored in absolute WCS coordinates and does not
        # represent the new transformed geometry.
        self.update_proxy_graphic()
        return self

    def update_proxy_graphic(self):
        # Maybe this is supported in the future - its very unlikeLy!
        self.proxy_graphic = None


@register_entity
class MLeader(MultiLeader):  # same entity different name
    DXFTYPE = "MLEADER"


class MLeaderContext:
    ATTRIBS = {
        40: "scale",
        10: "base_point",
        41: "char_height",
        140: "arrow_head_size",
        145: "landing_gap_size",
        174: "left_attachment",
        175: "right_attachment",
        176: "text_align_type",
        177: "attachment_type",
        110: "plane_origin",
        111: "plane_x_axis",
        112: "plane_y_axis",
        297: "plane_normal_reversed",
        272: "top_attachment",
        273: "bottom_attachment",
    }

    def __init__(self) -> None:
        self.leaders: list[LeaderData] = []
        self.scale: float = 1.0  # overall scale

        # MTEXT base point: is not the MTEXT insertion point!
        # HORIZONTAL leader attachment:
        # the "base_point" is always the start point of the leader on the LEFT side
        # of the MTEXT, regardless of alignment and which side leaders are attached.
        # VERTICAL leader attachment:
        # the "base_point" is always the start point of the leader on the BOTTOM
        # side of the MTEXT, regardless of alignment and which side leaders are
        # attached.

        # BLOCK base point: is not the BLOCK insertion point!
        # HORIZONTAL leader attachment:
        # Strange results, setting the "base_point" to the left center of the BLOCK,
        # regardless which side leaders are attached, seems to be reasonable.
        # VERTICAL leader attachment: not supported by BricsCAD
        self.base_point: Vec3 = NULLVEC
        self.char_height = 4.0  # scaled char height!
        self.arrow_head_size = 4.0
        self.landing_gap_size = 2.0
        self.left_attachment = 1
        self.right_attachment = 1
        self.text_align_type = 0  # 0=left, 1=center, 2=right
        self.attachment_type = 0  # 0=content extents, 1=insertion point
        self.mtext: Optional[MTextData] = None
        self.block: Optional[BlockData] = None
        self.plane_origin: Vec3 = NULLVEC
        self.plane_x_axis: Vec3 = X_AXIS
        self.plane_y_axis: Vec3 = Y_AXIS
        self.plane_normal_reversed: int = 0
        self.top_attachment = 9
        self.bottom_attachment = 9

    @classmethod
    def load(cls, context: list[Union[DXFTag, list]]) -> MLeaderContext:
        assert context[0] == (START_CONTEXT_DATA, CONTEXT_STR)
        ctx = cls()
        content = None
        for tag in context:
            if isinstance(tag, list):  # Leader()
                ctx.leaders.append(LeaderData.load(tag))
                continue
            # parse context tags
            code, value = tag
            if content:
                if content.parse(code, value):
                    continue
                else:
                    content = None

            if code == 290 and value == 1:
                content = MTextData()
                ctx.mtext = content
            elif code == 296 and value == 1:
                content = BlockData()  # type: ignore
                ctx.block = content
            else:
                name = MLeaderContext.ATTRIBS.get(code)
                if name:
                    ctx.__setattr__(name, cast_value(code, value))
        return ctx

    @property
    def is_valid(self) -> bool:
        return True

    @property
    def plane_z_axis(self) -> Vec3:
        z_axis = self.plane_x_axis.cross(self.plane_y_axis).normalize()
        if self.plane_normal_reversed:
            z_axis = -z_axis
        return z_axis

    def export_dxf(self, tagwriter: AbstractTagWriter) -> None:
        write_tag2 = tagwriter.write_tag2
        write_vertex = tagwriter.write_vertex
        write_tag2(START_CONTEXT_DATA, CONTEXT_STR)
        # All MLeaderContext tags:
        write_tag2(40, self.scale)
        write_vertex(10, self.base_point)
        write_tag2(41, self.char_height)
        write_tag2(140, self.arrow_head_size)
        write_tag2(145, self.landing_gap_size)
        write_tag2(174, self.left_attachment)
        write_tag2(175, self.right_attachment)
        write_tag2(176, self.text_align_type)
        write_tag2(177, self.attachment_type)

        if self.mtext:
            write_tag2(290, 1)  # has mtext content
            self.mtext.export_dxf(tagwriter)
        else:
            write_tag2(290, 0)

        if self.block:
            write_tag2(296, 1)  # has block content
            self.block.export_dxf(tagwriter)
        else:
            write_tag2(296, 0)

        write_vertex(110, self.plane_origin)
        write_vertex(111, self.plane_x_axis)
        write_vertex(112, self.plane_y_axis)
        write_tag2(297, self.plane_normal_reversed)

        # Export Leader and LiederLine objects:
        for leader in self.leaders:
            leader.export_dxf(tagwriter)

        # Additional MLeaderContext tags:
        if tagwriter.dxfversion >= const.DXF2010:
            write_tag2(272, self.top_attachment)
            write_tag2(273, self.bottom_attachment)
        write_tag2(END_CONTEXT_DATA, "}")

    def transform(self, wcs: WCSTransform) -> None:
        m = wcs.m
        scale = wcs.uniform_scale
        self.scale *= scale
        self.base_point = m.transform(self.base_point)
        self.char_height *= scale
        self.arrow_head_size *= scale
        self.landing_gap_size *= scale
        self.plane_origin = m.transform(
            self.plane_origin
        )  # confirmed by BricsCAD
        self.plane_x_axis = m.transform_direction(
            self.plane_x_axis, normalize=True
        )
        self.plane_y_axis = m.transform_direction(
            self.plane_y_axis, normalize=True
        )
        self.plane_normal_reversed = 0
        z_axis = m.transform_direction(Z_AXIS, normalize=True)
        if z_axis.isclose(-self.plane_z_axis):  # reversed z-axis?
            self.plane_normal_reversed = 1

        for leader in self.leaders:
            leader.transform(wcs)
        if self.mtext is not None:
            self.mtext.transform(wcs)
        if self.block is not None:
            self.block.transform(wcs)

    def set_scale(self, value: float):
        try:
            conversion_factor = value / self.scale
        except ZeroDivisionError:
            return
        self.scale = value
        self.char_height *= conversion_factor
        self.arrow_head_size *= conversion_factor
        self.landing_gap_size *= conversion_factor


class MTextData:
    ATTRIBS = {
        304: "default_content",
        11: "extrusion",
        340: "style_handle",
        12: "insert",
        13: "text_direction",
        42: "rotation",
        43: "width",
        44: "defined_height",
        45: "line_spacing_factor",
        170: "line_spacing_style",
        90: "color",
        171: "alignment",
        172: "flow_direction",
        91: "bg_color",
        141: "bg_scale_factor",
        92: "bg_transparency",
        291: "use_window_bg_color",
        292: "has_bg_fill",
        173: "column_type",
        293: "use_auto_height",
        142: "column_width",
        143: "column_gutter_width",
        294: "column_flow_reversed",
        144: "column_sizes",  # multiple values
        295: "use_word_break",
    }

    def __init__(self) -> None:
        self.default_content: str = ""
        self.extrusion: Vec3 = Z_AXIS
        self.style_handle: str = "0"  # handle of TextStyle() table entry
        self.insert: Vec3 = NULLVEC
        self.text_direction: Vec3 = X_AXIS  # text direction
        self.rotation: float = 0.0  # in radians!
        self.width: float = 0.0  # MTEXT width, not scaled
        self.defined_height: float = 0.0  # defined column height, not scaled
        self.line_spacing_factor: float = 1.0
        self.line_spacing_style: int = 1  # 1=at least, 2=exactly
        self.color: int = colors.BY_BLOCK_RAW_VALUE
        self.alignment: int = 1  # 1=left, 2=center, 3=right
        self.flow_direction: int = 1  # 1=horiz, 3=vert, 6=by style
        self.bg_color: int = colors.WINDOW_BG_RAW_VALUE
        self.bg_scale_factor: float = 1.5
        self.bg_transparency: int = 0
        self.use_window_bg_color: int = 0
        self.has_bg_fill: int = 0
        self.column_type: int = 0  # unknown values
        self.use_auto_height: int = 0
        self.column_width: float = 0.0  # not scaled
        self.column_gutter_width: float = 0.0  # not scaled
        self.column_flow_reversed: int = 0
        self.column_sizes: list[float] = []  # heights?, not scaled
        self.use_word_break: int = 1

    def parse(self, code: int, value) -> bool:
        # return True if data belongs to mtext else False (end of mtext section)
        if code == 144:
            self.column_sizes.append(float(value))
            return True
        attrib = MTextData.ATTRIBS.get(code)
        if attrib:
            self.__setattr__(attrib, cast_value(code, value))
        return bool(attrib)

    def export_dxf(self, tagwriter: AbstractTagWriter) -> None:
        write_tag2 = tagwriter.write_tag2
        write_vertex = tagwriter.write_vertex
        write_tag2(304, safe_string(self.default_content, EXT_MAX_STR_LEN))
        write_vertex(11, self.extrusion)
        write_tag2(340, safe_handle(self.style_handle))
        write_vertex(12, self.insert)
        write_vertex(13, self.text_direction)
        write_tag2(42, self.rotation)
        write_tag2(43, self.width)
        write_tag2(44, self.defined_height)
        write_tag2(45, self.line_spacing_factor)
        write_tag2(170, self.line_spacing_style)
        write_tag2(90, self.color)
        write_tag2(171, self.alignment)
        write_tag2(172, self.flow_direction)
        write_tag2(91, self.bg_color)
        write_tag2(141, self.bg_scale_factor)
        write_tag2(92, self.bg_transparency)
        write_tag2(291, self.use_window_bg_color)
        write_tag2(292, self.has_bg_fill)
        write_tag2(173, self.column_type)
        write_tag2(293, self.use_auto_height)
        write_tag2(142, self.column_width)
        write_tag2(143, self.column_gutter_width)
        write_tag2(294, self.column_flow_reversed)
        for size in self.column_sizes:
            write_tag2(144, size)
        write_tag2(295, self.use_word_break)

    def transform(self, wcs: WCSTransform) -> None:
        # MTEXT is not really an OCS entity!
        m = wcs.m
        ocs = OCSTransform(self.extrusion, m)  # source extrusion!
        self.extrusion = ocs.new_extrusion  # process like an OCS entity!
        self.insert = m.transform(self.insert)  # WCS
        self.text_direction = wcs.m.transform_direction(  # WCS
            self.text_direction, normalize=True
        )
        # don't use rotation ;)
        self.rotation = ocs.transform_angle(self.rotation)
        scale = wcs.uniform_scale
        self.width *= scale
        self.defined_height *= scale
        self.column_width *= scale
        self.column_gutter_width *= scale
        self.column_sizes = [size * scale for size in self.column_sizes]

    def apply_conversion_factor(self, conversion_factor: float):
        # conversion_factor: convert from an old scaling to a new scaling
        self.width *= conversion_factor
        self.defined_height *= conversion_factor
        self.column_width *= conversion_factor
        self.column_gutter_width *= conversion_factor
        self.column_sizes = [h * conversion_factor for h in self.column_sizes]


class BlockData:
    ATTRIBS = {
        341: "block_record_handle",
        14: "extrusion",
        15: "insert",
        16: "scale",
        46: "rotation",
        93: "color",
    }

    def __init__(self) -> None:
        self.block_record_handle: Optional[str] = None
        self.extrusion: Vec3 = Z_AXIS
        self.insert: Vec3 = NULLVEC
        self.scale: Vec3 = Vec3(1, 1, 1)
        self.rotation: float = 0  # in radians!
        self.color: int = colors.BY_BLOCK_RAW_VALUE
        # The transformation matrix is stored in transposed order
        # of ezdxf.math.Matrix44()!
        self._matrix: list[float] = []  # group code 47 x 16

    @property
    def matrix44(self) -> Matrix44:
        if len(self._matrix) == 16:
            m = Matrix44(self._matrix)
        else:
            m = Matrix44()
        m.transpose()
        return m

    @matrix44.setter
    def matrix44(self, m: Matrix44) -> None:
        m = m.copy()
        m.transpose()
        self._matrix = list(m)

    def parse(self, code: int, value) -> bool:
        attrib = BlockData.ATTRIBS.get(code)
        if attrib:
            self.__setattr__(attrib, cast_value(code, value))
        elif code == 47:
            self._matrix.append(float(value))
        else:
            return False
        # return True if data belongs to block else False (end of block section)
        return True

    def export_dxf(self, tagwriter: AbstractTagWriter) -> None:
        write_tag2 = tagwriter.write_tag2
        write_vertex = tagwriter.write_vertex
        if self.block_record_handle:
            write_tag2(341, self.block_record_handle)
        else:
            # Do not write None, but "0" is also not valid!
            # DXF structure error should be detected before export.
            write_tag2(341, "0")
        write_vertex(14, self.extrusion)
        write_vertex(15, self.insert)
        write_vertex(16, self.scale)
        write_tag2(46, self.rotation)
        write_tag2(93, self.color)
        m = self._matrix
        if len(m) != 16:
            # The content of this matrix is not used for block transformation,
            # not by AutoCAD nor by BricsCAD, but BricsCAD gets irritated
            # if this matrix is missing. AutoCAD is fine without the presence
            # of this matrix.
            m = list(Matrix44())  # identity matrix
        for value in m:
            write_tag2(47, value)

    def transform(self, wcs: WCSTransform) -> None:
        # INSERT is an OCS entity, but the insert point is WCS!!!
        m = wcs.m
        ocs = OCSTransform(self.extrusion, m)  # source extrusion!
        self.extrusion = ocs.new_extrusion
        self.insert = m.transform(self.insert)  # WCS coordinates!!!
        self.scale = ocs.transform_scale_vector(self.scale)
        self.rotation = ocs.transform_angle(self.rotation)
        self.matrix44 = self.matrix44 * m

    def apply_conversion_factor(self, conversion_factor: float):
        # conversion_factor: convert from an old scaling to a new scaling
        self.scale *= conversion_factor


class LeaderData:
    def __init__(self) -> None:
        self.lines: list[LeaderLine] = []
        # has_last_leader_line:
        # in AutoCAD the leader is invisible if set to 0
        # BricsCAD ignores this flag
        self.has_last_leader_line: int = 0  # group code 290,
        self.has_dogleg_vector: int = 0  # group code 291
        self.last_leader_point: Vec3 = NULLVEC  # group code (10, 20, 30) in WCS
        self.dogleg_vector: Vec3 = X_AXIS  # group code (11, 21, 31) in WCS
        self.dogleg_length: float = 1.0  # group code 40
        self.index: int = 0  # group code 90

        # 0=horizontal; 1=vertical
        self.attachment_direction: int = 0  # group code 271, R2010+
        self.breaks: list[
            Vec3
        ] = []  # group code 12, 13 - multiple breaks possible!

    @property
    def has_horizontal_attachment(self) -> bool:
        return not bool(self.attachment_direction)

    @classmethod
    def load(cls, context: list[Union[DXFTag, list]]):
        assert context[0] == (START_LEADER, LEADER_STR)
        leader = cls()
        for tag in context:
            if isinstance(tag, list):  # LeaderLine()
                leader.lines.append(LeaderLine.load(tag))
                continue

            code, value = tag
            if code == 290:
                leader.has_last_leader_line = value
            elif code == 291:
                leader.has_dogleg_vector = value
            elif code == 10:
                leader.last_leader_point = Vec3(value)
            elif code == 11:
                leader.dogleg_vector = Vec3(value)
            elif code == 40:
                leader.dogleg_length = value
            elif code == 90:
                leader.index = value
            elif code == 271:
                leader.attachment_direction = value
            elif code in (12, 13):
                leader.breaks.append(Vec3(value))

        return leader

    def export_dxf(self, tagwriter: AbstractTagWriter) -> None:
        write_tag2 = tagwriter.write_tag2
        write_vertex = tagwriter.write_vertex

        write_tag2(START_LEADER, LEADER_STR)
        write_tag2(290, self.has_last_leader_line)
        write_tag2(291, self.has_dogleg_vector)
        write_vertex(10, self.last_leader_point)
        write_vertex(11, self.dogleg_vector)

        code = 0
        for vertex in self.breaks:
            # write alternate group code 12 and 13
            write_vertex(12 + code, vertex)
            code = 1 - code
        write_tag2(90, self.index)
        write_tag2(40, self.dogleg_length)

        # Export leader lines:
        for line in self.lines:
            line.export_dxf(tagwriter)

        if tagwriter.dxfversion >= const.DXF2010:
            write_tag2(271, self.attachment_direction)
        write_tag2(END_LEADER, "}")

    def transform(self, wcs: WCSTransform) -> None:
        m = wcs.m
        self.last_leader_point = m.transform(self.last_leader_point)
        try:
            dog_leg = m.transform_direction(
                self.dogleg_vector.normalize(self.dogleg_length)
            )
        except ZeroDivisionError:  # dogleg_vector is NULL
            dog_leg = m.transform_direction(Vec3(self.dogleg_length, 0, 0))
        self.dogleg_vector = dog_leg.normalize()
        self.dogleg_length = dog_leg.magnitude
        self.breaks = list(m.transform_vertices(self.breaks))
        for leader_line in self.lines:
            leader_line.transform(wcs)


class LeaderLine:
    def __init__(self) -> None:
        self.vertices: list[Vec3] = []  # WCS coordinates
        self.breaks: list[Union[int, Vec3]] = []
        # Breaks: 90, 11, 12, [11, 12, ...] [, 90, 11, 12 [11, 12, ...]]
        # group code 90 = break index
        # group code 11 = start vertex of break
        # group code 12 = end vertex of break
        # multiple breaks per index possible
        self.index: int = 0  # group code 91
        self.color: int = colors.BY_BLOCK_RAW_VALUE  # group code 92
        # R2010+: override properties see ODA DWG pg. 214-215

    @classmethod
    def load(cls, tags: list[DXFTag]):
        assert tags[0] == (START_LEADER_LINE, LEADER_LINE_STR)
        line = LeaderLine()
        vertices = line.vertices
        breaks = []
        for code, value in tags:
            if code == 10:
                vertices.append(Vec3(value))
            elif code in (90, 11, 12):
                breaks.append(cast_value(code, value))
            elif code == 91:
                line.index = value
            elif code == 92:
                line.color = value
        if breaks:
            line.breaks = breaks
        return line

    def export_dxf(self, tagwriter: AbstractTagWriter) -> None:
        write_tag2 = tagwriter.write_tag2
        write_vertex = tagwriter.write_vertex

        write_tag2(START_LEADER_LINE, LEADER_LINE_STR)
        for vertex in self.vertices:
            write_vertex(10, vertex)
        if self.breaks:
            code = 0
            for value in self.breaks:
                if isinstance(value, int):
                    # break index
                    write_tag2(90, value)
                else:
                    # 11 .. start vertex of break
                    # 12 .. end vertex of break
                    write_vertex(11 + code, value)
                    code = 1 - code
        write_tag2(91, self.index)
        write_tag2(92, self.color)
        write_tag2(END_LEADER_LINE, "}")

    def transform(self, wcs: WCSTransform) -> None:
        m = wcs.m
        self.vertices = list(m.transform_vertices(self.vertices))
        if self.breaks:
            breaks: list[Union[int, Vec3]] = []
            for value in self.breaks:
                if isinstance(value, Vec3):
                    breaks.append(m.transform(value))
                else:  # break index
                    breaks.append(value)
            self.breaks = breaks


acdb_mleader_style = DefSubclass(
    "AcDbMLeaderStyle",
    {
        "unknown1": DXFAttr(179, default=2),
        "content_type": DXFAttr(170, default=2),
        "draw_mleader_order_type": DXFAttr(171, default=1),
        "draw_leader_order_type": DXFAttr(172, default=0),
        "max_leader_segments_points": DXFAttr(90, default=2),
        "first_segment_angle_constraint": DXFAttr(40, default=0.0),
        "second_segment_angle_constraint": DXFAttr(41, default=0.0),
        # leader_type:
        # 0 = invisible
        # 1 = straight line leader
        # 2 = spline leader
        "leader_type": DXFAttr(173, default=1),
        "leader_line_color": DXFAttr(91, default=colors.BY_BLOCK_RAW_VALUE),
        "leader_linetype_handle": DXFAttr(340),
        "leader_lineweight": DXFAttr(92, default=-2),
        "has_landing": DXFAttr(290, default=1),
        "landing_gap_size": DXFAttr(42, default=2.0),
        "has_dogleg": DXFAttr(291, default=1),
        "dogleg_length": DXFAttr(43, default=8),
        "name": DXFAttr(3, default="Standard"),  # description text
        # no handle is default arrow 'closed filled':
        "arrow_head_handle": DXFAttr(341),
        "arrow_head_size": DXFAttr(44, default=4),
        "default_text_content": DXFAttr(300, default=""),
        "text_style_handle": DXFAttr(342),
        "text_left_attachment_type": DXFAttr(174, default=1),
        "text_angle_type": DXFAttr(175, default=1),
        "text_alignment_type": DXFAttr(176, default=0),
        "text_right_attachment_type": DXFAttr(178, default=1),
        "text_color": DXFAttr(93, default=colors.BY_BLOCK_RAW_VALUE),
        "char_height": DXFAttr(45, default=4),
        "has_text_frame": DXFAttr(292, default=0),
        "text_align_always_left": DXFAttr(297, default=0),
        "align_space": DXFAttr(46, default=4),
        "has_block_scaling": DXFAttr(293),
        "block_record_handle": DXFAttr(343),
        "block_color": DXFAttr(94, default=colors.BY_BLOCK_RAW_VALUE),
        "block_scale_x": DXFAttr(47, default=1),
        "block_scale_y": DXFAttr(49, default=1),
        "block_scale_z": DXFAttr(140, default=1),
        "has_block_rotation": DXFAttr(294, default=1),
        "block_rotation": DXFAttr(141, default=0),
        "block_connection_type": DXFAttr(177, default=0),
        "scale": DXFAttr(142, default=1),
        "overwrite_property_value": DXFAttr(295, default=0),
        "is_annotative": DXFAttr(296, default=0),
        "break_gap_size": DXFAttr(143, default=3.75),
        # 0 = Horizontal; 1 = Vertical:
        "text_attachment_direction": DXFAttr(271, default=0),
        # 9 = Center; 10 = Underline and Center:
        "text_bottom_attachment_type": DXFAttr(272, default=9),
        # 9 = Center; 10 = Overline and Center:
        "text_top_attachment_type": DXFAttr(273, default=9),
        "unknown2": DXFAttr(298, optional=True),  # boolean flag ?
    },
)
acdb_mleader_style_group_codes = group_code_mapping(acdb_mleader_style)


@register_entity
class MLeaderStyle(DXFObject):
    DXFTYPE = "MLEADERSTYLE"
    DXFATTRIBS = DXFAttributes(base_class, acdb_mleader_style)
    MIN_DXF_VERSION_FOR_EXPORT = const.DXF2000

    def load_dxf_attribs(
        self, processor: Optional[SubclassProcessor] = None
    ) -> DXFNamespace:
        dxf = super().load_dxf_attribs(processor)
        if processor:
            processor.fast_load_dxfattribs(
                dxf, acdb_mleader_style_group_codes, subclass=1
            )
        return dxf

    def set_mtext_style(self, name: str) -> None:
        assert self.doc is not None, "valid DXF document required"
        style = self.doc.styles.get(name)
        if style is not None:
            self.dxf.text_style_handle = style.dxf.handle
        else:
            raise ValueError(f"text style '{name}' does not exist")

    def set_leader_properties(
        self,
        color: Union[int, colors.RGB] = colors.BYBLOCK,
        linetype: str = "BYBLOCK",
        lineweight: int = const.LINEWEIGHT_BYBLOCK,
        leader_type: int = 1,
    ):
        assert self.doc is not None, "valid DXF document required"
        self.dxf.leader_line_color = colors.encode_raw_color(color)  # type: ignore
        linetype_ = self.doc.linetypes.get(linetype)
        if linetype_ is None:
            raise ValueError(f"invalid linetype name '{linetype}'")
        self.dxf.leader_linetype_handle = linetype_.dxf.handle
        self.dxf.leader_lineweight = int(lineweight)
        self.dxf.leader_type = int(leader_type)

    def set_arrow_head(self, name: str = ""):
        from ezdxf.render.arrows import ARROWS

        assert self.doc is not None, "valid DXF document required"
        if name:
            self.dxf.arrow_head_handle = ARROWS.arrow_handle(
                self.doc.blocks, name
            )
        else:
            # empty string is the default "closed filled" arrow,
            # no handle needed
            del self.dxf.arrow_head_handle

    def export_entity(self, tagwriter: AbstractTagWriter) -> None:
        super().export_entity(tagwriter)
        tagwriter.write_tag2(const.SUBCLASS_MARKER, acdb_mleader_style.name)
        self.dxf.export_dxf_attribs(
            tagwriter, acdb_mleader_style.attribs.keys()
        )

    def __referenced_blocks__(self) -> Iterable[str]:
        """Support for "ReferencedBlocks" protocol."""
        for name in ("block_record_handle", "arrow_head_handle"):
            handle = self.dxf.get(name, None)
            if handle is not None:
                yield handle

    def audit(self, auditor: Auditor) -> None:
        from ezdxf.audit import AuditError

        if not self.is_alive:
            return
        super().audit(auditor)
        entitydb = auditor.entitydb
        dxf = self.dxf
        name = str(self)
        handle = dxf.get("text_style_handle", None)
        if handle is not None and handle not in entitydb:
            standard = auditor.doc.styles.get("Standard")
            if standard is not None:
                self.dxf.text_style_handle = standard.dxf.handle
                auditor.fixed_error(
                    AuditError.UNDEFINED_TEXT_STYLE,
                    f"{name}: text_style_handle={handle} is not valid, replaced by "
                    f"'Standard' text style",
                    self,
                )
            else:
                logger.warning("required text style 'Standard' does not exist")
                self.dxf.discard("text_style_handle")
                auditor.fixed_error(
                    AuditError.UNDEFINED_TEXT_STYLE,
                    f"{name}: removed invalid text_style_handle={handle}",
                    self,
                )
        for attrib in ("arrow_head_handle", "block_record_handle"):
            handle = dxf.get(attrib)
            if handle is None or handle == "0":  # legit "undefined values"
                continue
            if handle not in entitydb:
                dxf.discard(attrib)
                auditor.fixed_error(
                    AuditError.UNDEFINED_BLOCK,
                    f"{name}: removed invalid {attrib}={handle}",
                    self,
                )


class MLeaderStyleCollection(ObjectCollection[MLeaderStyle]):
    def __init__(self, doc: Drawing):
        super().__init__(
            doc, dict_name="ACAD_MLEADERSTYLE", object_type="MLEADERSTYLE"
        )
        self.create_required_entries()

    def create_required_entries(self) -> None:
        if "Standard" not in self:
            mleader_style = self.new("Standard")
            # set standard text style
            text_style = self.doc.styles.get("Standard")
            mleader_style.dxf.text_style_handle = text_style.dxf.handle
