# Created: 24.05.2015
# Copyright (c) 2015-2018, Manfred Moitzi
# License: MIT License
from typing import TYPE_CHECKING, Optional, List, Tuple, Sequence, Union, Iterable
import math
from contextlib import contextmanager

from ezdxf.lldxf.attributes import DXFAttr, DXFAttributes, DefSubclass, XType
from ezdxf.lldxf.types import DXFTag, DXFVertex
from ezdxf.lldxf.tags import DXFStructureError, group_tags, Tags
from ezdxf.lldxf.extendedtags import ExtendedTags
from ezdxf.lldxf import const
from ezdxf.tools.pattern import PATTERN  # acad standard pattern definitions
from ezdxf.tools.rgb import rgb2int, int2rgb
from ezdxf.lldxf.const import DXFValueError, DXFVersionError
from ezdxf.math.bspline import bspline_control_frame

from .graphics import none_subclass, entity_subclass, ModernGraphicEntity

if TYPE_CHECKING:
    from ezdxf.eztypes import RGB

_HATCH_TPL = """0
HATCH
5
0
330
0
100
AcDbEntity
8
0
62
1
100
AcDbHatch
10
0.0
20
0.0
30
0.0
210
0.0
220
0.0
230
1.0
2
SOLID
70
1
71
0
91
0
75
1
76
1
98
1
10
0.0
20
0.0
"""

# removed tag (code=47, 0.0442352806926743) from Template: pixel size - caused problems in AutoCAD

# default is a solid fil hatch
hatch_subclass = DefSubclass('AcDbHatch', {
    'elevation': DXFAttr(10, xtype=XType.point3d, default=(0.0, 0.0, 0.0)),
    'extrusion': DXFAttr(210, xtype=XType.point3d, default=(0.0, 0.0, 1.0)),
    'pattern_name': DXFAttr(2, default='SOLID'),  # for solid fill
    'solid_fill': DXFAttr(70, default=1),  # pattern fill = 0
    'associative': DXFAttr(71, default=0),  # associative flag = 0
    'hatch_style': DXFAttr(75, default=const.HATCH_STYLE_OUTERMOST),  # 0=normal; 1=outer; 2=ignore
    'pattern_type': DXFAttr(76, default=const.HATCH_TYPE_PREDEFINED),  # 0=user; 1=predefined; 2=custom???
    'pattern_angle': DXFAttr(52, default=0.0),  # degrees (360deg = circle)
    'pattern_scale': DXFAttr(41, default=1.0),
    'pattern_double': DXFAttr(77, default=0),  # 0=not double; 1= double
    'n_seed_points': DXFAttr(98),  # number of seed points
})

GRADIENT_CODES = frozenset([450, 451, 452, 453, 460, 461, 462, 463, 470, 421, 63])
PATH_CODES = frozenset([10, 11, 12, 13, 40, 42, 50, 51, 42, 72, 73, 74, 92, 93, 94, 95, 96, 97, 330])
PATTERN_DEFINITION_LINE_CODES = frozenset((53, 43, 44, 45, 46, 79, 49))


