# Created: 16.03.2011
# Copyright (c) 2011-2018, Manfred Moitzi
# License: MIT License
from typing import TYPE_CHECKING, Sequence, Tuple, Iterable
from ezdxf.dxfentity import DXFEntity
from ezdxf.lldxf.tags import DXFTag
from ezdxf.lldxf import const
from ezdxf.lldxf.extendedtags import ExtendedTags
from ezdxf.lldxf.attributes import DXFAttr, DXFAttributes, DefSubclass, XType
from ezdxf.lldxf.validator import is_valid_layer_name
from ezdxf.lldxf.const import DXFInvalidLayerName, DXFValueError, DXFKeyError, DXFVersionError, DIMJUST, DIMTAD
from ezdxf.render.arrows import ARROWS
from ezdxf.math.ucs import UCS as UserCoordinateSystem
import logging

logger = logging.getLogger('ezdxf')

if TYPE_CHECKING:
    from ezdxf.eztypes import DXFFactoryType

_LAYERTEMPLATE = """0
LAYER
5
0
2
LAYERNAME
70
0
62
7
6
CONTINUOUS
"""


# noinspection PyAugmentAssignment,PyUnresolvedReferences
class Layer(DXFEntity):
    __slots__ = ()
    TEMPLATE = ExtendedTags.from_text(_LAYERTEMPLATE)
    DXFATTRIBS = DXFAttributes(DefSubclass(None, {
        'handle': DXFAttr(5),
        'name': DXFAttr(2),
        'flags': DXFAttr(70),
        'color': DXFAttr(62),  # dxf color index, if < 0 layer is off
        'linetype': DXFAttr(6),
    }))
    FROZEN = 0b00000001
    THAW = 0b11111110
    LOCK = 0b00000100
    UNLOCK = 0b11111011

    def post_new_hook(self) -> None:
        if not is_valid_layer_name(self.dxf.name):
            raise DXFInvalidLayerName("Invalid layer name '{}'".format(self.dxf.name))

    def is_frozen(self) -> bool:
        return self.dxf.flags & Layer.FROZEN > 0

    def freeze(self) -> None:
        self.dxf.flags = self.dxf.flags | Layer.FROZEN

    def thaw(self) -> None:
        self.dxf.flags = self.dxf.flags & Layer.THAW

    def is_locked(self) -> bool:
        return self.dxf.flags & Layer.LOCK > 0

    def lock(self) -> None:
        self.dxf.flags = self.dxf.flags | Layer.LOCK

    def unlock(self) -> None:
        self.dxf.flags = self.dxf.flags & Layer.UNLOCK

    def is_off(self) -> bool:
        return self.dxf.color < 0

    def is_on(self) -> bool:
        return not self.is_off()

    def on(self) -> None:
        self.dxf.color = abs(self.dxf.color)

    def off(self) -> None:
        self.dxf.color = -abs(self.dxf.color)

    def get_color(self) -> int:
        return abs(self.dxf.color)

    def set_color(self, color: int) -> None:
        color = abs(color) if self.is_on() else -abs(color)
        self.dxf.color = color


_STYLETEMPLATE = """0
STYLE
5
0
2
STYLENAME
70
0
40
0.0
41
1.0
50
0.0
71
0
42
1.0
3
OpenSans-Regular.ttf
4

"""


class Style(DXFEntity):
    __slots__ = ()
    TEMPLATE = ExtendedTags.from_text(_STYLETEMPLATE)
    DXFATTRIBS = DXFAttributes(DefSubclass(None, {
        'handle': DXFAttr(5),
        'name': DXFAttr(2),
        'flags': DXFAttr(70),
        'height': DXFAttr(40),  # fixed height, 0 if not fixed
        'width': DXFAttr(41),  # width factor
        'oblique': DXFAttr(50),  # oblique angle in degree, 0 = vertical
        'generation_flags': DXFAttr(71),  # 2 = backward, 4 = mirrored in Y
        'last_height': DXFAttr(42),  # last height used
        'font': DXFAttr(3),  # primary font file name
        'bigfont': DXFAttr(4),  # big font name, blank if none
    }))


_LTYPETEMPLATE = """0
LTYPE
5
0
2
LTYPENAME
70
0
3
LTYPEDESCRIPTION
72
65
"""


