import random
import math
from ..nvector import NVector
from ..objects.shapes import Path
from .. import objects
from ..objects import easing
from ..objects import properties


def shake(position_prop, x_radius, y_radius, start_time, end_time, n_frames, interp=easing.Linear()):
    if not isinstance(position_prop, list):
        position_prop = [position_prop]

    n_frames = int(round(n_frames))
    frame_time = (end_time - start_time) / n_frames
    startpoints = list(map(
        lambda pp: pp.get_value(start_time),
        position_prop
    ))

    for i in range(n_frames):
        x = (random.random() * 2 - 1) * x_radius
        y = (random.random() * 2 - 1) * y_radius
        for pp, start in zip(position_prop, startpoints):
            px = start[0] + x
            py = start[1] + y
            pp.add_keyframe(start_time + i * frame_time, NVector(px, py), interp)

    for pp, start in zip(position_prop, startpoints):
        pp.add_keyframe(end_time, start, interp)


def rot_shake(rotation_prop, angles, start_time, end_time, n_frames):
    frame_time = (end_time - start_time) / n_frames
    start = rotation_prop.get_value(start_time)

    for i in range(0, n_frames):
        a = angles[i % len(angles)] * math.sin(i/n_frames * math.pi)
        rotation_prop.add_keyframe(start_time + i * frame_time, start + a)
    rotation_prop.add_keyframe(end_time, start)


def spring_pull(position_prop, point, start_time, end_time, falloff=15, oscillations=7):
    start = position_prop.get_value(start_time)
    d = start-point

    delta = (end_time - start_time) / oscillations

    for i in range(oscillations):
        time_x = i / oscillations
        factor = math.cos(time_x * math.pi * oscillations) * (1-time_x**(1/falloff))
        p = point + d * factor
        position_prop.add_keyframe(start_time + delta * i, p)

    position_prop.add_keyframe(end_time, point)


def follow_path(position_prop, bezier, start_time, end_time, n_keyframes,
                reverse=False, offset=NVector(0, 0), start_t=0, rotation_prop=None, rotation_offset=0):
    delta = (end_time - start_time) / (n_keyframes-1)
    fact = start_t
    factd = 1 / (n_keyframes-1)

    if rotation_prop:
        start_rot = rotation_prop.get_value(start_time) if rotation_offset is None else rotation_offset

    for i in range(n_keyframes):
        time = start_time + i * delta

        if fact > 1 + factd/2:
            fact -= 1
            if time != start_time:
                easing.Jump()(position_prop.keyframes[-1])
                if rotation_prop:
                    easing.Jump()(rotation_prop.keyframes[-1])

        f = 1 - fact if reverse else fact
        position_prop.add_keyframe(time, bezier.point_at(f)+offset)

        if rotation_prop:
            rotation_prop.add_keyframe(time, bezier.tangent_angle_at(f) / math.pi * 180 + start_rot)

        fact += factd


def generate_path_appear(bezier, appear_start, appear_end, n_keyframes, reverse=False):
    obj = Path()
    beziers = []
    maxp = 0

    time_delta = (appear_end - appear_start) / n_keyframes
    for i in range(n_keyframes+1):
        time = appear_start + i * time_delta
        t2 = (time - appear_start) / (appear_end - appear_start)

        if reverse:
            t2 = 1 - t2
            segment = bezier.segment(t2, 1)
            segment.reverse()
        else:
            segment = bezier.segment(0, t2)

        beziers.append(segment)
        if len(segment.vertices) > maxp:
            maxp = len(segment.vertices)

        obj.shape.add_keyframe(time, segment)

    for segment in beziers:
        deltap = maxp - len(segment.vertices)
        if deltap > 0:
            segment.vertices += [segment.vertices[-1]] * deltap
            segment.in_tangents += [NVector(0, 0)] * deltap
            segment.out_tangents += [NVector(0, 0)] * deltap

    return obj


