import math
from .base import LottieObject, LottieProp, LottieEnum, NVector
from .properties import Value, MultiDimensional, GradientColors, ShapeProperty, Bezier, ColorValue, PositionValue
from ..utils.color import Color
from .helpers import Transform, VisualObject, BlendMode


class BoundingBox:
    """!
    Shape bounding box
    """
    def __init__(self, x1=None, y1=None, x2=None, y2=None):
        self.x1 = x1
        self.y1 = y1
        self.x2 = x2
        self.y2 = y2

    def include(self, x, y):
        """!
        Expands the box to include the point at x, y
        """
        if x is not None:
            if self.x1 is None or self.x1 > x:
                self.x1 = x
            if self.x2 is None or self.x2 < x:
                self.x2 = x
        if y is not None:
            if self.y1 is None or self.y1 > y:
                self.y1 = y
            if self.y2 is None or self.y2 < y:
                self.y2 = y

    def expand(self, other):
        """!
        Expands the bounding box to include another bounding box
        """
        self.include(other.x1, other.y1)
        self.include(other.x2, other.y2)

    def center(self):
        """!
        Center point of the bounding box
        """
        return NVector((self.x1 + self.x2) / 2, (self.y1 + self.y2) / 2)

    def isnull(self):
        """!
        Whether the box is default-initialized
        """
        return self.x1 is None or self.y2 is None

    def __repr__(self):
        return "<BoundingBox [%s, %s] - [%s, %s]>" % (self.x1, self.y1, self.x2, self.y2)

    @property
    def width(self):
        if self.isnull():
            return 0
        return self.x2 - self.x1

    @property
    def height(self):
        if self.isnull():
            return 0
        return self.y2 - self.y1

    def size(self):
        return NVector(self.width, self.height)


## @ingroup Lottie
class ShapeElement(VisualObject):
    """!
    Base class for all elements of ShapeLayer and Group
    """
    _props = [
        LottieProp("hidden", "hd", bool, False),
        LottieProp("type", "ty", str, False),
        LottieProp("blend_mode", "bm", BlendMode, False),
        LottieProp("property_index", "ix", int, False),
        LottieProp("css_class", "cl", str, False),
        LottieProp("layer_xml_id", "ln", str, False),

    ]
    ## %Shape type.
    type = None
    _shape_classses = None

    def __init__(self):
        super().__init__()
        ## Property index
        self.property_index = None
        ## Hide element
        self.hidden = None
        ## Blend mode
        self.blend_mode = None
        ## CSS class used by the SVG renderer
        self.css_class = None
        ## `id` attribute used by the SVG renderer
        self.layer_xml_id = None

    def bounding_box(self, time=0):
        """!
        Bounding box of the shape element at the given time
        """
        return BoundingBox()

    @classmethod
    def _load_get_class(cls, lottiedict):
        if not ShapeElement._shape_classses:
            ShapeElement._shape_classses = {}
            ShapeElement._load_sub(ShapeElement._shape_classses)
        return ShapeElement._shape_classses[lottiedict["ty"]]

    @classmethod
    def _load_sub(cls, dict):
        for sc in cls.__subclasses__():
            if sc.type and sc.type not in dict:
                dict[sc.type] = sc
            sc._load_sub(dict)

    def __str__(self):
        return self.name or super().__str__()


#ingroup Lottie
class ShapeDirection(LottieEnum):
    Undefined = 0
    ## Usually clockwise
    Normal = 1
    ## Usually counter clockwise
    Reversed = 3


## @ingroup Lottie
class Shape(ShapeElement):
    """!
    Drawable shape
    """
    _props = [
        LottieProp("direction", "d", ShapeDirection, False),
    ]

    def __init__(self):
        ShapeElement.__init__(self)
        ## After Effect's Direction. Direction how the shape is drawn. Used for trim path for example.
        self.direction = ShapeDirection.Normal

    def to_bezier(self):
        """!
        Returns a Path corresponding to this Shape
        """
        raise NotImplementedError()