class Linetype(DXFEntity):
    __slots__ = ()
    TEMPLATE = ExtendedTags.from_text(_LTYPETEMPLATE)
    DXFATTRIBS = DXFAttributes(DefSubclass(None, {
        'handle': DXFAttr(5),
        'name': DXFAttr(2),
        'description': DXFAttr(3),
        'length': DXFAttr(40),
        'items': DXFAttr(73),
    }))

    @classmethod
    def new(cls, handle: str, dxfattribs: dict = None, dxffactory: 'DXFFactoryType' = None) -> DXFEntity:
        if dxfattribs is not None:
            pattern = dxfattribs.pop('pattern', [0.0])
            length = dxfattribs.pop('length', 0.)
        else:
            pattern = [0.0]
            length = 0.
        entity = super(Linetype, cls).new(handle, dxfattribs, dxffactory)
        entity._setup_pattern(pattern, length)
        return entity

    def _setup_pattern(self, pattern: Sequence[float], length: float) -> None:
        # length parameter is required for complex line types
        # pattern: [2.0, 1.25, -0.25, 0.25, -0.25] - 1. element is total pattern length
        # pattern elements: >0 line, <0 gap, =0 point
        self.tags.noclass.append(DXFTag(73, len(pattern) - 1))
        self.tags.noclass.append(DXFTag(40, float(pattern[0])))
        self.tags.noclass.extend((DXFTag(49, float(p)) for p in pattern[1:]))


_VPORTTEMPLATE = """0
VPORT
5
0
2
VPORTNAME
70
0
10
0.0
20
0.0
11
1.0
21
1.0
12
70.0
22
50.0
13
0.0
23
0.0
14
0.5
24
0.5
15
0.5
25
0.5
16
0.0
26
0.0
36
1.0
17
0.0
27
0.0
37
0.0
40
70.
41
1.34
42
50.0
43
0.0
44
0.0
50
0.0
51
0.0
71
0
72
1000
73
1
74
3
75
0
76
0
77
0
78
0
"""


class VPort(DXFEntity):
    __slots__ = ()
    TEMPLATE = ExtendedTags.from_text(_VPORTTEMPLATE)
    DXFATTRIBS = DXFAttributes(DefSubclass(None, {
        'handle': DXFAttr(5),
        'name': DXFAttr(2),
        'flags': DXFAttr(70),
        'lower_left': DXFAttr(10, xtype=XType.point2d),
        'upper_right': DXFAttr(11, xtype=XType.point2d),
        'center_point': DXFAttr(12, xtype=XType.point2d),
        'snap_base': DXFAttr(13, xtype=XType.point2d),
        'snap_spacing': DXFAttr(14, xtype=XType.point2d),
        'grid_spacing': DXFAttr(15, xtype=XType.point2d),
        'direction_point': DXFAttr(16, xtype=XType.point3d),
        'target_point': DXFAttr(17, xtype=XType.point3d),
        'height': DXFAttr(40),
        'aspect_ratio': DXFAttr(41),
        'lens_length': DXFAttr(42),
        'front_clipping': DXFAttr(43),
        'back_clipping': DXFAttr(44),
        'snap_rotation': DXFAttr(50),
        'view_twist': DXFAttr(51),
        'status': DXFAttr(68),
        # group code 69: 'id' is never saved in DXF, and DXF R13 and later has no group code 69
        'view_mode': DXFAttr(71),
        'circle_zoom': DXFAttr(72),
        'fast_zoom': DXFAttr(73),
        'ucs_icon': DXFAttr(74),
        'snap_on': DXFAttr(75),
        'grid_on': DXFAttr(76),
        'snap_style': DXFAttr(77),
        'snap_isopair': DXFAttr(78),
    }))


_UCSTEMPLATE = """0
UCS
5
0
2
UCSNAME
70
0
10
0.0
20
0.0
30
0.0
11
1.0
21
0.0
31
0.0
12
0.0
22
1.0
32
0.0
"""


