"""Module containing the function `make_swipe_gesture`, which is used to create
a natural-looking swipe gesture from a list of letter-points.
"""

import random
from typing import List, Tuple

import numpy as np
from scipy.special import comb

from kebbie.utils import euclidian_dist


MAX_RADIUS = 16
MIN_N_POINTS_PER_DIST = 0.1
MAX_N_POINTS_PER_DIST = 0.25
MIN_ACCELERATION = 0.2
MAX_ACCELERATION = 0.5


def make_swipe_gesture(control_points: List[Tuple[float, float]]) -> List[Tuple[float, float]]:
    """Function to generate artificial swipe gesture from a list of points.
    The given points represents the typed letters on the keyboard. This
    function simply generate several other points between the control points.
    Points are generated using sequential Bezier curves. The resulting swipe
    gesture pass by the control points.

    Args:
        control_points (List[Tuple[float, float]]): Control points,
            representing the letter typed. The resulting swipe gesture will
            pass by these points.

    Returns:
        Points generated by the swipe gesture.
    """
    gesture_points = [control_points[0]]

    # Pick a "style" (speed & acceleration) and keep it constant across the gesture
    speed = random.uniform(MIN_N_POINTS_PER_DIST, MAX_N_POINTS_PER_DIST)
    acceleration = random.uniform(MIN_ACCELERATION, MAX_ACCELERATION)

    # Generate bezier curves between each control points
    for p1, p2 in zip(control_points[:-1], control_points[1:]):
        # The distance between the 2 points will dictate the speed and radius
        d = euclidian_dist(p1, p2)
        radius = min(d, MAX_RADIUS)
        n_points = max(1, int(d * speed))

        linspace = accelerated_linspace(n_points, acceleration)

        # We don't want the curves to be straight between the control points,
        # so we generate random points to add curves
        p1_curv = random_point_around(p1, radius=radius)
        p2_curv = random_point_around(p2, radius=radius)

        # Make the bezier curve with the specified number of points
        xs, ys = bezier_curve([p2, p2_curv, p1_curv, p1], linspace=linspace)
        bezier_points = list(zip(xs, ys))

        # Make sure the control point p2 is here
        if bezier_points[-1] != p2:
            bezier_points.append(p2)
        # p1 was already added in the previous loop, no need to add it
        if bezier_points[0] == p1:
            bezier_points = bezier_points[1:]

        gesture_points.extend(bezier_points)

    return gesture_points


def random_point_around(p: Tuple[float, float], radius: float) -> Tuple[float, float]:
    """Generate a random point around the given point p, within the given
    radius.

    Args:
        p (Tuple[float, float]): Coordinates to use as a starting point.
        radius (float): Radius within the starting point to generate the random
            point.

    Returns:
        Coordinates of the generated random point.
    """
    rand_x = random.uniform(p[0] - radius, p[0] + radius)
    rand_y = random.uniform(p[1] - radius, p[1] + radius)
    return (rand_x, rand_y)


def bernstein_poly(i: int, n: int, t: float) -> float:
    """The Bernstein polynomial of n, i as a function of t.

    Taken from : https://stackoverflow.com/a/12644499/9494790

    Args:
        i (int): i
        n (int): n
        t (float): t

    Returns:
        The computed value for this polynomial function.
    """
    return comb(n, i) * (t ** (n - i)) * (1 - t) ** i


def bezier_curve(control_points: List[Tuple[float, float]], linspace: List[float]) -> Tuple[List[float], List[float]]:
    """Given a set of control points, return the bezier curve defined by the
    control points.

    See : http://processingjs.nihongoresources.com/bezierinfo/

    Taken from : https://stackoverflow.com/a/12644499/9494790

    Args:
        control_points (List[Tuple[float, float]]): Control points used to
            generate the bezier curve.
        linspace (List[float]): Linspace to use for sampling points across the
            Bezier curve.

    Returns:
        Sampled points along the bezier curve.
    """
    n_points = len(control_points)
    x_points = np.array([p[0] for p in control_points])
    y_points = np.array([p[1] for p in control_points])

    polynomial_array = np.array([bernstein_poly(i, n_points - 1, linspace) for i in range(0, n_points)])

    x_vals = np.dot(x_points, polynomial_array)
    y_vals = np.dot(y_points, polynomial_array)

    return x_vals, y_vals


def accelerated_linspace(n: int, acceleration: float) -> List[float]:
    """Alternative to np.linspace, instead of giving a range of number evenly
    distributed, this one is not evenly distributed, and simulate an
    acceleration at first, and then a deceleration.

    Args:
        n (int): Number of points to generate in the linspace.
        acceleration (float): A number that dictate how constant the
            acceleration is. The lower, the more S-shape is used.

    Returns:
        Generated points.
    """

    def norm(x):
        nom = x - x.min()
        denom = x.max() - x.min()
        return nom / denom

    def sigmoid(x, k):
        return 1 / (1 + np.exp(-x / k))

    linspace = np.linspace(-1.0, 1.0, n)

    if n <= 1:
        return linspace
    else:
        return norm(sigmoid(linspace, k=acceleration))