## @ingroup Lottie
class Rect(Shape):
    """!
    A simple rectangle shape
    """
    _props = [
        LottieProp("position", "p", PositionValue, False),
        LottieProp("size", "s", MultiDimensional, False),
        LottieProp("rounded", "r", Value, False),
    ]
    ## %Shape type.
    type = "rc"

    def __init__(self, pos=None, size=None, rounded=0):
        Shape.__init__(self)
        ## Rect's position
        self.position = PositionValue(pos or NVector(0, 0))
        ## Rect's size
        self.size = MultiDimensional(size or NVector(0, 0))
        ## Rect's rounded corners
        self.rounded = Value(rounded)

    def bounding_box(self, time=0):
        pos = self.position.get_value(time)
        sz = self.size.get_value(time)

        return BoundingBox(
            pos[0] - sz[0]/2,
            pos[1] - sz[1]/2,
            pos[0] + sz[0]/2,
            pos[1] + sz[1]/2,
        )

    def to_bezier(self):
        """!
        Returns a Shape corresponding to this rect
        """
        shape = Path()
        kft = set()
        if self.position.animated:
            kft |= set(kf.time for kf in self.position.keyframes)
        if self.size.animated:
            kft |= set(kf.time for kf in self.size.keyframes)
        if self.rounded.animated:
            kft |= set(kf.time for kf in self.rounded.keyframes)
        if not kft:
            shape.shape.value = self._bezier_t(0)
        else:
            for time in sorted(kft):
                shape.shape.add_keyframe(time, self._bezier_t(time))
        return shape

    def _bezier_t(self, time):
        bezier = Bezier()
        bb = self.bounding_box(time)
        rounded = self.rounded.get_value(time)
        tl = NVector(bb.x1, bb.y1)
        tr = NVector(bb.x2, bb.y1)
        br = NVector(bb.x2, bb.y2)
        bl = NVector(bb.x1, bb.y2)

        if not self.rounded.animated and rounded == 0:
            bezier.add_point(tl)
            bezier.add_point(tr)
            bezier.add_point(br)
            bezier.add_point(bl)
        else:
            hh = NVector(rounded/2, 0)
            vh = NVector(0, rounded/2)
            hd = NVector(rounded, 0)
            vd = NVector(0, rounded)
            bezier.add_point(tl+vd, outp=-vh)
            bezier.add_point(tl+hd, -hh)
            bezier.add_point(tr-hd, outp=hh)
            bezier.add_point(tr+vd, -vh)
            bezier.add_point(br-vd, outp=vh)
            bezier.add_point(br-hd, hh)
            bezier.add_point(bl+hd, outp=-hh)
            bezier.add_point(bl-vd, vh)

        bezier.close()
        return bezier


## @ingroup Lottie
class StarType(LottieEnum):
    Star = 1
    Polygon = 2