class UCS(DXFEntity):
    __slots__ = ()
    TEMPLATE = ExtendedTags.from_text(_UCSTEMPLATE)
    DXFATTRIBS = DXFAttributes(DefSubclass(None, {
        'handle': DXFAttr(5),
        'name': DXFAttr(2),
        'flags': DXFAttr(70),
        'origin': DXFAttr(10, xtype=XType.point3d),
        'xaxis': DXFAttr(11, xtype=XType.point3d),
        'yaxis': DXFAttr(12, xtype=XType.point3d),
    }))

    def ucs(self) -> UserCoordinateSystem:
        return UserCoordinateSystem(
            origin=self.get_dxf_attrib('origin', default=(0, 0, 0)),
            ux=self.get_dxf_attrib('xaxis', default=(1, 0, 0)),
            uy=self.get_dxf_attrib('yaxis', default=(0, 1, 0)),
        )


_APPIDTEMPLATE = """0
APPID
5
0
2
APPNAME
70
0
"""


class AppID(DXFEntity):
    __slots__ = ()
    TEMPLATE = ExtendedTags.from_text(_APPIDTEMPLATE)
    DXFATTRIBS = DXFAttributes(DefSubclass(None, {
        'handle': DXFAttr(5),
        'name': DXFAttr(2),
        'flags': DXFAttr(70),
    }))


_VIEWTEMPLATE = """0
VIEW
5
0
2
VIEWNAME
70
0
10
0.0
20
0.0
11
1.0
21
1.0
31
1.0
12
0.0
22
0.0
32
0.0
40
70.
41
1.0
42
50.0
43
0.0
44
0.0
50
0.0
71
0
"""


class View(DXFEntity):
    __slots__ = ()
    TEMPLATE = ExtendedTags.from_text(_VIEWTEMPLATE)
    DXFATTRIBS = DXFAttributes(DefSubclass(None, {
        'handle': DXFAttr(5),
        'name': DXFAttr(2),
        'flags': DXFAttr(70),
        'height': DXFAttr(40),
        'width': DXFAttr(41),
        'center_point': DXFAttr(10, xtype=XType.point2d),
        'direction_point': DXFAttr(11, xtype=XType.point3d),
        'target_point': DXFAttr(12, xtype=XType.point3d),
        'lens_length': DXFAttr(42),
        'front_clipping': DXFAttr(43),
        'back_clipping': DXFAttr(44),
        'view_twist': DXFAttr(50),
        'view_mode': DXFAttr(71),
    }))


_DIMSTYLETEMPLATE = """0
DIMSTYLE
105
0
2
DIMSTYLENAME
70
0
3

4

5

6

7

40
1.0
41
1.0
42
0.0
43
3.75
44
0.0
45
0.0
46
0.0
47
0.0
48
0.0
140
2.5
141
2.5
142
0.0
143
25.4
144
1.0
145
0.0
146
1.0
147
0.625
71
0
72
0
73
1
74
1
75
0
76
0
77
0
78
0
170
0
171
2
172
0
173
0
174
0
175
0
176
0
177
0
178
0
"""


def dim_filter(name: str) -> bool:
    return name.startswith('dim')