class Hatch(ModernGraphicEntity):
    __slots__ = ()
    TEMPLATE = ExtendedTags.from_text(_HATCH_TPL)
    DXFATTRIBS = DXFAttributes(none_subclass, entity_subclass, hatch_subclass)

    @property
    def AcDbHatch(self) -> Tags:
        return self.tags.subclasses[2]

    @property
    def has_solid_fill(self) -> bool:
        return bool(self.dxf.solid_fill)

    @property
    def has_pattern_fill(self) -> bool:
        return not bool(self.dxf.solid_fill)

    @property
    def has_gradient_data(self) -> bool:
        return bool(self.AcDbHatch.get_first_value(450, 0))

    @property
    def bgcolor(self) -> Optional['RGB']:
        try:
            xdata_bgcolor = self.tags.get_xdata('HATCHBACKGROUNDCOLOR')
        except ValueError:
            return None
        color = xdata_bgcolor.get_first_value(1071, 0)
        return int2rgb(color)

    @bgcolor.setter
    def bgcolor(self, rgb: 'RGB') -> None:
        color_value = rgb2int(rgb) | -0b111110000000000000000000000000  # it's magic
        try:
            xdata_bgcolor = self.tags.get_xdata('HATCHBACKGROUNDCOLOR')
        except DXFValueError:  # no xdata for background color found
            self.tags.xdata.append(Tags([
                DXFTag(1001, 'HATCHBACKGROUNDCOLOR'),
                DXFTag(1071, color_value),
            ]))
        else:
            xdata_bgcolor.set_first(DXFTag(1071, color_value))

    @bgcolor.deleter
    def bgcolor(self) -> None:
        try:
            xdata_bgcolor = self.tags.get_xdata('HATCHBACKGROUNDCOLOR')
        except DXFValueError:  # background color does not exist
            return
        else:
            self.tags.xdata.remove(xdata_bgcolor)

    @contextmanager
    def edit_boundary(self) -> 'BoundaryPathData':
        boundary_path_data = BoundaryPathData(self)
        yield boundary_path_data
        self._set_boundary_path_data(boundary_path_data)

    def _set_boundary_path_data(self, boundary_path_data: 'BoundaryPathData') -> None:
        # replace existing path tags by the new path
        start_index = boundary_path_data.start_index
        end_index = boundary_path_data.end_index
        self.AcDbHatch[start_index: end_index] = boundary_path_data.dxftags()

    def set_solid_fill(self, color: int = 7, style: int = 1, rgb: 'RGB' = None):
        self._remove_gradient_data()
        if self.has_pattern_fill:
            self._remove_pattern_data()
            self.dxf.solid_fill = 1

        self.dxf.color = color  # if a rgb value is present, the color value is ignored by AutoCAD
        self.dxf.hatch_style = style
        self.dxf.pattern_name = 'SOLID'
        self.dxf.pattern_type = const.HATCH_TYPE_PREDEFINED
        if rgb is not None:  # if a rgb value is present, the color value is ignored by AutoCAD
            self.rgb = rgb  # rgb should be a (r, g, b) tuple

    def _remove_pattern_data(self) -> None:
        if self.has_pattern_fill:
            with self.edit_pattern() as e:  # delete existing pattern definition
                e.clear()
            self.AcDbHatch.remove_tags((52, 41, 77))
            # Important: AutoCAD does not allow the tags pattern_angle (52), pattern_scale (41), pattern_double (77) for
            # hatches with SOLID fill.

    def set_gradient(self,
                     color1: 'RGB' = (0, 0, 0),
                     color2: 'RGB' = (255, 255, 255),
                     rotation: float = 0.,
                     centered: float = 0.,
                     one_color: int = 0,
                     tint: float = 0.,
                     name: str = 'LINEAR') -> None:
        if self.drawing is not None and self.drawing.dxfversion < 'AC1018':
            raise DXFVersionError(
                "Gradient support requires at least DXF version AC1018, this drawing is:%s" % self.drawing.dxfversion)
        gradient_data = GradientData()
        gradient_data.color1 = color1
        gradient_data.color2 = color2
        gradient_data.one_color = one_color
        gradient_data.rotation = rotation
        gradient_data.centered = centered
        gradient_data.tint = tint
        gradient_data.name = name
        self._set_gradient(gradient_data)

    def get_gradient(self) -> 'GradientData':
        if not self.has_gradient_data:
            raise DXFValueError('HATCH has no gradient data.')
        return GradientData.from_tags(self.AcDbHatch)

    def _set_gradient(self, gradient_data: 'GradientData') -> None:
        gradient_data.name = gradient_data.name.upper()
        if gradient_data.name not in const.GRADIENT_TYPES:
            raise DXFValueError('Invalid gradient type name: %s' % gradient_data.name)

        self._remove_pattern_data()
        self._remove_gradient_data()
        self.dxf.solid_fill = 1
        self.dxf.pattern_name = 'SOLID'
        self.dxf.pattern_type = const.HATCH_TYPE_PREDEFINED
        self.AcDbHatch.extend(gradient_data.dxftags())

    def _remove_gradient_data(self) -> None:
        self.AcDbHatch.remove_tags(GRADIENT_CODES)

    @contextmanager
    def edit_gradient(self) -> 'GradientData':
        if not self.has_gradient_data:
            raise DXFValueError('HATCH has no gradient data.')
        gradient_data = GradientData.from_tags(self.AcDbHatch)
        yield gradient_data
        self._set_gradient(gradient_data)

    def set_pattern_fill(self, name: str, color: int = 7, angle: float = 0., scale: float = 1., double: int = 0,
                         style: int = 1, pattern_type: int = 1, definition=None) -> None:
        self._remove_gradient_data()
        self.dxf.solid_fill = 0
        self.dxf.pattern_name = name
        self.dxf.color = color
        self.dxf.hatch_style = style
        self.dxf.pattern_type = pattern_type

        # safe version of adding pattern fill specific DXF tags:
        self.AcDbHatch.remove_tags((52, 41, 77))  # remove pattern angle, pattern scale & pattern double flag if exists
        try:
            index = self.AcDbHatch.tag_index(76) + 1  # find position of pattern type (76); + 1: insert after tag 76
        except DXFValueError:
            raise DXFStructureError("HATCH: Missing required DXF tag 'Hatch pattern type' (code=76).")
        # insert pattern angle, pattern scale & pattern double flag behind pattern type
        self.AcDbHatch[index:index] = [DXFTag(52, angle), DXFTag(41, scale), DXFTag(77, double)]
        # place pattern definition right behind pattern double flag (77)
        if definition is None:
            # get pattern definition from acad standard pattern, default is 'ANSI31'
            definition = PATTERN.get(name, PATTERN['ANSI31'])
        self.set_pattern_definition(definition)

    @contextmanager
    def edit_pattern(self) -> 'PatternData':
        if self.has_solid_fill:
            raise DXFValueError('Solid fill HATCH has no pattern data.')
        pattern_data = PatternData(self)
        yield pattern_data
        self._set_pattern_data(pattern_data)

    def _set_pattern_data(self, new_pattern_data: 'PatternData') -> None:
        start_index = new_pattern_data.existing_pattern_start_index
        end_index = new_pattern_data.existing_pattern_end_index
        pattern_tags = new_pattern_data.dxftags()
        if start_index == 0:  # no existing pattern data
            try:
                start_index = self.AcDbHatch.tag_index(77) + 1  # hatch pattern double flag, used as insertion point
                end_index = start_index
            except DXFValueError:
                raise DXFStructureError("HATCH: Missing required DXF tag 'Hatch pattern double flag' (code=77).")
        # replace existing pattern data
        self.AcDbHatch[start_index: end_index] = pattern_tags

    def set_pattern_definition(self, lines: Sequence) -> None:
        """
        Setup hatch patten definition by a list of definition lines and  a definition line is a 4-tuple [angle,
        base_point, offset, dash_length_items]

        - angle: line angle in degrees
        - base-point: 2-tuple (x, y)
        - offset: 2-tuple (dx, dy)
        - dash_length_items: list of dash items (item > 0 is a line, item < 0 is a gap and item == 0.0 is a point)

        :param lines: list of definition lines
        """
        pattern_lines = [PatternDefinitionLine(line[0], line[1], line[2], line[3]) for line in lines]
        with self.edit_pattern() as pattern_editor:
            pattern_editor.lines = pattern_lines

    def get_seed_points(self) -> List[Tuple[float, float]]:
        hatch_tags = self.AcDbHatch
        first_seed_point_index = self._get_seed_point_index(hatch_tags)
        seed_points = hatch_tags.collect_consecutive_tags([10], start=first_seed_point_index)
        return [tag.value for tag in seed_points]

    def _get_seed_point_index(self, hatch_tags: Tags) -> int:
        try:
            seed_count_index = hatch_tags.tag_index(98)  # find index of 'Number of seed points'
        except DXFValueError:
            raise DXFStructureError("HATCH: Missing required DXF tag 'Number of seed points' (code=98).")
        try:
            first_seed_point_index = hatch_tags.tag_index(10, seed_count_index + 1)
        except DXFValueError:
            raise DXFStructureError("HATCH: Missing required DXF tags 'seed point X value' (code=10).")
        return first_seed_point_index

    def set_seed_points(self, points: Sequence[Tuple[float, float]]) -> None:
        if len(points) < 1:
            raise DXFValueError("Param points should be a collection of 2D points and requires at least one point.")
        hatch_tags = self.AcDbHatch
        first_seed_point_index = self._get_seed_point_index(hatch_tags)
        existing_seed_points = hatch_tags.collect_consecutive_tags([10],
                                                                   start=first_seed_point_index)  # don't rely on 'Number of seed points'
        new_seed_points = [DXFVertex(10, (point[0], point[1])) for point in points]  # only use x and y coordinate,
        self.dxf.n_seed_points = len(new_seed_points)  # set new count of seed points
        # replace existing seed points
        hatch_tags[first_seed_point_index: first_seed_point_index + len(existing_seed_points)] = new_seed_points