## @ingroup Lottie
class Star(Shape):
    """!
    Star shape
    """
    _props = [
        LottieProp("position", "p", PositionValue, False),
        LottieProp("inner_radius", "ir", Value, False),
        LottieProp("inner_roundness", "is", Value, False),
        LottieProp("outer_radius", "or", Value, False),
        LottieProp("outer_roundness", "os", Value, False),
        LottieProp("rotation", "r", Value, False),
        LottieProp("points", "pt", Value, False),
        LottieProp("star_type", "sy", StarType, False),
    ]
    ## %Shape type.
    type = "sr"

    def __init__(self):
        Shape.__init__(self)
        ## Star's position
        self.position = PositionValue(NVector(0, 0))
        ## Star's inner radius. (Star only)
        self.inner_radius = Value()
        ## Star's inner roundness. (Star only)
        self.inner_roundness = Value()
        ## Star's outer radius.
        self.outer_radius = Value()
        ## Star's outer roundness.
        self.outer_roundness = Value()
        ## Star's rotation.
        self.rotation = Value()
        ## Star's number of points.
        self.points = Value(5)
        ## Star's type. Polygon or Star.
        self.star_type = StarType.Star

    def bounding_box(self, time=0):
        pos = self.position.get_value(time)
        r = self.outer_radius.get_value(time)

        return BoundingBox(
            pos[0] - r,
            pos[1] - r,
            pos[0] + r,
            pos[1] + r,
        )

    def to_bezier(self):
        """!
        Returns a Shape corresponding to this star
        """
        shape = Path()
        kft = set()
        if self.position.animated:
            kft |= set(kf.time for kf in self.position.keyframes)
        if self.inner_radius.animated:
            kft |= set(kf.time for kf in self.inner_radius.keyframes)
        if self.inner_roundness.animated:
            kft |= set(kf.time for kf in self.inner_roundness.keyframes)
        if self.points.animated:
            kft |= set(kf.time for kf in self.points.keyframes)
        if self.rotation.animated:
            kft |= set(kf.time for kf in self.rotation.keyframes)
        # TODO inner_roundness / outer_roundness
        if not kft:
            shape.shape.value = self._bezier_t(0)
        else:
            for time in sorted(kft):
                shape.shape.add_keyframe(time, self._bezier_t(time))
        return shape

    def _bezier_t(self, time):
        bezier = Bezier()
        pos = self.position.get_value(time)
        r1 = self.inner_radius.get_value(time)
        r2 = self.outer_radius.get_value(time)
        rot = -(self.rotation.get_value(time)) * math.pi / 180 + math.pi
        p = self.points.get_value(time)
        halfd = -math.pi / p

        for i in range(int(p)):
            main_angle = rot + i * halfd * 2
            dx = r2 * math.sin(main_angle)
            dy = r2 * math.cos(main_angle)
            bezier.add_point(NVector(pos.x + dx, pos.y + dy))

            if self.star_type == StarType.Star:
                dx = r1 * math.sin(main_angle+halfd)
                dy = r1 * math.cos(main_angle+halfd)
                bezier.add_point(NVector(pos.x + dx, pos.y + dy))

        bezier.close()
        return bezier


## @ingroup Lottie
class Ellipse(Shape):
    """!
    Ellipse shape
    """
    _props = [
        LottieProp("position", "p", PositionValue, False),
        LottieProp("size", "s", MultiDimensional, False),
    ]
    ## %Shape type.
    type = "el"

    def __init__(self, position=None, size=None):
        Shape.__init__(self)
        ## Ellipse's position
        self.position = MultiDimensional(position or NVector(0, 0))
        ## Ellipse's size
        self.size = MultiDimensional(size or NVector(0, 0))

    def bounding_box(self, time=0):
        pos = self.position.get_value(time)
        sz = self.size.get_value(time)

        return BoundingBox(
            pos[0] - sz[0]/2,
            pos[1] - sz[1]/2,
            pos[0] + sz[0]/2,
            pos[1] + sz[1]/2,
        )

    def to_bezier(self):
        """!
        Returns a Shape corresponding to this ellipse
        """
        shape = Path()
        kft = set()
        if self.position.animated:
            kft |= set(kf.time for kf in self.position.keyframes)
        if self.size.animated:
            kft |= set(kf.time for kf in self.size.keyframes)
        if not kft:
            shape.shape.value = self._bezier_t(0)
        else:
            for time in sorted(kft):
                shape.shape.add_keyframe(time, self._bezier_t(time))
        return shape

    def _bezier_t(self, time):
        from ..utils.ellipse import Ellipse as EllipseConverter

        position = self.position.get_value(time)
        radii = self.size.get_value(time) / 2
        el = EllipseConverter(position, radii, 0)
        bezier = el.to_bezier(0, math.pi*2)
        bezier.close()
        return bezier


## @ingroup Lottie
class Path(Shape):
    """!
    Animatable Bezier curve
    """
    _props = [
        LottieProp("shape", "ks", ShapeProperty, False),
        LottieProp("index", "ind", int, False),
    ]
    ## %Shape type.
    type = "sh"

    def __init__(self, bezier=None):
        Shape.__init__(self)
        ## Shape's vertices
        self.shape = ShapeProperty(bezier or Bezier())
        ## @todo Index?
        self.index = None

    def bounding_box(self, time=0):
        pos = self.shape.get_value(time)

        bb = BoundingBox()
        for v, i, o in zip(pos.vertices, pos.in_tangents, pos.out_tangents):
            bb.include(*v)
            bb.include(*(v+i))
            bb.include(*(v+o))

        return bb

    def to_bezier(self):
        return self.clone()


