import math
from ... import objects
from ...objects import easing
from . import api, ast
from ... import NVector, PolarVector

try:
    from ...utils import font
    has_font = True
except ImportError:
    has_font = False


def convert(canvas: api.Canvas):
    return Converter().convert(canvas)


class Converter:
    def __init__(self):
        pass

    def _animated(self, sifval):
        return isinstance(sifval, ast.SifAnimated)

    def convert(self, canvas: api.Canvas):
        self.canvas = canvas
        self.animation = objects.Animation(
            self._time(canvas.end_time),
            canvas.fps
        )
        self.animation.in_point = self._time(canvas.begin_time)
        self.animation.width = canvas.width
        self.animation.height = canvas.height
        self.view_p1 = NVector(canvas.view_box[0], canvas.view_box[1])
        self.view_p2 = NVector(canvas.view_box[2], canvas.view_box[3])
        self.target_size = NVector(canvas.width, canvas.height)
        self.shape_layer = self.animation.add_layer(objects.ShapeLayer())
        self.gamma = NVector(canvas.gamma_r, canvas.gamma_g, canvas.gamma_b)
        self._process_layers(canvas.layers, self.shape_layer)
        return self.animation

    def _time(self, t: api.FrameTime):
        return self.canvas.time_to_frames(t)

    def _process_layers(self, layers, parent):
        old_gamma = self.gamma

        for layer in reversed(layers):
            if not layer.active:
                continue
            elif isinstance(layer, api.GroupLayerBase):
                parent.add_shape(self._convert_group(layer))
            elif isinstance(layer, api.RectangleLayer):
                parent.add_shape(self._convert_fill(layer, self._convert_rect))
            elif isinstance(layer, api.CircleLayer):
                parent.add_shape(self._convert_fill(layer, self._convert_circle))
            elif isinstance(layer, api.StarLayer):
                parent.add_shape(self._convert_fill(layer, self._convert_star))
            elif isinstance(layer, api.PolygonLayer):
                parent.add_shape(self._convert_fill(layer, self._convert_polygon))
            elif isinstance(layer, api.RegionLayer):
                parent.add_shape(self._convert_fill(layer, self._convert_bline))
            elif isinstance(layer, api.AbstractOutline):
                parent.add_shape(self._convert_outline(layer, self._convert_bline))
            elif isinstance(layer, api.GradientLayer):
                parent.add_shape(self._convert_gradient(layer, parent))
            elif isinstance(layer, api.TransformDown):
                shape = self._convert_transform_down(layer)
                parent.add_shape(shape)
                parent = shape
            elif isinstance(layer, api.TextLayer):
                if has_font:
                    parent.add_shape(self._convert_fill(layer, self._convert_text))
            elif isinstance(layer, api.ColorCorrectLayer):
                self.gamma = self.gamma * NVector(layer.gamma.value, layer.gamma.value, layer.gamma.value)

        self.gamma = old_gamma

    def _convert_group(self, layer: api.GroupLayer):
        shape = objects.Group()
        self._set_name(shape, layer)
        shape.transform.anchor_point = self._adjust_coords(self._convert_vector(layer.origin))
        self._convert_transform(layer.transformation, shape.transform)
        self._process_layers(layer.layers, shape)
        shape.transform.opacity = self._adjust_animated(
            self._convert_scalar(layer.amount),
            lambda x: x*100
        )
        return shape

    def _convert_transform(self, sif_transform: api.AbstractTransform, lottie_transform: objects.Transform):
        if isinstance(sif_transform, api.BoneLinkTransform):
            base_transform = sif_transform.base_value
        else:
            base_transform = sif_transform

        position = self._adjust_coords(self._convert_vector(base_transform.offset))
        rotation = self._adjust_angle(self._convert_scalar(base_transform.angle))
        scale = self._adjust_animated(
            self._convert_vector(base_transform.scale),
            lambda x: x * 100
        )

        lottie_transform.skew_axis = self._adjust_angle(self._convert_scalar(base_transform.skew_angle))

        if isinstance(sif_transform, api.BoneLinkTransform):
            lottie_transform.position = position
            lottie_transform.rotation = rotation
            lottie_transform.scale = scale
            #bone = sif_transform.bone
            #b_pos = self._adjust_coords(self._convert_vector(bone.origin))
            #old_anchor = lottie_transform.anchor_point

            #if sif_transform.translate:
                #self._mix_animations_into(
                    #[position, b_pos, old_anchor],
                    #lottie_transform.position,
                    #lambda base_p, bone_p, anchor: (anchor-self.target_size/2)/2+self.target_size/2
                #)
            #else:
                #lottie_transform.position = position

            #lottie_transform.anchor_point = b_pos
            #lottie_transform.anchor_point.value += NVector(100,0)

            #if sif_transform.rotate:
                #b_rot = self._convert_scalar(bone.angle)
                #self._mix_animations_into([rotation, b_rot], lottie_transform.rotation, lambda a, b: a-b)
            #else:
                #lottie_transform.rotation = rotation

            #if sif_transform.scale_y:
                #b_scale = self._convert_scalar(bone.scalelx)
                #self._mix_animations_into(
                    #scale, b_scale, lottie_transform.scale,
                    #lambda a, b: NVector(a.x, a.y * b)
                #)
            #else:
                #lottie_transform.scale = scale
        else:
            lottie_transform.position = position
            lottie_transform.rotation = rotation
            lottie_transform.scale = scale

    def _mix_animations_into(self, animations, output, mix):
        if not any(x.animated for x in animations):
            output.value = mix(*(x.value for x in animations))
        else:
            for vals in self._mix_animations(*animations):
                time = vals.pop(0)
                output.add_keyframe(time, mix(*vals))

    def _convert_fill(self, layer, converter):
        shape = objects.Group()
        self._set_name(shape, layer)
        shape.add_shape(converter(layer))
        if layer.invert.value:
            shape.add_shape(objects.Rect(self.target_size/2, self.target_size))

        fill = objects.Fill()
        fill.color = self._convert_color(layer.color)
        fill.opacity = self._adjust_animated(
            self._convert_scalar(layer.amount),
            lambda x: x * 100
        )
        shape.add_shape(fill)
        return shape

    def _convert_linecap(self, lc: api.LineCap):
        if lc == api.LineCap.Rounded:
            return objects.LineCap.Round
        if lc == api.LineCap.Squared:
            return objects.LineCap.Square
        return objects.LineCap.Butt

    def _convert_cusp(self, lc: api.CuspStyle):
        if lc == api.CuspStyle.Miter:
            return objects.LineJoin.Miter
        if lc == api.CuspStyle.Bevel:
            return objects.LineJoin.Bevel
        return objects.LineJoin.Round

    def _convert_outline(self, layer: api.AbstractOutline, converter):
        shape = objects.Group()
        self._set_name(shape, layer)
        shape.add_shape(converter(layer))
        stroke = objects.Stroke()
        stroke.color = self._convert_color(layer.color)
        stroke.line_cap = self._convert_linecap(layer.start_tip)
        stroke.line_join = self._convert_cusp(layer.cusp_type)
        stroke.width = self._adjust_scalar(self._convert_scalar(layer.width))
        shape.add_shape(stroke)
        return shape

    def _convert_rect(self, layer: api.RectangleLayer):
        rect = objects.Rect()
        p1 = self._adjust_coords(self._convert_vector(layer.point1))
        p2 = self._adjust_coords(self._convert_vector(layer.point2))
        if p1.animated or p2.animated:
            for time, p1v, p2v in self._mix_animations(p1, p2):
                rect.position.add_keyframe(time, (p1v + p2v) / 2)
                rect.size.add_keyframe(time, abs(p2v - p1v))
            pass
        else:
            rect.position.value = (p1.value + p2.value) / 2
            rect.size.value = abs(p2.value - p1.value)
        rect.rounded = self._adjust_scalar(self._convert_scalar(layer.bevel))
        return rect

    def _convert_circle(self, layer: api.CircleLayer):
        shape = objects.Ellipse()
        shape.position = self._adjust_coords(self._convert_vector(layer.origin))
        radius = self._adjust_scalar(self._convert_scalar(layer.radius))
        shape.size = self._adjust_add_dimension(radius, lambda x: NVector(x, x) * 2)
        return shape

    def _convert_star(self, layer: api.StarLayer):
        shape = objects.Star()
        shape.position = self._adjust_coords(self._convert_vector(layer.origin))
        shape.inner_radius = self._adjust_scalar(self._convert_scalar(layer.radius2))
        shape.outer_radius = self._adjust_scalar(self._convert_scalar(layer.radius1))
        shape.rotation = self._adjust_animated(
            self._convert_scalar(layer.angle),
            lambda x: 90-x
        )
        shape.points = self._convert_scalar(layer.points)
        if layer.regular_polygon.value:
            shape.star_type = objects.StarType.Polygon
        return shape

    def _mix_animations(self, *animatable):
        times = set()
        for v in animatable:
            self._force_animated(v)
            for kf in v.keyframes:
                times.add(kf.time)

        for time in sorted(times):
            yield [time] + [v.get_value(time) for v in animatable]

    def _force_animated(self, lottieval):
        if not lottieval.animated:
            v = lottieval.value
            lottieval.add_keyframe(0, v)
            lottieval.add_keyframe(self.animation.out_point, v)

    def _convert_easing_part(self, interp: api.Interpolation):
        if interp == api.Interpolation.Linear:
            return easing.Linear()
        return easing.Sigmoid()

    def _convert_easing(self, start: api.Interpolation, end: api.Interpolation):
        if api.Interpolation.Constant in (start, end):
            return easing.Hold()
        if start == end:
            return self._convert_easing_part(start)
        return easing.Split(self._convert_easing_part(start), self._convert_easing_part(end))

    def _convert_animatable(self, v: ast.SifAstNode, lot: objects.properties.AnimatableMixin):
        if self._animated(v):
            if len(v.keyframes) == 1:
                lot.value = self._convert_ast_value(v.keyframes[0].value)
            else:
                for i, kf in enumerate(v.keyframes):
                    if i+1 < len(v.keyframes):
                        start = kf.after
                        end = v.keyframes[i+1].before
                        ease = self._convert_easing(start, end)
                    else:
                        ease = easing.Linear()

                    lot.add_keyframe(self._time(kf.time), self._convert_ast_value(kf.value), ease)
        else:
            lot.value = self._convert_ast_value(v)
        return lot

    def _convert_ast_value(self, v):
        if isinstance(v, ast.SifRadialComposite):
            return self._polar(v.radius.value, v.theta.value, 1)
        elif isinstance(v, ast.SifValue):
            return v.value
        elif isinstance(v, ast.SifVectorComposite):
            return NVector(v.x.value, v.y.value)
        else:
            return v

    def _converted_vector_values(self, v):
        if isinstance(v, ast.SifRadialComposite):
            return [self._convert_scalar(v.radius), self._convert_scalar(v.theta)]
        return self._convert_vector(v)

    def _convert_color(self, v: ast.SifAstNode):
        return self._adjust_animated(
            self._convert_animatable(v, objects.ColorValue()),
            self._color_gamma
        )

    def _convert_vector(self, v: ast.SifAstNode):
        return self._convert_animatable(v, objects.MultiDimensional())

    def _convert_scalar(self, v: ast.SifAstNode):
        return self._convert_animatable(v, objects.Value())

    def _color_gamma(self, color):
        color = color.clone()
        for i in range(3):
            color[i] = color[i] ** (1/self.gamma[i])
        return color

    def _adjust_animated(self, lottieval, transform):
        if lottieval.animated:
            for kf in lottieval.keyframes:
                if kf.start is not None:
                    kf.start = transform(kf.start)
                if kf.end is not None:
                    kf.end = transform(kf.end)
        else:
            lottieval.value = transform(lottieval.value)
        return lottieval

    def _adjust_scalar(self, lottieval: objects.Value):
        return self._adjust_animated(lottieval, self._scalar_mult)

    def _adjust_angle(self, lottieval: objects.Value):
        return self._adjust_animated(lottieval, lambda x: -x)

    def _adjust_add_dimension(self, lottieval, transform):
        to_val = objects.MultiDimensional()
        to_val.animated = lottieval.animated
        if lottieval.animated:
            to_val.keyframes = []
            for kf in lottieval.keyframes:
                if kf.start is not None:
                    kf.start = transform(kf.start[0])
                if kf.end is not None:
                    kf.end = transform(kf.end[0])
                to_val.keyframes.append(kf)
        else:
            to_val.value = transform(lottieval.value)
        return to_val

    def _scalar_mult(self, x):
        return x * 60

    def _adjust_coords(self, lottieval: objects.MultiDimensional):
        return self._adjust_animated(lottieval, self._coord)

    def _coord(self, val: NVector):
        return NVector(
            self.target_size.x * (val.x / (self.view_p2.x - self.view_p1.x) + 0.5),
            self.target_size.y * (val.y / (self.view_p2.y - self.view_p1.y) + 0.5),
        )

    def _convert_polygon(self, layer: api.PolygonLayer):
        lot = objects.Path()
        animatables = [self._convert_vector(layer.origin)] + [
            self._convert_vector(p)
            for p in layer.points
        ]
        animated = any(x.animated for x in animatables)
        if not animated:
            lot.shape.value = self._polygon([x.value for x in animatables[1:]], animatables[0].value)
        else:
            for values in self._mix_animations(*animatables):
                time = values[0]
                origin = values[1]
                points = values[2:]
                lot.shape.add_keyframe(time, self._polygon(points, origin))
        return lot

    def _polygon(self, points, origin):
        bezier = objects.Bezier()
        bezier.closed = True
        for point in points:
            bezier.add_point(self._coord(point+origin))
        return bezier

    def _convert_bline(self, layer: api.AbstractOutline):
        lot = objects.Path()
        closed = layer.bline.loop
        animatables = [
            self._convert_vector(layer.origin)
        ]
        for p in layer.bline.points:
            animatables += [
                self._convert_vector(p.point),
                self._convert_scalar(p.t1.radius) if hasattr(p.t1, "radius") else objects.Value(0),
                self._convert_scalar(p.t1.theta) if hasattr(p.t1, "radius") else objects.Value(0),
                self._convert_scalar(p.t2.radius) if hasattr(p.t2, "radius") else objects.Value(0),
                self._convert_scalar(p.t2.theta) if hasattr(p.t2, "radius") else objects.Value(0)
            ]
        animated = any(x.animated for x in animatables)
        if not animated:
            lot.shape.value = self._bezier(
                closed, [x.value for x in animatables[1:]], animatables[0].value, layer.bline.points
            )
        else:
            for values in self._mix_animations(*animatables):
                time = values[0]
                origin = values[1]
                values = values[2:]
                lot.shape.add_keyframe(time, self._bezier(closed, values, origin, layer.bline.points))
        return lot

    def _bezier(self, closed, values, origin, points):
        chunk_size = 5
        bezier = objects.Bezier()
        bezier.closed = closed
        for i in range(0, len(values), chunk_size):
            point, r1, a1, r2, a2 = values[i:i+chunk_size]
            sifvert = point+origin
            vert = self._coord(sifvert)
            if not points[i//chunk_size].split_radius.value:
                r2 = r1
            if not points[i//chunk_size].split_angle.value:
                a2 = a1
            t1 = self._coord(sifvert + self._polar(r1, a1, 1)) - vert
            t2 = self._coord(sifvert + self._polar(r2, a2, 2)) - vert
            bezier.add_point(vert, t1, t2)
        return bezier

    def _polar(self, radius, angle, dir):
        offset_angle = 0
        if dir == 1:
            offset_angle += 180
        return PolarVector(radius/3, (angle+offset_angle) * math.pi / 180)

    def _convert_transform_down(self, tl: api.TransformDown):
        group = objects.Group()
        self._set_name(group, tl)

        if isinstance(tl, api.TranslateLayer):
            group.transform.anchor_point.value = self.target_size / 2
            group.transform.position = self._adjust_coords(self._convert_vector(tl.origin))
        elif isinstance(tl, api.RotateLayer):
            group.transform.anchor_point = self._adjust_coords(self._convert_vector(tl.origin))
            group.transform.position = group.transform.anchor_point.clone()
            group.transform.rotation = self._adjust_angle(self._convert_scalar(tl.amount))
        elif isinstance(tl, api.ScaleLayer):
            group.transform.anchor_point = self._adjust_coords(self._convert_vector(tl.center))
            group.transform.position = group.transform.anchor_point.clone()
            group.transform.scale = self._adjust_add_dimension(
                self._convert_scalar(tl.amount),
                self._zoom_to_scale
            )

        return group

    def _zoom_to_scale(self, value):
        zoom = math.e ** value * 100
        return NVector(zoom, zoom)

    def _set_name(self, lottie, sif):
        lottie.name = sif.desc if sif.desc is not None else sif.__class__.__name__

    def _convert_gradient(self, layer: api.GradientLayer, parent):
        group = objects.Group()

        parent_shapes = parent.shapes
        parent.shapes = []
        if isinstance(parent, objects.Group):
            parent.shapes.append(parent_shapes[-1])

        self._gradient_gather_shapes(parent_shapes, group)

        gradient = objects.GradientFill()
        self._set_name(gradient, layer)
        group.add_shape(gradient)
        gradient.colors = self._convert_gradient_stops(layer.gradient)
        gradient.opacity = self._adjust_animated(
            self._convert_scalar(layer.amount),
            lambda x: x * 100
        )

        if isinstance(layer, api.LinearGradient):
            gradient.start_point = self._adjust_coords(self._convert_vector(layer.p1))
            gradient.end_point = self._adjust_coords(self._convert_vector(layer.p2))
            gradient.gradient_type = objects.GradientType.Linear
        elif isinstance(layer, api.RadialGradient):
            gradient.gradient_type = objects.GradientType.Radial
            gradient.start_point = self._adjust_coords(self._convert_vector(layer.center))
            radius = self._adjust_animated(self._convert_scalar(layer.radius), lambda x: x*45)
            if not radius.animated and not gradient.start_point.animated:
                gradient.end_point.value = gradient.start_point.value + NVector(radius.value, radius.value)
            else:
                for time, c, r in self._mix_animations(gradient.start_point.clone(), radius):
                    gradient.end_point.add_keyframe(time, c + NVector(r + r))

        return group

    def _gradient_gather_shapes(self, shapes, output: objects.Group):
        for shape in shapes:
            if isinstance(shape, objects.Shape):
                output.add_shape(shape)
            elif isinstance(shape, objects.Group):
                self._gradient_gather_shapes(shape.shapes, output)

    def _convert_gradient_stops(self, sif_gradient):
        stops = objects.GradientColors()
        if not self._animated(sif_gradient):
            stops.set_stops(self._flatten_gradient_colors(sif_gradient.value))
            stops.count = len(sif_gradient.value)
        else:
            # TODO easing
            for kf in sif_gradient.keyframes:
                stops.add_keyframe(self._time(kf.time), self._flatten_gradient_colors(kf.value))
                stops.count = len(kf.value)

        return stops

    def _flatten_gradient_colors(self, stops):
        return [
            (stop.pos, self._color_gamma(stop.color))
            for stop in stops
        ]

    def _convert_text(self, layer: api.TextLayer):
        shape = font.FontShape(layer.text.value, font.FontStyle(layer.family.value, 110, font.TextJustify.Center))
        shape.refresh()
        trans = shape.wrapped.transform
        trans.anchor_point.value = shape.wrapped.bounding_box().center()
        trans.anchor_point.value.x /= 2
        trans.position = self._adjust_coords(self._convert_vector(layer.origin))
        trans.scale = self._adjust_animated(
            self._convert_vector(layer.size),
            lambda v: v * 100
        )
        return shape