class BoundaryPathData:
    def __init__(self, hatch):
        self.start_index = 0
        self.end_index = 0
        self.paths = self._setup_paths(hatch.AcDbHatch)

    def _setup_paths(self, tags: Tags) -> List['PolylinePath']:
        paths = []
        try:
            self.start_index = tags.tag_index(91)  # code 91=Number of boundary paths (loops)
            n_paths = tags[self.start_index].value
        except DXFValueError:
            raise DXFStructureError("HATCH: Missing required DXF tag 'Number of boundary paths (loops)' (code=91).")

        self.end_index = self.start_index + 1  # + 1 for Tag(91, Number of boundary paths)
        if n_paths == 0:  # created by ezdxf from template without path data
            return paths

        all_path_tags = tags.collect_consecutive_tags(PATH_CODES, start=self.start_index + 1)
        self.end_index = self.start_index + len(all_path_tags) + 1  # + 1 for Tag(91, Number of boundary paths)
        # end_index: stored for Hatch._set_boundary_path_data()
        grouped_path_tags = group_tags(all_path_tags, splitcode=92)
        for path_tags in grouped_path_tags:
            path_type_flags = path_tags[0].value
            is_polyline_path = bool(path_type_flags & 2)
            path = PolylinePath.from_tags(path_tags) if is_polyline_path else EdgePath.from_tags(path_tags)
            path.path_type_flags = path_type_flags
            paths.append(path)
        return paths

    def clear(self) -> None:
        self.paths = []

    def add_polyline_path(self, path_vertices: Sequence[Tuple[float, float]], is_closed: int = 1,
                          flags: int = 1) -> 'PolylinePath':
        new_path = PolylinePath()
        new_path.set_vertices(path_vertices, is_closed)
        new_path.path_type_flags = flags | const.BOUNDARY_PATH_POLYLINE
        self.paths.append(new_path)
        return new_path

    def add_edge_path(self, flags: int = 1) -> 'EdgePath':
        new_path = EdgePath()
        new_path.path_type_flags = flags
        self.paths.append(new_path)
        return new_path

    def dxftags(self) -> List[DXFTag]:
        tags = [DXFTag(91, len(self.paths))]
        for path in self.paths:
            tags.extend(path.dxftags())
        return tags