## @ingroup Lottie
class Group(ShapeElement):
    """!
    ShapeElement that can contain other shapes
    @note Shapes inside the same group will create "holes" in other shapes
    """
    _props = [
        LottieProp("number_of_properties", "np", float, False),
        LottieProp("shapes", "it", ShapeElement, True),
    ]
    ## %Shape type.
    type = "gr"

    def __init__(self):
        ShapeElement.__init__(self)
        ## Group number of properties. Used for expressions.
        self.number_of_properties = None
        ## Group list of items
        self.shapes = [TransformShape()]

    @property
    def transform(self):
        return self.shapes[-1]

    def bounding_box(self, time=0):
        bb = BoundingBox()
        for v in self.shapes:
            bb.expand(v.bounding_box(time))

        if not bb.isnull():
            mat = self.transform.to_matrix(time)
            points = [
                mat.apply(NVector(bb.x1, bb.y1)),
                mat.apply(NVector(bb.x1, bb.y2)),
                mat.apply(NVector(bb.x2, bb.y2)),
                mat.apply(NVector(bb.x2, bb.y1)),
            ]
            x1 = min(p.x for p in points)
            x2 = max(p.x for p in points)
            y1 = min(p.y for p in points)
            y2 = max(p.y for p in points)
            return BoundingBox(x1, y1, x2, y2)
        return bb

    def add_shape(self, shape):
        self.shapes.insert(-1, shape)
        return shape

    def insert_shape(self, index, shape):
        self.shapes.insert(index, shape)
        return shape

    @classmethod
    def load(cls, lottiedict):
        object = ShapeElement.load(lottiedict)

        shapes = []
        transform = None
        for obj in object.shapes:
            if isinstance(obj, TransformShape):
                if not transform:
                    transform = obj
            else:
                shapes.append(obj)

        object.shapes = shapes
        object.shapes.append(transform)
        return object


## @ingroup Lottie
class FillRule(LottieEnum):
    NonZero = 1
    EvenOdd = 2


## @ingroup Lottie
class Fill(ShapeElement):
    """!
    Solid fill color
    """
    _props = [
        LottieProp("opacity", "o", Value, False),
        LottieProp("color", "c", ColorValue, False),
        LottieProp("fill_rule", "r", FillRule, False),
    ]
    ## %Shape type.
    type = "fl"

    def __init__(self, color=None):
        ShapeElement.__init__(self)
        ## Fill Opacity
        self.opacity = Value(100)
        ## Fill Color
        self.color = ColorValue(color or Color(1, 1, 1))
        ## Fill rule
        self.fill_rule = None


## @ingroup Lottie
class GradientType(LottieEnum):
    Linear = 1
    Radial = 2


## @ingroup Lottie
class Gradient(LottieObject):
    _props = [
        LottieProp("start_point", "s", MultiDimensional, False),
        LottieProp("end_point", "e", MultiDimensional, False),
        LottieProp("gradient_type", "t", GradientType, False),
        LottieProp("highlight_length", "h", Value, False),
        LottieProp("highlight_angle", "a", Value, False),
        LottieProp("colors", "g", GradientColors, False),
    ]

    def __init__(self, colors=[]):
        ## Fill Opacity
        self.opacity = Value(100)
        ## Gradient Start Point
        self.start_point = MultiDimensional(NVector(0, 0))
        ## Gradient End Point
        self.end_point = MultiDimensional(NVector(0, 0))
        ## Gradient Type
        self.gradient_type = GradientType.Linear
        ## Gradient Highlight Length. Only if type is Radial
        self.highlight_length = Value()
        ## Highlight Angle. Only if type is Radial
        self.highlight_angle = Value()
        ## Gradient Colors
        self.colors = GradientColors(colors)


## @ingroup Lottie
class GradientFill(ShapeElement, Gradient):
    """!
    Gradient fill
    """
    _props = [
        LottieProp("opacity", "o", Value, False),
        LottieProp("fill_rule", "r", FillRule, False),
    ]
    ## %Shape type.
    type = "gf"

    def __init__(self, colors=[]):
        ShapeElement.__init__(self)
        Gradient.__init__(self, colors)
        ## Fill Opacity
        self.opacity = Value(100)
        ## Fill rule
        self.fill_rule = None


