from typing import Any, TYPE_CHECKING, Tuple
from ezdxf.lldxf import const
from ezdxf.lldxf.const import DXFAttributeError, DIMJUST, DIMTAD
from ezdxf.render.arrows import ARROWS
from ezdxf.math import Vector
import logging

logger = logging.getLogger('ezdxf')

if TYPE_CHECKING:
    from ezdxf.eztypes import Dimension, UCS, Drawing, DimStyle, Vertex, BaseDimensionRenderer


class DimStyleOverride:
    def __init__(self, dimension: 'Dimension', override: dict = None):
        self.dimension = dimension  # type: Dimension
        dim_style_name = dimension.get_dxf_attrib('dimstyle', 'STANDARD')
        self.dimstyle = self.drawing.dimstyles.get(dim_style_name)  # type: DimStyle
        self.dimstyle_attribs = self.get_dstyle_dict()  # type: dict

        # special ezdxf attributes beyond the DXF reference, therefore not stored in the DSTYLE data.
        # This are only rendering effects or data transfer objects
        # user_location: Vector - user location override if not None
        # relative_user_location: bool - user location override relative to dimline center if True
        # text_shift_h: float - shift text in text direction, relative to standard text location
        # text_shift_v: float - shift text perpendicular to text direction, relative to standard text location
        self.update(override or {})

    @property
    def drawing(self) -> 'Drawing':
        return self.dimension.drawing

    @property
    def dxfversion(self) -> str:
        return self.dimension.drawing.dxfversion

    def get_dstyle_dict(self) -> dict:
        return self.dimension.get_acad_dstyle(self.dimstyle)

    def get(self, attribute: str, default: Any = None) -> Any:
        if attribute in self.dimstyle_attribs:
            result = self.dimstyle_attribs[attribute]
        else:
            # Return default value for attributes not supported by DXF R12.
            # This is a hack to use the same algorithm to render DXF R2000 and DXF R12 DIMENSION entities.
            # But the DXF R2000 attributes are not stored in the DXF R12 file!!!
            # Does not catch invalid attributes names! Look into debug log for ignored DIMSTYLE attributes.
            try:
                result = self.dimstyle.get_dxf_attrib(attribute, default)
            except DXFAttributeError:
                # return default value
                result = default
        return result

    def pop(self, attribute: str, default: Any = None) -> Any:
        value = self.get(attribute, default)
        # delete just from override dict
        del self[attribute]
        return value

    def update(self, attribs: dict) -> None:
        self.dimstyle_attribs.update(attribs)

    def __getitem__(self, item: str) -> Any:
        return self.get(item)

    def __setitem__(self, key: str, value: Any) -> None:
        self.dimstyle_attribs[key] = value

    def __delitem__(self, key: str) -> None:
        try:
            del self.dimstyle_attribs[key]
        except KeyError:  # silent discard
            pass

    def commit(self) -> None:
        """
        Write overwritten DIMSTYLE attributes into XDATA section of the DIMENSION entity.

        """

        def set_arrow_handle(attrib_name, block_name):
            attrib_name += '_handle'
            if block_name in ARROWS:  # create all arrows on demand
                block_name = ARROWS.create_block(blocks, block_name)
            if block_name == '_CLOSEDFILLED':  # special arrow
                handle = '0'  # set special #0 handle for closed filled arrow
            else:
                block = blocks.get(block_name)
                handle = block.block_record_handle
            self.dimstyle_attribs[attrib_name] = handle

        def set_linetype_handle(attrib_name, linetype_name):
            ltype = self.drawing.linetypes.get(linetype_name)
            self.dimstyle_attribs[attrib_name + '_handle'] = ltype.dxf.handle

        if self.drawing.dxfversion > 'AC1009':
            # transform block names into block record handles
            blocks = self.drawing.blocks
            for attrib_name in ('dimblk', 'dimblk1', 'dimblk2', 'dimldrblk'):
                try:
                    block_name = self.dimstyle_attribs.pop(attrib_name)
                except KeyError:
                    pass
                else:
                    set_arrow_handle(attrib_name, block_name)

        if self.drawing.dxfversion >= 'AC1021':
            # transform linetype names into LTYPE entry handles
            for attrib_name in ('dimltype', 'dimltex1', 'dimltex2'):
                try:
                    linetype_name = self.dimstyle_attribs.pop(attrib_name)
                except KeyError:
                    pass
                else:
                    set_linetype_handle(attrib_name, linetype_name)

        self.dimension.set_acad_dstyle(self.dimstyle_attribs)

    def set_arrows(self, blk: str = None, blk1: str = None, blk2: str = None, ldrblk: str = None,
                   size: float = None) -> None:
        """
        Set arrows or user defined blocks and disable oblique stroke as tick.

        Args:
            blk: defines both arrows at once as name str or user defined block (name)
            blk1: defines left arrow as name str or as user defined block (name)
            blk2: defines right arrow as name str or as user defined block (name)
            ldrblk: defines leader arrow as name str or as user defined block (name)
            size: arrow size in drawing units

        """

        def set_arrow(dimvar: str, name: str) -> None:
            self.dimstyle_attribs[dimvar] = name

        if size is not None:
            self.dimstyle_attribs['dimasz'] = float(size)
        if blk is not None:
            set_arrow('dimblk', blk)
            self.dimstyle_attribs['dimsah'] = 0
            self.dimstyle_attribs['dimtsz'] = 0.  # use arrows
        if blk1 is not None:
            set_arrow('dimblk1', blk1)
            self.dimstyle_attribs['dimsah'] = 1
            self.dimstyle_attribs['dimtsz'] = 0.  # use arrows
        if blk2 is not None:
            set_arrow('dimblk2', blk2)
            self.dimstyle_attribs['dimsah'] = 1
            self.dimstyle_attribs['dimtsz'] = 0.  # use arrows
        if ldrblk is not None:
            set_arrow('dimldrblk', ldrblk)

    def get_arrow_names(self) -> Tuple[str, str]:
        """
        Get arrows as name strings like 'ARCHTICK'.

        """
        dimtsz = self.get('dimtsz')
        blk1, blk2 = None, None
        if dimtsz == 0.:
            if bool(self.get('dimsah')):
                blk1 = self.get('dimblk1')
                blk2 = self.get('dimblk2')
            else:
                blk = self.get('dimblk')
                blk1 = blk
                blk2 = blk
        return blk1, blk2

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

        Args:
            size: arrow size in daring units

        """
        self.dimstyle_attribs['dimtsz'] = float(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, `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 halign:
            self.dimstyle_attribs['dimjust'] = DIMJUST[halign.lower()]

        if valign:
            valign = valign.lower()
            self.dimstyle_attribs['dimtad'] = DIMTAD[valign]
            if valign == 'center' and vshift is not None:
                self.dimstyle_attribs['dimtvp'] = float(vshift)

    def set_tolerance(self, upper: float, lower: float = None, hfactor: float = None,
                      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"
            dec: Sets the number of decimal places displayed
            leading_zeros: suppress leading zeros for decimal dimensions if False
            trailing_zeros: suppress trailing zeros for decimal dimensions if False

        """
        self.dimstyle_attribs['dimtol'] = 1
        self.dimstyle_attribs['dimlim'] = 0
        self.dimstyle_attribs['dimtp'] = float(upper)
        if lower is not None:
            self.dimstyle_attribs['dimtm'] = float(lower)
        else:
            self.dimstyle_attribs['dimtm'] = float(upper)
        if hfactor is not None:
            self.dimstyle_attribs['dimtfac'] = float(hfactor)
        if align is not None:
            self.dimstyle_attribs['dimtolj'] = const.MTEXT_INLINE_ALIGN[align.upper()]
        if dec is not None:
            self.dimstyle_attribs['dimtdec'] = dec

        # 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.dimstyle_attribs['dimtzin'] = dimtzin

    def set_limits(self, upper: float, lower: float, hfactor: float = None,
                   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.dimstyle_attribs['dimlim'] = 1
        self.dimstyle_attribs['dimtol'] = 0
        self.dimstyle_attribs['dimtp'] = float(upper)
        self.dimstyle_attribs['dimtm'] = float(lower)
        if hfactor is not None:
            self.dimstyle_attribs['dimtfac'] = float(hfactor)

        # 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.dimstyle_attribs['dimtzin'] = dimtzin

        if dec is not None:
            self.dimstyle_attribs['dimtdec'] = int(dec)

    def set_text_format(self, prefix: str = '', postfix: str = '', rnd: float = None, dec: int = None, sep: str = None,
                        leading_zeros: bool = None, trailing_zeros: bool = None) -> None:
        """
        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
            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.dimstyle_attribs['dimpost'] = prefix + '<>' + postfix
        if rnd is not None:
            self.dimstyle_attribs['dimrnd'] = rnd
        if dec is not None:
            self.dimstyle_attribs['dimdec'] = dec
        if sep is not None:
            self.dimstyle_attribs['dimdsep'] = ord(sep)
        # 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.dimstyle_attribs['dimzin'] = dimzin

    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
            lineweight: line weight as int, 13 = 0.13mm, 200 = 2.00mm
            extension: extension length
            disable1: True to suppress first part of dimension line
            disable2: True to suppress second part of dimension line

        """
        if color is not None:
            self.dimstyle_attribs['dimclrd'] = color
        if linetype is not None:
            self.dimstyle_attribs['dimltype'] = linetype
        if lineweight is not None:
            self.dimstyle_attribs['dimlwd'] = lineweight
        if extension is not None:
            self.dimstyle_attribs['dimdle'] = extension
        if disable1 is not None:
            self.dimstyle_attribs['dimsd1'] = disable1
        if disable2 is not None:
            self.dimstyle_attribs['dimsd2'] = disable2

    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.dimstyle_attribs['dimclre'] = color
        if lineweight is not None:
            self.dimstyle_attribs['dimlwe'] = lineweight
        if extension is not None:
            self.dimstyle_attribs['dimexe'] = extension
        if offset is not None:
            self.dimstyle_attribs['dimexo'] = offset
        if fixed_length is not None:
            self.dimstyle_attribs['dimflxon'] = 1
            self.dimstyle_attribs['dimflx'] = fixed_length

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

        Args:
            linetype: linetype for extension line 1
            disable: disable extension line 1 if True

        """
        if linetype is not None:
            self.dimstyle_attribs['dimltex1'] = linetype
        if disable:
            self.dimstyle_attribs['dimse1'] = 1

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

        Args:
            linetype: linetype for extension line 2
            disable: disable extension line 2 if True

        """
        if linetype is not None:
            self.dimstyle_attribs['dimltex2'] = linetype
        if disable:
            self.dimstyle_attribs['dimse2'] = 1

    def set_text(self, text='<>') -> None:
        """
        Set dimension text.

            - text == ' ' ... suppress dimension text
            - text == '' or '<>' ... use measured distance as dimension text
            - else use text literally

        Args:
            text: string

        """
        self.dimension.dxf.text = text

    def shift_text(self, dh: float, dv: float) -> None:
        """
        Set relative text movement, implemented as user location override without leader.

        Args:
            dh: shift text in text direction
            dv: shift text perpendicular to text direction

        """
        self.dimstyle_attribs['text_shift_h'] = dh
        self.dimstyle_attribs['text_shift_v'] = dv

    def set_location(self, location: 'Vertex', leader=False, relative=False):
        self.dimstyle_attribs['dimtmove'] = 1 if leader else 2
        self.dimension.set_flag_state(self.dimension.USER_LOCATION_OVERRIDE, state=True, name='dimtype')
        self.dimstyle_attribs['user_location'] = Vector(location)
        self.dimstyle_attribs['relative_user_location'] = relative

    def get_renderer(self, ucs: 'UCS' = None):
        return self.drawing.dimension_renderer.dispatch(self, ucs)

    def render(self, ucs: 'UCS' = None, discard=False) -> 'BaseDimensionRenderer':
        """
        Initiate dimension line rendering process and also writes overridden dimension style attributes into the DSTYLE
        XDATA section.

        For a friendly CAD applications like BricsCAD you can discard the dimension line rendering, because it is done
        automatically by BricsCAD, if no dimension rendering BLOCK is available and it is likely to get better results
        as by ezdxf.

        AutoCAD does not render DIMENSION entities automatically, so I rate AutoCAD as unfriendly CAD application.

        Args:
            ucs: user coordinate system
            discard: discard rendering done by ezdxf (works with BricsCAD, but not with AutoCAD)

        Returns: used renderer for analytics

        """

        renderer = self.get_renderer(ucs)
        if discard:
            self.drawing.add_acad_incompatibility_message('DIMENSION without geometry as BLOCK (discard=True)')
        else:
            block = self.drawing.blocks.new_anonymous_block(type_char='D')
            self.dimension.dxf.geometry = block.name
            renderer.render(block)

        # should be called after rendering
        renderer.finalize()

        if len(self.dimstyle_attribs):
            self.commit()
        return renderer