def generate_path_disappear(bezier, disappear_start, disappear_end, n_keyframes, reverse=False):
    obj = Path()
    beziers = []
    maxp = 0

    time_delta = (disappear_end - disappear_start) / n_keyframes
    for i in range(n_keyframes+1):
        time = disappear_start + i * time_delta
        t1 = (time - disappear_start) / (disappear_end - disappear_start)
        if reverse:
            t1 = 1 - t1
            segment = bezier.segment(0, t1)
        else:
            segment = bezier.segment(1, t1)
            segment.reverse()

        beziers.append(segment)
        if len(segment.vertices) > maxp:
            maxp = len(segment.vertices)

        obj.shape.add_keyframe(time, segment)

    for segment in beziers:
        deltap = maxp - len(segment.vertices)
        if deltap > 0:
            segment.vertices += [segment.vertices[-1]] * deltap
            segment.in_tangents += [NVector(0, 0)] * deltap
            segment.out_tangents += [NVector(0, 0)] * deltap

    return obj


def generate_path_segment(bezier, appear_start, appear_end, disappear_start, disappear_end, n_keyframes, reverse=False):
    obj = Path()
    beziers = []
    maxp = 0

    # HACK: For some reason reversed works better
    if not reverse:
        bezier.reverse()

    time_delta = (appear_end - appear_start) / n_keyframes
    for i in range(n_keyframes+1):
        time = appear_start + i * time_delta
        t1 = (time - disappear_start) / (disappear_end - disappear_start)
        t2 = (time - appear_start) / (appear_end - appear_start)

        t1 = max(0, min(1, t1))
        t2 = max(0, min(1, t2))

        #if reverse:
        if True:
            t1 = 1 - t1
            t2 = 1 - t2
            segment = bezier.segment(t2, t1)
            segment.reverse()
        #else:
            #segment = bezier.segment(t1, t2)
            #segment.reverse()

        beziers.append(segment)
        if len(segment.vertices) > maxp:
            maxp = len(segment.vertices)

        obj.shape.add_keyframe(time, segment)

    for segment in beziers:
        deltap = maxp - len(segment.vertices)
        if deltap > 0:
            segment.split_self_chunks(deltap+1)

    # HACK: Restore
    if not reverse:
        bezier.reverse()
    return obj


class PointDisplacer:
    def __init__(self, time_start, time_end, n_frames):
        """!
        @param time_start   When the animation shall start
        @param time_end     When the animation shall end
        @param n_frames     Number of frames in the animation
        """
        ## When the animation shall start
        self.time_start = time_start
        ## When the animation shall end
        self.time_end = time_end
        ## Number of frames in the animation
        self.n_frames = n_frames
        ## Length of a frame
        self.time_delta = (time_end - time_start) / n_frames

    def animate_point(self, prop):
        startpos = prop.get_value(self.time_start)
        for f in range(self.n_frames+1):
            p = self._on_displace(startpos, f)
            prop.add_keyframe(self.frame_time(f), startpos+p)

    def _on_displace(self, startpos, f):
        raise NotImplementedError()

    def animate_bezier(self, prop):
        initial = prop.get_value(self.time_start)

        for f in range(self.n_frames+1):
            bezier = objects.Bezier()
            bezier.closed = initial.closed

            for pi in range(len(initial.vertices)):
                startpos = initial.vertices[pi]
                dp = self._on_displace(startpos, f)
                t1sp = initial.in_tangents[pi] + startpos
                t1fin = initial.in_tangents[pi] + self._on_displace(t1sp, f) - dp
                t2sp = initial.out_tangents[pi] + startpos
                t2fin = initial.out_tangents[pi] + self._on_displace(t2sp, f) - dp

                bezier.add_point(dp + startpos, t1fin, t2fin)

            prop.add_keyframe(self.frame_time(f), bezier)

    def frame_time(self, f):
        return f * self.time_delta + self.time_start

    def _init_lerp(self, val_from, val_to, easing):
        self._kf = properties.OffsetKeyframe(0, NVector(val_from), NVector(val_to), easing)

    def _lerp_get(self, offset):
        return self._kf.interpolated_value(offset / self.n_frames)[0]


class SineDisplacer(PointDisplacer):
    def __init__(
        self,
        wavelength,
        amplitude,
        time_start,
        time_end,
        n_frames,
        speed=1,
        axis=90,
    ):
        """!
        Displaces points as if they were following a sine wave

        @param wavelength  Distance between consecutive peaks
        @param amplitude   Distance from a peak to the original position
        @param time_start  When the animation shall start
        @param time_end    When the animation shall end
        @param n_frames    Number of keyframes to add
        @param speed       Number of peaks a point will go through in the given time
                           If negative, it will go the other way
        @param axis        Wave peak direction
        """
        super().__init__(time_start, time_end, n_frames)

        self.wavelength = wavelength
        self.amplitude = amplitude
        self.speed_f = math.pi * 2 * speed
        self.axis = axis / 180 * math.pi

    def _on_displace(self, startpos, f):
        off = -math.sin(startpos[0]/self.wavelength*math.pi*2-f*self.speed_f/self.n_frames) * self.amplitude
        return NVector(off * math.cos(self.axis), off * math.sin(self.axis))