## @ingroup Lottie
class LineJoin(LottieEnum):
    Miter = 1
    Round = 2
    Bevel = 3


## @ingroup Lottie
class LineCap(LottieEnum):
    Butt = 1
    Round = 2
    Square = 3


## @ingroup Lottie
class StrokeDashType(LottieEnum):
    Dash = "d"
    Gap = "g"
    Offset = "o"


## @ingroup Lottie
class StrokeDash(VisualObject):
    _props = [
        LottieProp("type", "n", StrokeDashType, False),
        LottieProp("length", "v", Value, False),
    ]

    def __init__(self, length=0, type=StrokeDashType.Dash):
        super().__init__()
        self.name = type.name.lower()
        self.type = type
        self.length = Value(length)

    def __str__(self):
        return self.name or super().__str__()


## @ingroup Lottie
class BaseStroke(LottieObject):
    _props = [
        LottieProp("line_cap", "lc", LineCap, False),
        LottieProp("line_join", "lj", LineJoin, False),
        LottieProp("miter_limit", "ml", float, False),
        LottieProp("animated_miter_limit", "ml2", Value, False),
        LottieProp("opacity", "o", Value, False),
        LottieProp("width", "w", Value, False),
        LottieProp("dashes", "d", StrokeDash, True),
    ]

    def __init__(self, width=1):
        ## Stroke Line Cap
        self.line_cap = LineCap.Round
        ## Stroke Line Join
        self.line_join = LineJoin.Round
        ## Stroke Miter Limit. Only if Line Join is set to Miter.
        self.miter_limit = 0
        ## Animatable alternative to ml
        self.animated_miter_limit = None
        ## Stroke Opacity
        self.opacity = Value(100)
        ## Stroke Width
        self.width = Value(width)
        ## Dashes
        self.dashes = None


## @ingroup Lottie
class Stroke(ShapeElement, BaseStroke):
    """!
    Solid stroke
    """
    _props = [
        LottieProp("color", "c", ColorValue, False),
    ]
    ## %Shape type.
    type = "st"

    def __init__(self, color=None, width=1):
        ShapeElement.__init__(self)
        BaseStroke.__init__(self, width)
        ## Stroke Color
        self.color = ColorValue(color or Color(0, 0, 0))


## @ingroup Lottie
class GradientStroke(ShapeElement, BaseStroke, Gradient):
    """!
    Gradient stroke
    """
    ## %Shape type.
    type = "gs"

    def __init__(self, stroke_width=1):
        ShapeElement.__init__(self)
        BaseStroke.__init__(self, stroke_width)
        Gradient.__init__(self)

    def bounding_box(self, time=0):
        return BoundingBox()


## @ingroup Lottie
class TransformShape(ShapeElement, Transform):
    """!
    Group transform
    """
    ## %Shape type.
    type = "tr"

    def __init__(self):
        ShapeElement.__init__(self)
        Transform.__init__(self)
        self.anchor_point = MultiDimensional(NVector(0, 0))


## @ingroup Lottie
class Composite(LottieEnum):
    Above = 1
    Below = 2


## @ingroup Lottie
class RepeaterTransform(Transform):
    _props = [
        LottieProp("start_opacity", "so", Value, False),
        LottieProp("end_opacity", "eo", Value, False),
    ]

    def __init__(self):
        Transform.__init__(self)
        self.start_opacity = Value(100)
        self.end_opacity = Value(100)


## @ingroup Lottie
class Modifier(ShapeElement):
    pass


## @ingroup Lottie
class TrimMultipleShapes(LottieEnum):
    Individually = 1
    Simultaneously = 2


## @ingroup Lottie
## @todo Implement SIF Export
class Trim(Modifier):
    """
    Trims shapes into a segment
    """
    _props = [
        LottieProp("start", "s", Value, False),
        LottieProp("end", "e", Value, False),
        LottieProp("offset", "o", Value, False),
        LottieProp("multiple", "m", TrimMultipleShapes, False),
    ]
    ## %Shape type.
    type = "tm"

    def __init__(self):
        ShapeElement.__init__(self)
        ## Start of the segment, as a percentage
        self.start = Value(0)
        ## End of the segment, as a percentage
        self.end = Value(100)
        ## start/end offset, as an angle (0, 360)
        self.offset = Value(0)
        ## @todo?
        self.multiple = None


