"""A ring effect."""

import asyncio
from math import cos, dist, sin, tau
from random import choice, random
from time import perf_counter
from typing import cast

import numpy as np

from ...array_types import Coords
from ...colors import BLUE, WHITE, Color, gradient, lerp_colors
from ...geometry import BezierCurve, Point, move_along_path
from ..text import Text
from ..text_field import TextParticleField
from ._particle import Particle

RNG = np.random.default_rng()
RING_COLORS = [Color.from_hex("8a008a"), Color.from_hex("00d1ff")]
DISPERSE_COLORS = gradient(BLUE, WHITE, n=10)


class _RingParticle(Particle):
    final_pos: Point
    ring_point: tuple[float, float]
    ring_color: Color


async def ring_effect(text: Text):
    """
    Create a ring effect.

    Parameters
    ----------
    text : Text
        The text gadget which to run the effect.

    Warnings
    --------
    Modifying `text` size while effect is running will break the effect.
    """
    field = TextParticleField(size_hint={"height_hint": 1.0, "width_hint": 1.0})
    field.particles_from_cells(text.canvas)

    particles = list(_RingParticle.iter_from_field(field))
    for particle in particles:
        particle.final_pos = Point(int(particle.pos.y), int(particle.pos.x))
    coords = RNG.random((field.nparticles, 2)) * text.size  # type: ignore
    coords = cast(Coords, coords)
    field.particle_coords = coords

    min_dim = min(text.height, text.width / 2)
    max_radius = int(2**0.5 * (min_dim / 2))

    radii = list(range(max_radius - 3, 3, -int(min_dim / 5)))
    center = Point(text.height // 2, text.width // 2)

    text.add_gadget(field)

    radius_to_particles = await _move_to_rings(particles, radii, center)
    await _spin_rings(radius_to_particles, center)
    await _disperse(particles)
    radius_to_particles = await _move_to_rings(particles, radii, center)
    await _spin_rings(radius_to_particles, center, reverse=True)
    await _disperse(particles)
    radius_to_particles = await _move_to_rings(particles, radii, center)
    await _spin_rings(radius_to_particles, center)
    await _settle(particles, text)

    text.remove_gadget(field)


def _distance_to_circle(p: Point, radius: int, center: Point) -> float:
    py, px = p
    px /= 2
    cy, cx = center
    cx /= 2
    return abs(dist((py, px), (cy, cx)) - radius)


def _closest_point_on_circle(
    p: Point, radius: int, center: Point
) -> tuple[float, float]:
    py, px = p
    px /= 2
    cy, cx = center
    cx /= 2
    vy, vx = py - cy, px - cx
    length_v = (vy**2 + vx**2) ** 0.5
    if length_v == 0:
        return cy, 2 * cx
    ty = cy + vy / length_v * radius
    tx = cx + vx / length_v * radius
    return ty, 2 * tx


def _rotate_around_center(
    p: tuple[float, float], center: Point, theta: float
) -> tuple[float, float]:
    py, px = p
    cy, cx = center
    oy, ox = py - cy, (px - cx) / 2

    sth = sin(theta)
    cth = cos(theta)

    return oy * sth - ox * cth + cy, 2 * (oy * cth + ox * sth) + cx


def _random_nearby_point(p: Point) -> tuple[float, float]:
    d = random() * 7
    theta = random() * tau
    py, px = p

    return d * sin(theta) + py, d * cos(theta) + px


async def _move_to_rings(
    particles: list[_RingParticle], radii: list[int], center: Point
) -> dict[int, list[_RingParticle]]:
    ring_colors = {
        radius: RING_COLORS[i % len(RING_COLORS)] for i, radius in enumerate(radii)
    }
    radius_to_particles = {radius: [] for radius in radii}
    paths = []
    for particle in particles:
        radius = min(
            radii, key=lambda radius: _distance_to_circle(particle.pos, radius, center)
        )
        radius_to_particles[radius].append(particle)
        p = _closest_point_on_circle(particle.pos, radius, center)
        paths.append([BezierCurve(np.array([particle.pos, p]))])
        particle.ring_point = p
        particle.ring_color = ring_colors[radius]

    def create_fade(particle):
        a = Color(*particle.cell["fg_color"].tolist())
        b = particle.ring_color

        def fade_particle(p):
            particle.cell["fg_color"] = lerp_colors(a, b, p)

        return fade_particle

    motions = [
        move_along_path(
            particle,
            path,
            speed=5,
            easing="in_circ",
            on_progress=create_fade(particle),
        )
        for particle, path in zip(particles, paths)
    ]

    await asyncio.gather(*motions)

    return radius_to_particles


async def _spin_rings(
    radius_to_particles: dict[int, list[_RingParticle]],
    center: Point,
    reverse: bool = False,
):
    start = perf_counter()
    theta = tau / 100

    while True:
        elapsed = perf_counter() - start
        if elapsed > 3:
            return

        for i, particles in enumerate(radius_to_particles.values()):
            direction = 1 if (i + reverse) % 2 else -1
            for particle in particles:
                y, x = _rotate_around_center(
                    particle.ring_point, center, direction * theta
                )
                particle.pos = int(y), int(x)

        theta += tau / 100
        await asyncio.sleep(0)


async def _disperse(particles: list[_RingParticle]):
    paths = []
    for particle in particles:
        controls = [particle.pos] + [
            _random_nearby_point(particle.pos) for _ in range(6)
        ]
        particle_path = [
            BezierCurve(np.array(controls[:3])),
            BezierCurve(np.array(controls[2:5])),
            BezierCurve(np.array(controls[4:])),
        ]
        paths.append(particle_path)

    def create_fade(particle):
        a = Color(*particle.cell["fg_color"].tolist())
        b = choice(DISPERSE_COLORS)

        def fade_particle(p):
            particle.cell["fg_color"] = lerp_colors(a, b, p)

        return fade_particle

    motions = [
        move_along_path(
            particle,
            path,
            speed=10,
            on_progress=create_fade(particle),
        )
        for particle, path in zip(particles, paths)
    ]

    await asyncio.gather(*motions)


async def _settle(particles: list[_RingParticle], text: Text):
    paths = []
    for particle in particles:
        controls = [
            particle.pos,
            *(_random_nearby_point(particle.pos) for _ in range(5)),
            particle.final_pos,
        ]
        particle_path = [
            BezierCurve(np.array(controls[:3])),
            BezierCurve(np.array(controls[2:5])),
            BezierCurve(np.array(controls[4:])),
        ]
        paths.append(particle_path)

    def create_fade(particle):
        a = Color(*particle.cell["fg_color"].tolist())
        b = Color(*text.canvas[particle.final_pos]["fg_color"].tolist())

        def fade_particle(p):
            particle.cell["fg_color"] = lerp_colors(a, b, p)

        return fade_particle

    motions = [
        move_along_path(
            particle,
            path,
            speed=20,
            easing="out_quad",
            on_progress=create_fade(particle),
        )
        for particle, path in zip(particles, paths)
    ]

    await asyncio.gather(*motions)