def pop_source_boundary_objects_tags(all_path_tags: Tags) -> List[DXFTag]:
    source_boundary_object_tags = []
    while len(all_path_tags):
        if all_path_tags[-1].code in (97, 333):
            last_tag = all_path_tags.pop()
            if last_tag.code == 330:
                source_boundary_object_tags.append(last_tag)
            else:  # code == 97
                # result list does not contain the length tag!
                source_boundary_object_tags.reverse()
                return source_boundary_object_tags
        else:
            return []  # no source boundary objects found - entity is not valid for AutoCAD


def build_source_boundary_object_tags(source_boundary_objects: Sequence[DXFTag]) -> List[DXFTag]:
    source_boundary_object_tags = [DXFTag(97, len(source_boundary_objects))]
    source_boundary_object_tags.extend(source_boundary_objects)
    return source_boundary_object_tags


class PolylinePath:
    PATH_TYPE = 'PolylinePath'

    def __init__(self):
        self.path_type_flags = const.BOUNDARY_PATH_POLYLINE
        self.is_closed = 0
        self.vertices = []  # type: List[Tuple[float, float, float]]  # list of 2D coordinates with bulge values (x, y, bulge); bulge default = 0.0
        self.source_boundary_objects = []  # type: List[DXFTag]  # (330, handle) tags

    @staticmethod
    def from_tags(tags) -> 'PolylinePath':
        polyline_path = PolylinePath()
        polyline_path._setup_path(tags)
        return polyline_path

    def _setup_path(self, tags: Tags) -> None:
        self.source_boundary_objects = pop_source_boundary_objects_tags(tags)
        for tag in tags:
            code, value = tag
            if code == 10:  # vertex coordinates
                self.vertices.append((value[0], value[1], 0.0))  # (x, y, bulge); bulge default = 0.0
            elif code == 42:  # bulge value
                x, y, bulge = self.vertices.pop()  # last value
                self.vertices.append((x, y, value))  # last coordinates with new bulge value
            elif code == 72:
                pass  # ignore this value
            elif code == 73:
                self.is_closed = value
            elif code == 92:
                self.path_type_flags = value
            elif code == 93:  # number of polyline vertices
                pass  # ignore this value

    def set_vertices(self, vertices: Sequence[Sequence[float]], is_closed: bool = 1) -> None:
        new_vertices = []
        for vertex in vertices:
            if len(vertex) == 2:
                x, y = vertex
                bulge = 0
            elif len(vertex) == 3:
                x, y, bulge = vertex
            else:
                raise DXFValueError("Invalid vertex format, expected (x, y) or (x, y, bulge)")
            new_vertices.append((x, y, bulge))
        self.vertices = new_vertices
        self.is_closed = is_closed

    def clear(self) -> None:
        self.vertices = []
        self.is_closed = 0
        self.source_boundary_objects = []

    def has_bulge(self) -> bool:
        for x, y, bulge in self.vertices:
            if bulge != 0:
                return True
        return False

    def dxftags(self) -> List[DXFTag]:
        has_bulge = self.has_bulge()

        vtags = []
        for x, y, bulge in self.vertices:
            vtags.append(DXFVertex(10, (float(x), float(y))))
            if has_bulge:
                vtags.append(DXFTag(42, float(bulge)))

        tags = [
            DXFTag(92, int(self.path_type_flags)),
            DXFTag(72, int(has_bulge)),
            DXFTag(73, int(self.is_closed)),
            DXFTag(93, len(self.vertices)),
        ]
        tags.extend(vtags)
        tags.extend(build_source_boundary_object_tags(self.source_boundary_objects))
        return tags