class MultiSineDisplacer(PointDisplacer):
    def __init__(
        self,
        waves,
        time_start,
        time_end,
        n_frames,
        speed=1,
        axis=90,
        amplitude_scale=1,
    ):
        """!
        Displaces points as if they were following a sine wave

        @param waves       List of tuples (wavelength, amplitude)
        @param time_start  When the animation shall start
        @param time_end    When the animation shall end
        @param n_frames    Number of keyframes to add
        @param speed       Number of peaks a point will go through in the given time
                           If negative, it will go the other way
        @param axis        Wave peak direction
        @param amplitude_scale  Multiplies the resulting amplitude by this factor
        """
        super().__init__(time_start, time_end, n_frames)

        self.waves = waves
        self.speed_f = math.pi * 2 * speed
        self.axis = axis / 180 * math.pi
        self.amplitude_scale = amplitude_scale

    def _on_displace(self, startpos, f):
        off = 0
        for wavelength, amplitude in self.waves:
            off -= math.sin(startpos[0]/wavelength*math.pi*2-f*self.speed_f/self.n_frames) * amplitude

        off *= self.amplitude_scale
        return NVector(off * math.cos(self.axis), off * math.sin(self.axis))


class DepthRotationAxis:
    def __init__(self, x, y, keep):
        self.x = x / x.length
        self.y = y / y.length
        self.keep = keep / keep.length # should be the cross product

    def rot_center(self, center, point):
        return (
            self.x * self.x.dot(center) +
            self.y * self.y.dot(center) +
            self.keep * self.keep.dot(point)
        )

    def extract_component(self, vector, axis):
        return sum(vector.element_scaled(axis).components)

    @classmethod
    def from_points(cls, keep_point, center=NVector(0, 0, 0)):
        keep = keep_point - center
        keep /= keep.length
        # Hughes-Moller to find x and y
        if abs(keep.x) > abs(keep.z):
            y = NVector(-keep.y, keep.x, 0)
        else:
            y = NVector(0, -keep.z, keep.y)
        y /= y.length
        x = y.cross(keep)
        return cls(x, y, keep)


class DepthRotation:
    axis_x = DepthRotationAxis(NVector(0, 0, 1), NVector(0, 1, 0), NVector(1, 0, 0))
    axis_y = DepthRotationAxis(NVector(1, 0, 0), NVector(0, 0, 1), NVector(0, 1, 0))
    axis_z = DepthRotationAxis(NVector(1, 0, 0), NVector(0, 1, 0), NVector(0, 0, 1))

    def __init__(self, center):
        self.center = center

    def rotate3d_y(self, point, angle):
        return self.rotate3d(point, angle, self.axis_y)
        # Hard-coded version:
        #c = NVector(self.center.x, point.y, self.center.z)
        #rad = angle * math.pi / 180
        #delta = point - c
        #pol_l = delta.length
        #pol_a = math.atan2(delta.z, delta.x)
        #dest_a = pol_a + rad
        #return NVector(
        #    c.x + pol_l * math.cos(dest_a),
        #    point.y,
        #    c.z + pol_l * math.sin(dest_a)
        #)

    def rotate3d_x(self, point, angle):
        return self.rotate3d(point, angle, self.axis_x)
        # Hard-coded version:
        #c = NVector(point.x, self.center.y, self.center.z)
        #rad = angle * math.pi / 180
        #delta = point - c
        #pol_l = delta.length
        #pol_a = math.atan2(delta.y, delta.z)
        #dest_a = pol_a + rad
        #return NVector(
        #    point.x,
        #    c.y + pol_l * math.sin(dest_a),
        #    c.z + pol_l * math.cos(dest_a),
        #)

    def rotate3d_z(self, point, angle):
        return self.rotate3d(point, angle, self.axis_z)

    def rotate3d(self, point, angle, axis):
        c = axis.rot_center(self.center, point)
        rad = angle * math.pi / 180
        delta = point - c
        pol_l = delta.length
        pol_a = math.atan2(
            axis.extract_component(delta, axis.y),
            axis.extract_component(delta, axis.x)
        )
        dest_a = pol_a + rad
        return c + axis.x * pol_l * math.cos(dest_a) + axis.y * pol_l * math.sin(dest_a)


