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


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(LottieObject):
    """!
    Base class for all elements of ShapeLayer and Group
    """
    _props = [
        #LottieProp("match_name", "mn", str, False),
        LottieProp("hidden", "hd", bool, False),
        LottieProp("name", "nm", str, False),
        LottieProp("type", "ty", str, False),
        LottieProp("property_index", "ix", int, False),
        LottieProp("bm", "bm", int, False),
    ]
    ## %Shape type.
    type = None
    _shape_classses = None

    def __init__(self):
        # After Effect's Match Name. Used for expressions.
        #self.match_name = ""

        ## After Effect's Name. Used for expressions.
        self.name = None
        ## Property index
        self.property_index = None
        ## Hide element
        self.hidden = None
        ## @todo figure out?
        self.bm = 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:
                dict[sc.type] = sc
            sc._load_sub(dict)

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


## @ingroup Lottie
class Shape(ShapeElement):
    """!
    Drawable shape
    """
    _props = [
        LottieProp("direction", "d", float, 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 = 0

    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", MultiDimensional, 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 = MultiDimensional(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", MultiDimensional, 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 = MultiDimensional(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", MultiDimensional, 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

        bezier = Bezier()
        position = self.position.get_value(time)
        radii = self.size.get_value(time) / 2

        el = EllipseConverter(position, radii, 0)
        points = el.to_bezier(0, math.pi*2)
        for point in points[1:]:
            bezier.add_point(point.vertex, point.in_tangent, point.out_tangent)

        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 in pos.vertices:
            bb.include(*v)

        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


## @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(LottieObject):
    _props = [
        LottieProp("name", "nm", str, False),
        LottieProp("type", "n", StrokeDashType, False),
        LottieProp("length", "v", Value, False),
    ]

    def __init__(self, length=0, type=StrokeDashType.Dash):
        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("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
        ## 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", MultiDimensional, 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):
    Simultaneously = 1
    Individually = 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):
        Modifier.__init__(self)
        ## Rounded Corner Radius
        self.radius = Value()


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

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


## @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))