class EdgePath:
    PATH_TYPE = 'EdgePath'

    def __init__(self):
        self.path_type_flags = const.BOUNDARY_PATH_DEFAULT
        self.edges = []
        self.source_boundary_objects = []

    @staticmethod
    def from_tags(tags: Tags) -> 'EdgePath':
        edge_path = EdgePath()
        edge_path._setup_path(tags)
        return edge_path

    def _setup_path(self, tags: Tags) -> None:
        self.source_boundary_objects = pop_source_boundary_objects_tags(tags)
        edge_groups = group_tags(tags, splitcode=72)
        for edge_tags in edge_groups:
            self.edges.append(self._setup_edge(edge_tags))

    def _setup_edge(self, tags: Tags) -> 'EdgeTypes':
        edge_type = tags[0].value
        if 0 < edge_type < 5:
            return EDGE_CLASSES[edge_type].from_tags(tags[1:])
        else:
            raise DXFStructureError("HATCH: unknown edge type: {}".format(edge_type))

    def add_line(self, start: int, end: int) -> 'LineEdge':
        line = LineEdge()
        line.start = start
        line.end = end
        self.edges.append(line)
        return line

    def add_arc(self, center: Tuple[float, float],
                radius: float = 1.,
                start_angle: float = 0.,
                end_angle: float = 360.,
                is_counter_clockwise: int = 0) -> 'ArcEdge':
        arc = ArcEdge()
        arc.center = center
        arc.radius = radius
        arc.start_angle = start_angle
        arc.end_angle = end_angle
        arc.is_counter_clockwise = 1 if bool(is_counter_clockwise) else 0
        self.edges.append(arc)
        return arc

    def add_ellipse(self, center: Tuple[float, float],
                    major_axis: Tuple[float, float] = (1., 0.),
                    ratio: float = 1.,
                    start_angle: float = 0.,
                    end_angle: float = 360.,
                    is_counter_clockwise: int = 0) -> 'EllipseEdge':
        if ratio > 1.:
            raise DXFValueError("Parameter 'ratio' has to be <= 1.0")
        ellipse = EllipseEdge()
        ellipse.center = center
        ellipse.major_axis = major_axis
        ellipse.ratio = ratio
        ellipse.start_angle = start_angle
        ellipse.end_angle = end_angle
        ellipse.is_counter_clockwise = is_counter_clockwise
        self.edges.append(ellipse)
        return ellipse

    def add_spline(self, fit_points: Iterable[Tuple[float, float]] = None,
                   control_points: Iterable[Tuple[float, float]] = None,
                   knot_values: Iterable[float] = None,
                   weights: Iterable[float] = None,
                   degree: int = 3,
                   rational: int = 0,
                   periodic: int = 0) -> 'SplineEdge':
        spline = SplineEdge()
        if fit_points is not None:
            spline.fit_points = list(fit_points)
        if control_points is not None:
            spline.control_points = list(control_points)
        if knot_values is not None:
            spline.knot_values = list(knot_values)
        if weights is not None:
            spline.weights = list(weights)
        spline.degree = degree
        spline.rational = int(rational)
        spline.periodic = int(periodic)
        self.edges.append(spline)
        return spline

    def add_spline_control_frame(self, fit_points: Iterable[Tuple[float, float]],
                                 degree: int = 3,
                                 method: str = 'distance',
                                 power: float = .5) -> 'SplineEdge':
        bspline = bspline_control_frame(fit_points=fit_points, degree=degree, method=method, power=power)
        return self.add_spline(
            fit_points=fit_points,
            control_points=bspline.control_points,
            knot_values=bspline.knot_values(),
        )

    def clear(self) -> None:
        self.edges = []

    def dxftags(self) -> List[DXFTag]:
        tags = [DXFTag(92, int(self.path_type_flags)), DXFTag(93, len(self.edges))]
        for edge in self.edges:
            tags.extend(edge.dxftags())
        tags.extend(build_source_boundary_object_tags(self.source_boundary_objects))
        return tags


