# Copyright (c) 2019-2022, Manfred Moitzi
# License: MIT License
from __future__ import annotations
from typing import TYPE_CHECKING, Optional
import logging
from ezdxf.lldxf import validator, const
from ezdxf.lldxf.attributes import (
    DXFAttr,
    DXFAttributes,
    DefSubclass,
    RETURN_DEFAULT,
    group_code_mapping,
)
from ezdxf.lldxf.const import DXF12, SUBCLASS_MARKER
from ezdxf.entities.dxfentity import base_class, SubclassProcessor, DXFEntity
from ezdxf.entities.layer import acdb_symbol_table_record
from .factory import register_entity

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

__all__ = ["Textstyle", "get_textstyle"]
logger = logging.getLogger("ezdxf")

acdb_style = DefSubclass(
    "AcDbTextStyleTableRecord",
    {
        "name": DXFAttr(
            2,
            default="Standard",
            validator=validator.is_valid_table_name,
        ),
        # Flags: Standard flag values (bit-coded values):
        # 1 = If set, this entry describes a shape
        # 4 = Vertical text
        # 16 = If set, table entry is externally dependent on a xref
        # 32 = If both this bit and bit 16 are set, the externally dependent xref ...
        # 64 = If set, the table entry was referenced by at least one entity in ...
        # Vertical text works only for SHX fonts in AutoCAD and BricsCAD
        "flags": DXFAttr(70, default=0),
        # Fixed height, 0 if not fixed
        "height": DXFAttr(
            40,
            default=0,
            validator=validator.is_greater_or_equal_zero,
            fixer=RETURN_DEFAULT,
        ),
        # Width factor:  a.k.a. "Stretch"
        "width": DXFAttr(
            41,
            default=1,
            validator=validator.is_greater_zero,
            fixer=RETURN_DEFAULT,
        ),
        # Oblique angle in degree, 0 = vertical
        "oblique": DXFAttr(50, default=0),
        # Generation flags:
        # 2 = backward
        # 4 = mirrored in Y
        "generation_flags": DXFAttr(71, default=0),
        # Last height used:
        "last_height": DXFAttr(42, default=2.5),
        # Primary font file name:
        # ATTENTION: The font file name can be an empty string and the font family
        # may be stored in XDATA! See also posts at the (unrelated) issue #380.
        "font": DXFAttr(3, default=const.DEFAULT_TEXT_FONT),
        # Big font name, blank if none
        "bigfont": DXFAttr(4, default=""),
    },
)
acdb_style_group_codes = group_code_mapping(acdb_style)


# XDATA: This is not a reliable source for font data!
# 1001 <ctrl> ACAD
# 1000 <str> Arial  ; font-family sometimes an empty string!
# 1071 <int> 34  ; flags
# ----
# "Arial" "normal" flags = 34               = 0b00:00000000:00000000:00100010
# "Arial" "italic" flags = 16777250         = 0b01:00000000:00000000:00100010
# "Arial" "bold" flags = 33554466           = 0b10:00000000:00000000:00100010
# "Arial" "bold+italic" flags = 50331682    = 0b11:00000000:00000000:00100010


