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

from ezdxf.lldxf.attributes import DXFAttr, DXFAttributes, DefSubclass, XType
from ezdxf.lldxf.types import DXFTag
from ezdxf.lldxf.extendedtags import ExtendedTags
from ezdxf.lldxf.const import DXFValueError
from ezdxf.lldxf.packedtags import TagArray, VertexArray
from ezdxf.math.bspline import knot_uniform, knot_open_uniform
from ezdxf.lldxf import loader

from .graphics import none_subclass, entity_subclass, ModernGraphicEntity

if TYPE_CHECKING:
    from ezdxf.eztypes import Tags, Vertex
    import array


class KnotTags(TagArray):
    code = -40  # compatible with DXFTag.code
    VALUE_CODE = 40
    DTYPE = 'f'

    def dxftags(self) -> Iterable[DXFTag]:
        # knot value count
        yield DXFTag(72, len(self.value))
        # Python 2.7 compatible
        for t in super(KnotTags, self).dxftags():
            yield t


class WeightTags(TagArray):
    code = -41  # compatible with DXFTag.code
    VALUE_CODE = 41
    DTYPE = 'f'


class ControlPoints(VertexArray):
    code = -10  # compatible with DXFTag.code
    VERTEX_CODE = 10
    VERTEX_SIZE = 3

    def dxftags(self) -> Iterable[DXFTag]:
        # control point count
        yield DXFTag(73, len(self))
        # Python 2.7 compatible
        for t in super(ControlPoints, self).dxftags():
            yield t


class FitPoints(VertexArray):
    code = -11  # compatible with DXFTag.code
    VERTEX_CODE = 11
    VERTEX_SIZE = 3

    def dxftags(self) -> Iterable[DXFTag]:
        # fit point count
        yield DXFTag(74, len(self))
        # Python 2.7 compatible
        for t in super(FitPoints, self).dxftags():
            yield t


REMOVE_CODES = (ControlPoints.VERTEX_CODE, FitPoints.VERTEX_CODE, KnotTags.VALUE_CODE, WeightTags.VALUE_CODE) + (
    72, 73, 74)


@loader.register('SPLINE', legacy=False)
def tag_processor(tags: ExtendedTags) -> ExtendedTags:
    spline_tags = tags.get_subclass('AcDbSpline')
    control_points = ControlPoints.from_tags(spline_tags)
    fit_points = FitPoints.from_tags(spline_tags)
    knots = KnotTags.from_tags(spline_tags)
    weights = WeightTags.from_tags(spline_tags)
    spline_tags.remove_tags(codes=REMOVE_CODES)
    spline_tags.extend((knots, weights, control_points, fit_points))
    return tags


_SPLINE_TPL = """0
SPLINE
5
0
330
0
100
AcDbEntity
8
0
100
AcDbSpline
70
0
71
3
72
0
73
0
74
0
"""

spline_subclass = DefSubclass('AcDbSpline', {
    'flags': DXFAttr(70, default=0),
    'degree': DXFAttr(71),
    'n_knots': DXFAttr(72, xtype=XType.callback, getter='knot_value_count'),
    'n_control_points': DXFAttr(73, xtype=XType.callback, getter='control_point_count'),
    'n_fit_points': DXFAttr(74, xtype=XType.callback, getter='fit_point_count'),
    'knot_tolerance': DXFAttr(42, default=1e-10),
    'control_point_tolerance': DXFAttr(43, default=1e-10),
    'fit_tolerance': DXFAttr(44, default=1e-10),
    'start_tangent': DXFAttr(12, xtype=XType.point3d),
    'end_tangent': DXFAttr(13, xtype=XType.point3d),
    'extrusion': DXFAttr(210, xtype=XType.point3d, default=(0.0, 0.0, 1.0)),
    # 10: Control points (in WCS); one entry per control point
    # 11: Fit points (in WCS); one entry per fit point
    # 40: Knot value (one entry per knot)
    # 41: Weight (if not 1); with multiple group pairs, they are present if all are not 1
})