class LineEdge:
    EDGE_TYPE = "LineEdge"

    # more struct than object

    def __init__(self):
        self.start = (0, 0)  # type: Tuple[float, float]
        self.end = (0, 0)  # type: Tuple[float, float]

    @staticmethod
    def from_tags(tags: Tags) -> 'LineEdge':
        edge = LineEdge()
        for tag in tags:
            code, value = tag
            if code == 10:
                edge.start = value
            elif code == 11:
                edge.end = value
        return edge

    def dxftags(self) -> List[DXFTag]:
        return [
            DXFTag(72, 1),  # edge type
            DXFVertex(10, self.start),
            DXFVertex(11, self.end),
        ]


class ArcEdge:
    EDGE_TYPE = "ArcEdge"

    # more struct than object

    def __init__(self):
        self.center = (0., 0.)  # type: Tuple[float, float]
        self.radius = 1.
        self.start_angle = 0.
        self.end_angle = 360.
        self.is_counter_clockwise = 0

    @staticmethod
    def from_tags(tags: Tags) -> 'ArcEdge':
        edge = ArcEdge()
        for tag in tags:
            code, value = tag
            if code == 10:
                edge.center = value
            elif code == 40:
                edge.radius = value
            elif code == 50:
                edge.start_angle = value
            elif code == 51:
                edge.end_angle = value
            elif code == 73:
                edge.is_counter_clockwise = value
        return edge

    def dxftags(self) -> List[DXFTag]:
        return [
            DXFTag(72, 2),  # edge type
            DXFVertex(10, self.center),
            DXFTag(40, self.radius),
            DXFTag(50, self.start_angle),
            DXFTag(51, self.end_angle),
            DXFTag(73, self.is_counter_clockwise),
        ]


class EllipseEdge:
    EDGE_TYPE = "EllipseEdge"

    # more struct than object

    def __init__(self):
        self.center = (0., 0.)  # type: Tuple[float, float]
        self.major_axis = (1., 0.)    # type: Tuple[float, float] # Endpoint of major axis relative to center point (in OCS)
        self.ratio = 1.
        self.start_angle = 0.
        self.end_angle = 360.
        self.is_counter_clockwise = 0

    @staticmethod
    def from_tags(tags: Tags) -> 'EllipseEdge':
        edge = EllipseEdge()
        for tag in tags:
            code, value = tag
            if code == 10:
                edge.center = value
            elif code == 11:
                edge.major_axis = value
            elif code == 40:
                edge.ratio = value
            elif code == 50:
                edge.start_angle = value
            elif code == 51:
                edge.end_angle = value
            elif code == 73:
                edge.is_counter_clockwise = value
        return edge

    def dxftags(self) -> List[DXFTag]:
        return [
            DXFTag(72, 3),  # edge type
            DXFVertex(10, self.center),
            DXFVertex(11, self.major_axis),
            DXFTag(40, self.ratio),
            DXFTag(50, self.start_angle),
            DXFTag(51, self.end_angle),
            DXFTag(73, self.is_counter_clockwise)
        ]