## @ingroup Lottie
class Repeater(Modifier):
    """
    Duplicates previous shapes in a group
    """
    _props = [
        LottieProp("copies", "c", Value, False),
        LottieProp("offset", "o", Value, False),
        LottieProp("composite", "m", Composite, False),
        LottieProp("transform", "tr", RepeaterTransform, False),
    ]
    ## %Shape type.
    type = "rp"

    def __init__(self, copies=1):
        Modifier.__init__(self)
        ## Number of Copies
        self.copies = Value(copies)
        ## Offset of Copies
        self.offset = Value()
        ## Composite of copies
        self.composite = Composite.Above
        ## Transform values for each repeater copy
        self.transform = RepeaterTransform()


## @ingroup Lottie
## @todo Implement SIF Export
class RoundedCorners(Modifier):
    """
    Rounds corners of other shapes
    """
    _props = [
        LottieProp("radius", "r", Value, False),
    ]
    ## %Shape type.
    type = "rd"

    def __init__(self, radius=0):
        Modifier.__init__(self)
        ## Rounded Corner Radius
        self.radius = Value(radius)


#ingroup Lottie
class MergeMode(LottieEnum):
    """!
    Boolean operation on shapes
    """
    Normal = 1
    Add = 2
    Subtract = 3
    Intersect = 4
    ExcludeIntersections = 5


## @ingroup Lottie
## @note marked as unsupported by lottie
class Merge(ShapeElement):
    _props = [
        LottieProp("merge_mode", "mm", MergeMode, False),
    ]
    ## %Shape type.
    type = "mm"

    def __init__(self):
        ShapeElement.__init__(self)
        ## Merge Mode
        self.merge_mode = MergeMode.Normal


## @ingroup Lottie
## @note marked as unsupported by lottie
class Twist(ShapeElement):
    _props = [
        LottieProp("angle", "a", Value, False),
        LottieProp("center", "c", MultiDimensional, False),
    ]
    ## %Shape type.
    type = "tw"

    def __init__(self):
        ShapeElement.__init__(self)
        self.angle = Value(0)
        self.center = MultiDimensional(NVector(0, 0))


## @ingroup Lottie
class PuckerBloat(ShapeElement):
    """
    Interpolates the shape with its center point and bezier tangents with the opposite direction
    """
    _props = [
        LottieProp("amount", "a", Value, False),
    ]
    ## %Shape type.
    type = "pb"

    def __init__(self):
        ShapeElement.__init__(self)
        self.amount = Value(0)


#ingroup Lottie
class ZigZag(ShapeElement):
    """!
    Changes the edges of affected shapes into a series of peaks and valleys of uniform size
    """
    _props = [
        LottieProp("shape_type", "ty", str, False),
        LottieProp("frequency", "r", Value, False),
        LottieProp("amplitude", "s", Value, False),
        LottieProp("point_type", "pt", Value, False),
    ]
    ## %Shape type.
    type = "zz"

    def __init__(self):
        super().__init__()

        self.shape_type = "zz"
        ## Number of ridges per segment
        self.frequency = Value(0)
        ## Distance between peaks and troughs
        self.amplitude = Value(0)
        ## Point type (1 = corner, 2 = smooth)
        self.point_type = Value(1)


#ingroup Lottie
class OffsetPath(ShapeElement):
    """!
    Interpolates the shape with its center point and bezier tangents with the opposite direction
    """
    _props = [
        LottieProp("shape_type", "ty", str, False),
        LottieProp("amount", "a", Value, False),
        LottieProp("line_join", "lj", LineJoin, False),
        LottieProp("miter_limit", "ml", Value, False),
    ]
    ## %Shape type.
    type = "op"

    def __init__(self):
        super().__init__()

        self.amount = Value(0)
        self.line_join = LineJoin.Round
        self.miter_limit = Value(0)