class Spline(ModernGraphicEntity):
    __slots__ = ()
    TEMPLATE = tag_processor(ExtendedTags.from_text(_SPLINE_TPL))
    DXFATTRIBS = DXFAttributes(none_subclass, entity_subclass, spline_subclass)
    CLOSED = 1  # closed b-spline
    PERIODIC = 2  # uniform b-spline
    RATIONAL = 4  # rational b-spline
    PLANAR = 8  # all spline points in a plane, don't read or set this bit, just ignore like AutoCAD
    LINEAR = 16  # always set with PLANAR, don't read or set this bit, just ignore like AutoCAD

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

    @property
    def closed(self) -> bool:
        return self.get_flag_state(self.CLOSED, name='flags')

    @closed.setter
    def closed(self, status: bool) -> None:
        self.set_flag_state(self.CLOSED, state=status, name='flags')

    @property
    def knot_values(self) -> 'array.array':  # group code 40
        """
        Returns spline knot values as array.array('f').

        """
        return cast('array.array', self.AcDbSpline.get_first_tag(KnotTags.code).value)

    def knot_value_count(self) -> int:  # DXF callback attribute Spline.dxf.n_knots
        return len(self.knot_values)

    def get_knot_values(self) -> 'array.array':  # deprecated
        return self.knot_values

    def set_knot_values(self, knot_values: Iterable[float]) -> None:
        knots = cast(KnotTags, self.AcDbSpline.get_first_tag(KnotTags.code))
        knots.set_values(knot_values)

    @property
    def weights(self) -> 'array.array':  # group code 41
        """
        Returns spline control point weights as array.array('f').

        """
        return cast('array.array', self.AcDbSpline.get_first_tag(WeightTags.code).value)

    def get_weights(self):  # deprecated
        return self.weights

    def set_weights(self, values):
        weights = cast(WeightTags, self.AcDbSpline.get_first_tag(WeightTags.code))
        weights.set_values(values)

    @property
    def control_points(self) -> ControlPoints:  # group code 10
        """
        Returns spline control points as ControlPoints() object.

        """
        return self.AcDbSpline.get_first_tag(ControlPoints.code)

    def control_point_count(self) -> int:  # DXF callback attribute Spline.dxf.n_control_points
        return len(self.control_points)

    def get_control_points(self) -> ControlPoints:  # deprecated
        return self.control_points

    def set_control_points(self, points: Iterable['Vertex']) -> None:
        vertices = self.control_points
        vertices.clear()
        vertices.extend(points)

    @property
    def fit_points(self) -> FitPoints:  # group code 11
        """
        Returns spline fit points as FitPoints() object.

        """
        return self.AcDbSpline.get_first_tag(FitPoints.code)

    def fit_point_count(self) -> int:  # DXF callback attribute Spline.dxf.n_fit_points
        return len(self.fit_points)

    def get_fit_points(self) -> FitPoints:  # deprecated
        return self.fit_points

    def set_fit_points(self, points: Iterable['Vertex']) -> None:
        vertices = self.fit_points
        vertices.clear()
        vertices.extend(points)

    def set_open_uniform(self, control_points: Sequence['Vertex'], degree: int = 3) -> None:
        """
        Open B-spline with uniform knot vector, start and end at your first and last control points.

        """
        self.dxf.flags = 0  # clear all flags
        self.dxf.degree = degree
        self.set_control_points(control_points)
        self.set_knot_values(knot_open_uniform(len(control_points), degree + 1))

    def set_uniform(self, control_points: Sequence['Vertex'], degree: int = 3) -> None:
        """
        B-spline with uniform knot vector, does NOT start and end at your first and last control points.

        """
        self.dxf.flags = 0  # clear all flags
        self.dxf.degree = degree
        self.set_control_points(control_points)
        self.set_knot_values(knot_uniform(len(control_points), degree + 1))

    def set_periodic(self, control_points: Sequence['Vertex'], degree=3) -> None:
        """
        Closed B-spline with uniform knot vector, start and end at your first control point.

        """
        self.dxf.flags = self.PERIODIC | self.CLOSED
        self.dxf.degree = degree
        self.set_control_points(control_points)
        # AutoDesk Developer Docs:
        # If the spline is periodic, the length of knot vector will be greater than length of the control array by 1.
        self.set_knot_values(list(range(len(control_points) + 1)))

    def set_open_rational(self, control_points: Sequence['Vertex'], weights: Sequence[float], degree: int = 3) -> None:
        """
        Open rational B-spline with uniform knot vector, start and end at your first and last control points, and has
        additional control possibilities by weighting each control point.

        """
        self.set_open_uniform(control_points, degree=degree)
        self.dxf.flags = self.dxf.flags | self.RATIONAL
        if len(weights) != len(control_points):
            raise DXFValueError('Control point count must be equal to weights count.')
        self.set_weights(weights)

    def set_uniform_rational(self, control_points: Sequence['Vertex'], weights: Sequence[float],
                             degree: int = 3) -> None:
        """
        Rational B-spline with uniform knot vector, deos NOT start and end at your first and last control points, and
        has additional control possibilities by weighting each control point.

        """
        self.set_uniform(control_points, degree=degree)
        self.dxf.flags = self.dxf.flags | self.RATIONAL
        if len(weights) != len(control_points):
            raise DXFValueError('Control point count must be equal to weights count.')
        self.set_weights(weights)

    def set_periodic_rational(self, control_points: Sequence['Vertex'], weights: Sequence[float],
                              degree: int = 3) -> None:
        """
        Closed rational B-spline with uniform knot vector, start and end at your first control point, and has
        additional control possibilities by weighting each control point.

        """
        self.set_periodic(control_points, degree=degree)
        self.dxf.flags = self.dxf.flags | self.RATIONAL
        if len(weights) != len(control_points):
            raise DXFValueError('Control point count must be equal to weights count.')
        self.set_weights(weights)

    @contextmanager
    def edit_data(self) -> 'SplineData':
        """
        Edit spline data by context manager, usage::

        with spline.edit_data() as data:
            # set uniform knot vector
            data.knots_values = list(range(spline.dxf.n_control_points+spline.dxf.degree+1))

        Yields: SplineData()

        """
        data = SplineData(self)
        yield data
        if data.fit_points is not self.fit_points:
            self.set_fit_points(data.fit_points)

        if data.control_points is not self.control_points:
            self.set_control_points(data.control_points)

        if data.knot_values is not self.knot_values:
            self.set_knot_values(data.knot_values)

        if data.weights is not self.weights:
            self.set_weights(data.weights)


class SplineData:
    def __init__(self, spline: 'Spline'):
        self.fit_points = spline.fit_points
        self.control_points = spline.control_points
        self.knot_values = spline.knot_values
        self.weights = spline.weights