class SplineEdge:
    EDGE_TYPE = "SplineEdge"

    def __init__(self):
        self.degree = 3  # code = 94
        self.rational = 0  # code = 73
        self.periodic = 0  # code = 74
        self.knot_values = []  # type: List[float]
        self.control_points = []  # type: List[Tuple[float, float]]
        self.fit_points = []  # type: List[Tuple[float, float]]
        self.weights = []  # type: List[float]
        self.start_tangent = (0, 0)  # type: Tuple[float, float]
        self.end_tangent = (0, 0)  # type: Tuple[float, float]

    @staticmethod
    def from_tags(tags: Tags) -> 'SplineEdge':
        edge = SplineEdge()
        for tag in tags:
            code, value = tag
            if code == 94:
                edge.degree = value
            elif code == 73:
                edge.rational = value
            elif code == 74:
                edge.periodic = value
            elif code == 40:
                edge.knot_values.append(value)
            elif code == 42:
                edge.weights.append(value)
            elif code == 10:
                edge.control_points.append(value)
            elif code == 11:
                edge.fit_points.append(value)
            elif code == 12:
                edge.start_tangent = value
            elif code == 13:
                edge.end_tangent = value
        return edge

    def dxftags(self) -> List[DXFTag]:
        tags = [
            DXFTag(72, 4),  # edge type
            DXFTag(94, int(self.degree)),
            DXFTag(73, int(self.rational)),
            DXFTag(74, int(self.periodic)),
            DXFTag(95, len(self.knot_values)),  # number of knots
            DXFTag(96, len(self.control_points)),  # number of control points
        ]
        # build knot values list
        # knot values have to be present and valid, otherwise AutoCAD crashes
        if len(self.knot_values):
            tags.extend([DXFTag(40, float(value)) for value in self.knot_values])
        else:
            raise DXFValueError("SplineEdge: missing required knot values")

        # build control points
        # control points have to be present and valid, otherwise AutoCAD crashes
        tags.extend([DXFVertex(10, (float(value[0]), float(value[1]))) for value in self.control_points])

        # build weights list, optional
        tags.extend([DXFTag(42, float(value)) for value in self.weights])

        # build fit points
        # fit points have to be present and valid, otherwise AutoCAD crashes
        # edit 2016-12-20: this is not true - there are cases with no fit points and without crashing AutoCAD
        tags.append(DXFTag(97, len(self.fit_points)))
        tags.extend([DXFVertex(11, (float(value[0]), float(value[1]))) for value in self.fit_points])

        tags.append(DXFVertex(12, (float(self.start_tangent[0]), float(self.start_tangent[1]))))
        tags.append(DXFVertex(13, (float(self.end_tangent[0]), float(self.end_tangent[1]))))
        return tags


EDGE_CLASSES = [None, LineEdge, ArcEdge, EllipseEdge, SplineEdge]
EdgeTypes = Union[LineEdge, ArcEdge, EllipseEdge, SplineEdge]


class PatternData:
    def __init__(self, hatch: Hatch):
        self.existing_pattern_start_index = 0
        self.existing_pattern_end_index = 0
        self.lines = self._setup_pattern_lines(hatch.AcDbHatch)

    def _setup_pattern_lines(self, tags: Tags) -> List['PatternDefinitionLine']:
        try:
            self.existing_pattern_start_index = tags.tag_index(78)  # code 78=Number of patter definition lines
        except DXFValueError:  # no pattern definition lines found
            self.existing_pattern_start_index = 0
            self.existing_pattern_end_index = 0
            return []

        all_pattern_tags = tags.collect_consecutive_tags(PATTERN_DEFINITION_LINE_CODES,
                                                         start=self.existing_pattern_start_index + 1)
        self.existing_pattern_end_index = self.existing_pattern_start_index + len(
            all_pattern_tags) + 1  # + 1 for Tag(78, Number of boundary paths)
        # existing_pattern_end_index: stored for Hatch._set_pattern_data()
        grouped_line_tags = group_tags(all_pattern_tags, splitcode=53)
        return [PatternDefinitionLine.from_tags(line_tags) for line_tags in grouped_line_tags]

    def clear(self) -> None:
        self.lines = []

    def add_line(self,
                 angle: float = 0.,
                 base_point: Tuple[float, float] = (0., 0.),
                 offset: Tuple[float, float] = (0., 0.),
                 dash_length_items: List[float] = None) -> None:
        self.lines.append(self.new_line(angle, base_point, offset, dash_length_items))

    @staticmethod
    def new_line(angle: float = 0.,
                 base_point: Tuple[float, float] = (0., 0.),
                 offset: Tuple[float, float] = (0., 0.),
                 dash_length_items: List[float] = None) -> 'PatternDefinitionLine':
        if dash_length_items is None:
            raise DXFValueError("Parameter 'dash_length_items' must not be None.")
        return PatternDefinitionLine(angle, base_point, offset, dash_length_items)

    def dxftags(self) -> List[DXFTag]:
        if len(self.lines):
            tags = [DXFTag(78, len(self.lines))]
            for line in self.lines:
                tags.extend(line.dxftags())
        else:
            tags = []
        return tags

    def __str__(self) -> str:
        return "[" + ",".join(str(line) for line in self.lines) + "]"