class DepthRotationDisplacer(PointDisplacer):
    axis_x = DepthRotation.axis_x
    axis_y = DepthRotation.axis_y
    axis_z = DepthRotation.axis_z

    def __init__(self, center, time_start, time_end, n_frames, axis,
                 depth=0, angle=360, anglestart=0, ease=easing.Linear()):
        super().__init__(time_start, time_end, n_frames)
        self.rotation = DepthRotation(center)
        if isinstance(axis, NVector):
            axis = DepthRotationAxis.from_points(axis)
        self.axis = axis
        self.depth = depth
        self._angle = angle
        self.anglestart = anglestart
        self.ease = ease
        self._init_lerp(0, angle, ease)

    @property
    def angle(self):
        return self._angle

    @angle.setter
    def angle(self, value):
        self._angle = value
        self._init_lerp(0, value, self.ease)

    def _on_displace(self, startpos, f):
        angle = self.anglestart + self._lerp_get(f)
        if len(startpos) < 3:
            startpos = NVector(*(startpos.components + [self.depth]))
        return self.rotation.rotate3d(startpos, angle, self.axis) - startpos


class EnvelopeDeformation(PointDisplacer):
    def __init__(self, topleft, bottomright):
        self.topleft = topleft
        self.size = bottomright - topleft
        self.keyframes = []

    @property
    def time_start(self):
        return self.keyframes[0][0]

    def add_reset_keyframe(self, time):
        self.add_keyframe(
            time,
            self.topleft.clone(),
            NVector(self.topleft.x + self.size.x, self.topleft.y),
            NVector(self.topleft.x + self.size.x, self.topleft.y + self.size.y),
            NVector(self.topleft.x, self.topleft.y + self.size.y),
        )

    def add_keyframe(self, time, tl, tr, br, bl):
        self.keyframes.append([
            time,
            tl.clone(),
            tr.clone(),
            br.clone(),
            bl.clone()
        ])

    def _on_displace(self, startpos, f):
        _, tl, tr, br, bl = self.keyframes[f]
        relp = startpos - self.topleft
        relp.x /= self.size.x
        relp.y /= self.size.y

        x1 = tl.lerp(tr, relp.x)
        x2 = bl.lerp(br, relp.x)

        #return x1.lerp(x2, relp.y)
        return x1.lerp(x2, relp.y) - startpos

    @property
    def n_frames(self):
        return len(self.keyframes)-1

    def frame_time(self, f):
        return self.keyframes[f][0]


class DisplacerDampener(PointDisplacer):
    """!
    Given a displacer and a function that returns a factor for a point,
    multiplies the effect of the displacer by the factor
    """
    def __init__(self, displacer, dampener):
        self.displacer = displacer
        self.dampener = dampener

    @property
    def time_start(self):
        return self.displacer.time_start

    def _on_displace(self, startpos, f):
        disp = self.displacer._on_displace(startpos, f)
        damp = self.dampener(startpos)
        return disp * damp

    @property
    def n_frames(self):
        return self.displacer.n_frames

    def frame_time(self, f):
        return self.displacer.frame_time(f)


class FollowDisplacer(PointDisplacer):
    def __init__(
        self,
        origin,
        range,
        offset_func,
        time_start, time_end, n_frames,
        falloff_exp=1,
    ):
        """!
        @brief Uses a custom offset function, and applies a falloff to the displacement

        @param origin       Origin point for the falloff
        @param range        Radius after which the points will not move
        @param offset_func  Function returning an offset given a ratio of the time
        @param time_start   When the animation shall start
        @param time_end     When the animation shall end
        @param n_frames     Number of frames in the animation
        @param falloff_exp  Exponent for the falloff
        """
        super().__init__(time_start, time_end, n_frames)
        self.origin = origin
        self.range = range
        self.offset_func = offset_func
        self.falloff_exp = falloff_exp

    def _on_displace(self, startpos, f):
        influence = 1 - min(1, (startpos - self.origin).length / self.range) ** self.falloff_exp
        return self.offset_func(f / self.n_frames) * influence
