import math
import random
import numpy as np
from typing import Union, Optional, Dict, List
import bezier
import math
import random
from typing import Tuple, List, Optional


class Vector:
    def __init__(self, x: float, y: float):
        self.x = x
        self.y = y

    def __repr__(self):
        return "x: {}, y: {}".format(self.x, self.y)


origin = Vector(0, 0)


def sub(a: Vector, b: Vector) -> Vector:
    return Vector(a.x - b.x, a.y - b.y)


def div(a: Vector, b: float) -> Vector:
    return Vector(a.x / b, a.y / b)


def mult(a: Vector, b: float) -> Vector:
    return Vector(a.x * b, a.y * b)


def add(a: Vector, b: Vector) -> Vector:
    return Vector(a.x + b.x, a.y + b.y)


def direction(a: Vector, b: Vector) -> Vector:
    return sub(b, a)


def perpendicular(a: Vector) -> Vector:
    return Vector(a.y, -1 * a.x)


def magnitude(a: Vector) -> Vector:
    return math.sqrt(a.x**2 + a.y**2)


def unit(a: Vector) -> Vector:
    return div(a, magnitude(a))


def setMagnitude(a: Vector, amount: float) -> Vector:
    return mult(unit(a), amount)


def randomVectorOnLine(a: Vector, b: Vector) -> Vector:
    vec = direction(a, b)
    multiplier = random.random()
    return add(a, mult(vec, multiplier))


def randomNormalLine(a: Vector, b: Vector, range_: float) -> Tuple[Vector, Vector]:
    randMid = randomVectorOnLine(a, b)
    normalV = setMagnitude(perpendicular(direction(a, randMid)), range_)
    return randMid, normalV


def generateBezierAnchors(a: Vector, b: Vector, spread: float) -> List[Vector]:
    side = 1 if round(random.random()) == 1 else -1

    def calc() -> Vector:
        randMid, normalV = randomNormalLine(a, b, spread)
        choice = mult(normalV, side)
        return randomVectorOnLine(randMid, add(randMid, choice))

    return sorted([calc(), calc()], key=lambda vec: vec.x)


def clamp(target: float, min_: float, max_: float) -> float:
    return min(max_, max(min_, target))


def overshoot(coordinate: Vector, radius: float) -> Vector:
    a = random.random() * 2 * math.pi
    rad = radius * math.sqrt(random.random())
    vector = Vector(rad * math.cos(a), rad * math.sin(a))
    return add(coordinate, vector)


def bezierCurve(
    start: Vector, finish: Vector, overrideSpread: Optional[float]
) -> bezier.curve.Curve:
    min_ = 2
    max_ = 200
    vec = direction(start, finish)
    length = magnitude(vec)
    spread = clamp(length, min_, max_)
    anchors = generateBezierAnchors(
        start, finish, overrideSpread if overrideSpread is not None else spread
    )
    all_vectors = [start] + anchors + [finish]
    nodes = [
        [el.x for el in all_vectors],
        [el.y for el in all_vectors],
    ]
    return bezier.curve.Curve.from_nodes(nodes)


def fitts(distance: float, width: float) -> float:
    a = 0
    b = 2
    id_ = math.log2(distance / width + 1)
    return a + b * id_


def path(
    start: Vector, end: Union[Dict, Vector], spreadOverride: Optional[float] = None
) -> List[Vector]:
    defaultWidth = 100
    minSteps = 25
    if isinstance(end, dict):
        width = end["width"]
        end = Vector(end["x"], end["y"])
    else:
        width = defaultWidth
    curve = bezierCurve(start, end, spreadOverride)
    length = curve.length * 0.8
    baseTime = random.random() * minSteps
    steps = int(
        math.ceil((math.log2(fitts(length, width) + 1) + baseTime) * 3)
        * (8 + random.random())
    )
    s_vals = np.linspace(0.0, 1.0, steps)
    points = curve.evaluate_multi(s_vals)
    vectors = []
    for i in range(steps):
        vectors.append(Vector(points[0][i], points[1][i]))
    return clampPositive(vectors)


def clampPositive(vectors: List[Vector]) -> List[Vector]:
    clamp0 = lambda elem: max(0, elem)
    return [Vector(clamp0(el.x), clamp0(el.y)) for el in vectors]


overshootThreshold = 500


def should_overshoot(a: Vector, b: Vector) -> bool:
    return magnitude(direction(a, b)) > overshootThreshold


def get_path(start: Dict, end: Dict) -> List[Dict]:
    vectors = path(Vector(**start), Vector(**end))
    return [el.__dict__ for el in vectors]


def get_random_box_point(
    box: Dict, padding_percentage: Optional[float] = None
) -> Vector:
    """Get a random point on a box"""
    paddingWidth = paddingHeight = 0
    if (
        padding_percentage is not None
        and padding_percentage > 0
        and padding_percentage < 100
    ):
        paddingWidth = box["width"] * padding_percentage / 100
        paddingHeight = box["height"] * padding_percentage / 100
    return Vector(
        box["x"] + (paddingWidth / 2) + random.random() * (box["width"] - paddingWidth),
        box["y"]
        + (paddingHeight / 2)
        + random.random() * (box["height"] - paddingHeight),
    )


# start = {
#     "x": 220,
#     "y": 402,
# }

# end = {
#     "x": 902,
#     "y": 1032,
# }
def getRandomPath(num=100):
    while True:
        startX = random.randint(100, 1200)
        startY = random.randint(100, 1000)

        endX = random.randint(100, 1200)
        endY = random.randint(100, 1000)
        route = get_path({"x": startX, "y": startY}, {"x": endX, "y": endY})
        if len(route) < num:
            continue
        return route[:num]


if __name__ == "__main__":
    print(getRandomPath())