class PatternDefinitionLine:
    def __init__(self,
                 angle: float = 0.,
                 base_point: Tuple[float, float] = (0., 0.),
                 offset: Tuple[float, float] = (0., 0.),
                 dash_length_items: List[float] = None):
        self.angle = angle  # as always in degrees (circle = 360 deg)
        self.base_point = base_point
        self.offset = offset
        self.dash_length_items = [] if dash_length_items is None else dash_length_items  # type: List[float]
        # dash_length_items = [item0, item1, ...]
        # item > 0 is line, < 0 is gap, 0.0 = dot;

    @staticmethod
    def from_tags(tags: Tags) -> 'PatternDefinitionLine':
        p = {53: 0, 43: 0, 44: 0, 45: 0, 46: 0}
        dash_length_items = []
        for tag in tags:
            code, value = tag
            if code == 49:
                dash_length_items.append(value)
            else:
                p[code] = value
        return PatternDefinitionLine(p[53], (p[43], p[44]), (p[45], p[46]), dash_length_items)

    def dxftags(self) -> List[DXFTag]:
        tags = [
            DXFTag(53, self.angle),
            DXFTag(43, self.base_point[0]),
            DXFTag(44, self.base_point[1]),
            DXFTag(45, self.offset[0]),
            DXFTag(46, self.offset[1]),
            DXFTag(79, len(self.dash_length_items)),
        ]
        tags.extend(DXFTag(49, item) for item in self.dash_length_items)
        return tags

    def __str__(self):
        return "[{0.angle}, {0.base_point}, {0.offset}, {0.dash_length_items}]".format(self)


class GradientData:
    def __init__(self):
        self.color1 = (0, 0, 0)  # type: RGB
        self.color2 = (255, 255, 255)  # type: RGB
        self.one_color = 0  # if 1 - a fill that uses a smooth transition between color1 and a specified tint
        self.rotation = 0.  # use grad NOT radians here, because there should be ONE system for all angles
        self.centered = 0.
        self.tint = 0.0
        self.name = 'LINEAR'

    @staticmethod
    def from_tags(tags: Tags) -> 'GradientData':
        gdata = GradientData()
        try:
            index = tags.tag_index(450)  # required tag if hatch has gradient data
        except DXFValueError:
            raise DXFStructureError("HATCH: Missing required DXF tag for gradient data (code=450).")
        # if tags[index].value == 0 hatch is a solid ????
        first_color_value = True
        for code, value in tags[index:]:
            if code == 460:
                gdata.rotation = (value / math.pi) * 180.  # radians to grad
            elif code == 461:
                gdata.centered = value
            elif code == 452:
                gdata.one_color = value
            elif code == 462:
                gdata.tint = value
            elif code == 470:
                gdata.name = value
            elif code == 421:
                # code == 63 color as ACI, can be ignored
                if first_color_value:
                    gdata.color1 = int2rgb(value)
                    first_color_value = False
                else:
                    gdata.color2 = int2rgb(value)
        return gdata

    def dxftags(self) -> List[DXFTag]:
        return [
            DXFTag(450, 1),  # gradient
            DXFTag(451, 0),  # reserved for the future
            DXFTag(452, self.one_color),  # one (1) or two (0) color gradient
            DXFTag(453, 2),  # number of colors
            DXFTag(460, (self.rotation / 180.) * math.pi),  # rotation angle in radians
            DXFTag(461, self.centered),  # see DXF standard
            DXFTag(462, self.tint),  # see DXF standard
            DXFTag(463, 0),  # first value, see DXF standard
            # code == 63 "color as ACI" can be left off
            DXFTag(421, rgb2int(self.color1)),  # first color
            DXFTag(463, 1),  # second value, see DXF standard
            # code == 63 "color as ACI" can be left off
            DXFTag(421, rgb2int(self.color2)),  # second color
            DXFTag(470, self.name),
        ]