class DimStyle(DXFEntity):
    __slots__ = ()
    TEMPLATE = ExtendedTags.from_text(_DIMSTYLETEMPLATE)
    DXFATTRIBS = DXFAttributes(DefSubclass(None, {
        'handle': DXFAttr(105),
        'name': DXFAttr(2),
        'flags': DXFAttr(70),
        'dimpost': DXFAttr(3),
        'dimapost': DXFAttr(4),
        'dimblk': DXFAttr(5),
        'dimblk1': DXFAttr(6),
        'dimblk2': DXFAttr(7),
        'dimscale': DXFAttr(40),
        'dimasz': DXFAttr(41),
        'dimexo': DXFAttr(42),
        'dimdli': DXFAttr(43),
        'dimexe': DXFAttr(44),
        'dimrnd': DXFAttr(45),
        'dimdle': DXFAttr(46),
        'dimtp': DXFAttr(47),
        'dimtm': DXFAttr(48),
        'dimtxt': DXFAttr(140),
        'dimcen': DXFAttr(141),
        'dimtsz': DXFAttr(142),
        'dimaltf': DXFAttr(143, default=1.),
        'dimlfac': DXFAttr(144),
        'dimtvp': DXFAttr(145),
        'dimtfac': DXFAttr(146),
        'dimgap': DXFAttr(147),
        'dimtol': DXFAttr(71),
        'dimlim': DXFAttr(72),
        'dimtih': DXFAttr(73),
        'dimtoh': DXFAttr(74),
        'dimse1': DXFAttr(75),
        'dimse2': DXFAttr(76),
        'dimtad': DXFAttr(77),  # 0 center, 1 above, 4 below dimline
        'dimzin': DXFAttr(78),
        'dimalt': DXFAttr(170),
        'dimaltd': DXFAttr(171),
        'dimtofl': DXFAttr(172),
        'dimsah': DXFAttr(173),
        'dimtix': DXFAttr(174),
        'dimsoxd': DXFAttr(175),
        'dimclrd': DXFAttr(176),
        'dimclre': DXFAttr(177),
        'dimclrt': DXFAttr(178),
    }))
    CODE_TO_DXF_ATTRIB = dict(DXFATTRIBS.build_group_code_items(dim_filter))

    def dim_attribs(self) -> Iterable[Tuple[str, DXFAttr]]:
        return ((name, attrib) for name, attrib in self.DXFATTRIBS.items() if name.startswith('dim'))

    def print_dim_attribs(self) -> None:
        for name, attrib in self.dim_attribs():
            code = attrib.code
            value = self.get_dxf_attrib(name, None)
            if value is not None:
                print("{name} ({code}) = {value}".format(name=name, value=value, code=code))

    def copy_to_header(self, dwg):
        attribs = self.dxfattribs()
        header = dwg.header
        header['$DIMSTYLE'] = self.dxf.name
        for name, value in attribs.items():
            if name.startswith('dim'):
                header_var = '$' + name.upper()
                try:
                    header[header_var] = value
                except DXFKeyError:
                    logger.debug('Unsupported header variable: {}.'.format(header_var))

    def set_arrows(self, blk: str = '', blk1: str = '', blk2: str = '') -> None:
        """
        Set arrows by block names or AutoCAD standard arrow names, set dimtsz = 0 which disables tick.

        Args:
            blk: block/arrow name for both arrows, if dimsah == 0
            blk1: block/arrow name for first arrow, if dimsah == 1
            blk2: block/arrow name for second arrow, if dimsah == 1

        """
        self.set_dxf_attrib('dimblk', blk)
        self.set_dxf_attrib('dimblk1', blk1)
        self.set_dxf_attrib('dimblk2', blk2)
        self.set_dxf_attrib('dimtsz', 0)  # use blocks

        # only existing BLOCK definitions allowed
        if self.drawing:
            blocks = self.drawing.blocks
            for b in (blk, blk1, blk2):
                if ARROWS.is_acad_arrow(b):  # not real blocks
                    continue
                if b and b not in blocks:
                    raise DXFValueError('BLOCK "{}" does not exist.'.format(blk))

    def set_tick(self, size: float = 1) -> None:
        """
        Use oblique stroke as tick, disables arrows.

        Args:
            size: arrow size in daring units

        """
        self.set_dxf_attrib('dimtsz', size)

    def set_text_align(self, halign: str = None, valign: str = None, vshift: float = None) -> None:
        """
        Set measurement text alignment, `halign` defines the horizontal alignment (requires DXFR2000+),
        `valign` defines the vertical  alignment, `above1` and `above2` means above extension line 1 or 2 and aligned
        with extension line.

        Args:
            halign: `left`, `right`, `center`, `above1`, `above2`, requires DXF R2000+
            valign: `above`, `center`, `below`
            vshift: vertical text shift, if `valign` is `center`; >0 shift upward, <0 shift downwards

        """
        if valign:
            valign = valign.lower()
            self.set_dxf_attrib('dimtad', DIMTAD[valign])
            if valign == 'center' and vshift is not None:
                self.set_dxf_attrib('dimtvp', vshift)
        try:
            if halign:
                self.set_dxf_attrib('dimjust', DIMJUST[halign.lower()])
        except const.DXFAttributeError:
            raise DXFVersionError('DIMJUST require DXF R2000+')

    def set_text_format(self, prefix: str = '', postfix: str = '', rnd: float = None, dec: int = None, sep: str = None,
                        leading_zeros: bool = True, trailing_zeros: bool = True):
        """
        Set dimension text format, like prefix and postfix string, rounding rule and number of decimal places.

        Args:
            prefix: Dimension text prefix text as string
            postfix: Dimension text postfix text as string
            rnd: Rounds all dimensioning distances to the specified value, for instance, if DIMRND is set to 0.25, all
                 distances round to the nearest 0.25 unit. If you set DIMRND to 1.0, all distances round to the nearest
                 integer.
            dec: Sets the number of decimal places displayed for the primary units of a dimension. requires DXF R2000+
            sep: "." or "," as decimal separator requires DXF R2000+
            leading_zeros: suppress leading zeros for decimal dimensions if False
            trailing_zeros: suppress trailing zeros for decimal dimensions if False

        """
        if prefix or postfix:
            self.dxf.dimpost = prefix + '<>' + postfix
        if rnd is not None:
            self.dxf.dimrnd = rnd

        # works only with decimal dimensions not inch and feet, US user set dimzin directly
        if leading_zeros is not None or trailing_zeros is not None:
            dimzin = 0
            if leading_zeros is False:
                dimzin = const.DIMZIN_SUPPRESSES_LEADING_ZEROS
            if trailing_zeros is False:
                dimzin += const.DIMZIN_SUPPRESSES_TRAILING_ZEROS
            self.dxf.dimzin = dimzin
        try:
            if dec is not None:
                self.dxf.dimdec = dec
            if sep is not None:
                self.dxf.dimdsep = ord(sep)
        except const.DXFAttributeError:
            raise DXFVersionError('DIMDSEP and DIMDEC require DXF R2000+')

    def set_dimline_format(self, color: int = None, linetype: str = None, lineweight: int = None,
                           extension: float = None, disable1: bool = None, disable2: bool = None):
        """
        Set dimension line properties

        Args:
            color: color index
            linetype: linetype as string, requires DXF R2007+
            lineweight: line weight as int, 13 = 0.13mm, 200 = 2.00mm, requires DXF R2000+
            extension: extension length
            disable1: True to suppress first part of dimension line, requires DXF R2000+
            disable2: True to suppress second part of dimension line, requires DXF R2000+

        """
        if color is not None:
            self.dxf.dimclrd = color
        if extension is not None:
            self.dxf.dimdle = extension
        try:
            if lineweight is not None:
                self.dxf.dimlwd = lineweight
            if disable1 is not None:
                self.dxf.dimsd1 = disable1
            if disable2 is not None:
                self.dxf.dimsd2 = disable2
        except const.DXFAttributeError:
            raise DXFVersionError('DIMLWD, DIMSD1 and DIMSD2 requires DXF R2000+')
        try:
            if linetype is not None:
                self.dxf.dimltype = linetype
        except const.DXFAttributeError:
            raise DXFVersionError('DIMLTYPE requires DXF R2007+')

    def set_extline_format(self, color: int = None, lineweight: int = None, extension: float = None,
                           offset: float = None, fixed_length: float = None):
        """
        Set common extension line attributes.

        Args:
            color: color index
            lineweight: line weight as int, 13 = 0.13mm, 200 = 2.00mm
            extension: extension length above dimension line
            offset: offset from measurement point
            fixed_length: set fixed length extension line, length below the dimension line

        """
        if color is not None:
            self.dxf.dimclre = color
        if extension is not None:
            self.dxf.dimexe = extension
        if offset is not None:
            self.dxf.dimexo = offset
        try:
            if lineweight is not None:
                self.dxf.dimlwe = lineweight
        except const.DXFAttributeError:
            raise DXFVersionError('DIMLWE requires DXF R2000+')
        try:
            if fixed_length is not None:
                self.dxf.dimfxlon = 1
                self.dxf.dimfxl = fixed_length
        except const.DXFAttributeError:
            raise DXFVersionError('DIMFXL requires DXF R2007+')

    def set_extline1(self, linetype: str = None, disable=False):
        """
        Set extension line 1 attributes.

        Args:
            linetype: linetype for extension line 1, requires DXF R2007+
            disable: disable extension line 1 if True

        """
        if disable:
            self.dxf.dimse1 = 1
        try:
            if linetype is not None:
                self.dxf.dimltex1 = linetype
        except const.DXFAttributeError:
            raise DXFVersionError('DIMLTEX1 requires DXF R2007+')

    def set_extline2(self, linetype: str = None, disable=False):
        """
        Set extension line 2 attributes.

        Args:
            linetype: linetype for extension line 2, requires DXF R2007+
            disable: disable extension line 2 if True

        """
        if disable:
            self.dxf.dimse2 = 1
        try:
            if linetype is not None:
                self.dxf.dimltex2 = linetype
        except const.DXFAttributeError:
            raise DXFVersionError('DIMLTEX2 requires DXF R2007+')

    def set_tolerance(self, upper: float, lower: float = None, hfactor: float = 1.0,
                      align: str = None, dec: int = None, leading_zeros: bool = None,
                      trailing_zeros: bool = None) -> None:
        """
        Set tolerance text format, upper and lower value, text height factor, number of decimal places or leading and
        trailing zero suppression.

        Args:
            upper: upper tolerance value
            lower: lower tolerance value, if None same as upper
            hfactor: tolerance text height factor in relation to the dimension text height
            align: tolerance text alignment "TOP", "MIDDLE", "BOTTOM", required DXF R2000+
            dec: Sets the number of decimal places displayed, required DXF R2000+
            leading_zeros: suppress leading zeros for decimal dimensions if False, required DXF R2000+
            trailing_zeros: suppress trailing zeros for decimal dimensions if False, required DXF R2000+

        """
        # exclusive tolerances
        self.dxf.dimtol = 1
        self.dxf.dimlim = 0
        self.dxf.dimtp = float(upper)
        if lower is not None:
            self.dxf.dimtm = float(lower)
        else:
            self.dxf.dimtm = float(upper)
        if hfactor is not None:
            self.dxf.dimtfac = float(hfactor)

        if self.dxfversion > 'AC1009':
            # works only with decimal dimensions not inch and feet, US user set dimzin directly
            if leading_zeros is not None or trailing_zeros is not None:
                dimtzin = 0
                if leading_zeros is False:
                    dimtzin = const.DIMZIN_SUPPRESSES_LEADING_ZEROS
                if trailing_zeros is False:
                    dimtzin += const.DIMZIN_SUPPRESSES_TRAILING_ZEROS
                self.dxf.dimtzin = dimtzin

            if align is not None:
                self.dxf.dimtolj = const.MTEXT_INLINE_ALIGN[align.upper()]
            if dec is not None:
                self.dxf.dimtdec = int(dec)

    def set_limits(self, upper: float, lower: float, hfactor: float = 1.0,
                   dec: int = None, leading_zeros: bool = None, trailing_zeros: bool = None) -> None:
        """
        Set limits text format, upper and lower limit values, text height factor, number of decimal places or
        leading and trailing zero suppression.

        Args:
            upper: upper limit value added to measurement value
            lower: lower lower value subtracted from measurement value
            hfactor: limit text height factor in relation to the dimension text height
            dec: Sets the number of decimal places displayed, required DXF R2000+
            leading_zeros: suppress leading zeros for decimal dimensions if False, required DXF R2000+
            trailing_zeros: suppress trailing zeros for decimal dimensions if False, required DXF R2000+

        """
        # exclusive limits
        self.dxf.dimlim = 1
        self.dxf.dimtol = 0
        self.dxf.dimtp = float(upper)
        self.dxf.dimtm = float(lower)
        self.dxf.dimtfac = float(hfactor)

        if self.dxfversion > 'AC1009':
            # works only with decimal dimensions not inch and feet, US user set dimzin directly
            if leading_zeros is not None or trailing_zeros is not None:
                dimtzin = 0
                if leading_zeros is False:
                    dimtzin = const.DIMZIN_SUPPRESSES_LEADING_ZEROS
                if trailing_zeros is False:
                    dimtzin += const.DIMZIN_SUPPRESSES_TRAILING_ZEROS
                self.dxf.dimtzin = dimtzin
            self.dxf.dimtolj = 0  # set bottom as default
            if dec is not None:
                self.dxf.dimtdec = int(dec)