@register_entity
class Textstyle(DXFEntity):
    """DXF STYLE entity"""

    DXFTYPE = "STYLE"
    DXFATTRIBS = DXFAttributes(base_class, acdb_symbol_table_record, acdb_style)
    ITALIC = 0b01000000000000000000000000
    BOLD = 0b10000000000000000000000000

    def load_dxf_attribs(
        self, processor: Optional[SubclassProcessor] = None
    ) -> DXFNamespace:
        dxf = super().load_dxf_attribs(processor)
        if processor:
            processor.simple_dxfattribs_loader(dxf, acdb_style_group_codes)  # type: ignore
        return dxf

    def export_entity(self, tagwriter: AbstractTagWriter) -> None:
        super().export_entity(tagwriter)
        if tagwriter.dxfversion > DXF12:
            tagwriter.write_tag2(SUBCLASS_MARKER, acdb_symbol_table_record.name)
            tagwriter.write_tag2(SUBCLASS_MARKER, acdb_style.name)

        self.dxf.export_dxf_attribs(
            tagwriter,
            [
                "name",
                "flags",
                "height",
                "width",
                "oblique",
                "generation_flags",
                "last_height",
                "font",
                "bigfont",
            ],
        )

    @property
    def has_extended_font_data(self) -> bool:
        """Returns ``True`` if extended font data is present."""
        return self.has_xdata("ACAD")

    def get_extended_font_data(self) -> tuple[str, bool, bool]:
        """Returns extended font data as tuple (font-family, italic-flag,
        bold-flag).

        The extended font data is optional and not reliable! Returns
        ("", ``False``, ``False``) if extended font data is not present.

        """
        family = ""
        italic = False
        bold = False
        try:
            xdata = self.get_xdata("ACAD")
        except const.DXFValueError:
            pass
        else:
            if len(xdata) > 1:
                group_code, value = xdata[0]
                if group_code == 1000:
                    family = value
                group_code, value = xdata[1]
                if group_code == 1071:
                    italic = bool(self.ITALIC & value)
                    bold = bool(self.BOLD & value)
        return family, italic, bold

    def set_extended_font_data(
        self, family: str = "", *, italic=False, bold=False
    ) -> None:
        """Set extended font data, the font-family name `family` is not
        validated by `ezdxf`. Overwrites existing data.
        """
        if self.has_xdata("ACAD"):
            self.discard_xdata("ACAD")

        flags = 34  # unknown default flags
        if italic:
            flags += self.ITALIC
        if bold:
            flags += self.BOLD
        self.set_xdata("ACAD", [(1000, family), (1071, flags)])

    def discard_extended_font_data(self):
        """Discard extended font data."""
        self.discard_xdata("ACAD")

    @property
    def is_backward(self) -> bool:
        """Get/set text generation flag BACKWARDS, for mirrored text along the
        x-axis.
        """
        return self.get_flag_state(const.BACKWARD, "generation_flags")

    @is_backward.setter
    def is_backward(self, state) -> None:
        self.set_flag_state(const.BACKWARD, state, "generation_flags")

    @property
    def is_upside_down(self) -> bool:
        """Get/set text generation flag UPSIDE_DOWN, for mirrored text along
        the y-axis.

        """
        return self.get_flag_state(const.UPSIDE_DOWN, "generation_flags")

    @is_upside_down.setter
    def is_upside_down(self, state) -> None:
        self.set_flag_state(const.UPSIDE_DOWN, state, "generation_flags")

    @property
    def is_vertical_stacked(self) -> bool:
        """Get/set style flag VERTICAL_STACKED, for vertical stacked text."""
        return self.get_flag_state(const.VERTICAL_STACKED, "flags")

    @is_vertical_stacked.setter
    def is_vertical_stacked(self, state) -> None:
        self.set_flag_state(const.VERTICAL_STACKED, state, "flags")

    @property
    def is_shape_file(self) -> bool:
        """``True`` if entry describes a shape."""
        return self.dxf.name == "" and bool(self.dxf.flags & 1)

    def make_font(
        self,
        cap_height: Optional[float] = None,
        width_factor: Optional[float] = None,
    ) -> fonts.AbstractFont:
        """Returns a font abstraction :class:`~ezdxf.tools.fonts.AbstractFont`
        for this text style. Returns a font for a cap height of 1, if the
        text style has auto height (:attr:`Textstyle.dxf.height` is 0) and
        the given `cap_height` is ``None`` or 0.
        Uses the :attr:`Textstyle.dxf.width` attribute if the given `width_factor`
        is ``None`` or 0, the default value is 1.
        The attribute :attr:`Textstyle.dxf.big_font` is ignored.
        """
        from ezdxf.fonts import fonts

        ttf = ""
        if self.has_extended_font_data:
            family, italic, bold = self.get_extended_font_data()
            if family:
                text_style = "Italic" if italic else "Regular"
                text_weight = 700 if bold else 400
                font_face = fonts.FontFace(
                    family=family, style=text_style, weight=text_weight
                )
                ttf = fonts.find_font_file_name(font_face)
        else:
            ttf = self.dxf.get("font", const.DEFAULT_TTF)
        if ttf == "":
            ttf = const.DEFAULT_TTF
        if cap_height is None or cap_height == 0.0:
            cap_height = self.dxf.height
        if cap_height == 0.0:
            cap_height = 1.0
        if width_factor is None or width_factor == 0.0:
            width_factor = self.dxf.width
        return fonts.make_font(ttf, cap_height, width_factor)  # type: ignore


def get_textstyle(entity: DXFEntity) -> Textstyle:
    """Returns the :class:`Textstyle` for the given DXF entity."""
    if entity.doc is None:
        return Textstyle()
    try:
        name = entity.dxf.style
    except const.DXFAttributeError:
        name = "Standard"

    try:
        style = entity.doc.styles.get(name)
    except const.DXFTableEntryError:
        return Textstyle()
    return style
