from __future__ import annotations

from collections.abc import Callable
from math import cos, pi, sin
from typing import Any

from ..array import argpass
from ..path import Path
from .chord import ChordItem, ChordValue

HALF_PI = pi * 0.5
EPSILON = 1e-6


def constant(x: Any) -> Callable[..., Any]:
    def f(*args: Any) -> Any:
        return x

    return f


def default_source(d: ChordItem) -> float:
    return d.source


def default_target(d: ChordItem) -> float:
    return d.target


def default_radius(d: dict[str, float]) -> float:
    return d["radius"]


def default_start_angle(d: ChordValue) -> float:
    return d.start_angle


def default_end_angle(d: ChordValue) -> float:
    return d.end_angle


def default_pad_angle(d: ChordValue) -> float:
    return 0


def default_arrow_head_radius(d: ChordValue) -> float:
    return 10


class Ribbon:
    """
    Ribbon generator

    Parameters
    ----------
    head_radius : Callable[..., float] | None
        Arrow head radius accessor function
    """

    def __init__(self, head_radius: Callable[..., float] | None = None):
        self._head_radius = argpass(head_radius) if callable(head_radius) else None
        self._source = argpass(default_source)
        self._target = argpass(default_target)
        self._source_radius = argpass(default_radius)
        self._target_radius = argpass(default_radius)
        self._start_angle = argpass(default_start_angle)
        self._end_angle = argpass(default_end_angle)
        self._pad_angle = argpass(default_pad_angle)
        self._context = None

    def __call__(self, *args: Any) -> str | None:
        """
        Generates a ribbon for the given arguments. The arguments are
        arbitrary; they are propagated to the ribbon generator's accessor
        functions along with the this object.

        Parameters
        ----------
        *args : Any
            Additional arguments passed through ribbon generator's accessor
            methods

        Returns
        -------
        str | None
            Generated ribbon path
        """
        s = self._source(*args)
        t = self._target(*args)
        ap = self._pad_angle(*args) * 0.5
        argv = list(args).copy() if len(args) > 0 else [None]
        argv[0] = s
        sr = self._source_radius(*argv)
        sa0 = self._start_angle(*argv) - HALF_PI
        sa1 = self._end_angle(*argv) - HALF_PI
        argv[0] = t
        tr = self._target_radius(*argv)
        ta0 = self._start_angle(*argv) - HALF_PI
        ta1 = self._end_angle(*argv) - HALF_PI

        if self._context is None:
            self._context = buffer = Path()

        if ap > EPSILON:
            if abs(sa1 - sa0) > ap * 2 + EPSILON:
                if sa1 > sa0:
                    sa0 += ap
                    sa1 -= ap
                else:
                    sa0 -= ap
                    sa1 += ap
            else:
                sa0 = sa1 = (sa0 + sa1) / 2
            if abs(ta1 - ta0) > ap * 2 + EPSILON:
                if ta1 > ta0:
                    ta0 += ap
                    ta1 -= ap
                else:
                    ta0 -= ap
                    ta1 += ap
            else:
                ta0 = ta1 = (ta0 + ta1) / 2

        self._context.move_to(sr * cos(sa0), sr * sin(sa0))
        self._context.arc(0, 0, sr, sa0, sa1)
        if sa0 != ta0 or sa1 != ta1:
            if self._head_radius is None:
                self._context.quadratic_curve_to(0, 0, tr * cos(ta0), tr * sin(ta0))
                self._context.arc(0, 0, tr, ta0, ta1)
            else:
                hr = self.head_radius(*args)
                tr2 = tr - hr
                ta2 = (ta0 + ta1) / 2
                self._context.quadratic_curve_to(0, 0, tr2 * cos(ta0), tr2 * sin(ta0))
                self._context.line_to(tr * cos(ta2), tr * sin(ta2))
                self._context.line_to(tr2 * cos(ta1), tr2 * sin(ta1))
        self._context.quadratic_curve_to(0, 0, sr * cos(sa0), sr * sin(sa0))
        self._context.close_path()

        if buffer:
            self._context = None
            return str(buffer) or None

    def set_head_radius(
        self, head_radius: Callable[..., float] | float | None = None
    ) -> Ribbon:
        """
        Sets the arrowhead radius accessor to the specified function and
        returns this ribbon generator.

        Parameters
        ----------
        head_radius : Callable[..., float] | float | None
            Arrow head radius accessor function

        Returns
        -------
        Ribbon
            Itself
        """
        if head_radius is None:
            self._head_radius = None
            return self
        if callable(head_radius):
            self._head_radius = head_radius
        else:
            self._head_radius = constant(head_radius)
        self._head_radius = argpass(self._head_radius)
        return self

    def set_radius(self, radius: Callable[..., float] | float) -> Ribbon:
        """
        Sets the source and target radius accessor to the specified function
        and returns this ribbon generator.

        Parameters
        ----------
        radius : Callable[..., float] | float
            Radius function or constant value

        Returns
        -------
        Ribbon
            Itself
        """
        if callable(radius):
            self._source_radius = radius
            self._target_radius = radius
        else:
            radius = constant(radius)
            self._source_radius = radius
            self._target_radius = radius
        self._source_radius = argpass(self._source_radius)
        self._target_radius = argpass(self._target_radius)
        return self

    def set_source_radius(self, source_radius: Callable[..., float] | float) -> Ribbon:
        """
        Sets the source radius accessor to the specified function and returns
        this ribbon generator.

        Parameters
        ----------
        source_radius : Callable[..., float] | float
            Source radius function

        Returns
        -------
        Ribbon
            Itself
        """
        if callable(source_radius):
            self._source_radius = source_radius
        else:
            self._source_radius = constant(source_radius)
        self._source_radius = argpass(self._source_radius)
        return self

    def set_target_radius(self, target_radius: Callable[..., float] | float) -> Ribbon:
        """
        Sets the target radius accessor to the specified function and returns
        this ribbon generator.

        Parameters
        ----------
        target_radius : Callable[..., float] | float
            Target radius function

        Returns
        -------
        Ribbon
            Itself
        """
        if callable(target_radius):
            self._target_radius = target_radius
        else:
            self._target_radius = constant(target_radius)
        self._target_radius = argpass(self._target_radius)
        return self

    def set_start_angle(self, start_angle: Callable[..., float] | float) -> Ribbon:
        """
        Sets the start angle accessor to the specified function and returns
        this ribbon generator.

        Parameters
        ----------
        start_angle : Callable[..., float] | float
            Start angle function or constant value

        Returns
        -------
        Ribbon
            Itself
        """
        if callable(start_angle):
            self._start_angle = start_angle
        else:
            self._start_angle = constant(start_angle)
        self._start_angle = argpass(self._start_angle)
        return self

    def set_end_angle(self, end_angle: Callable[..., float] | float) -> Ribbon:
        """
        Sets the end angle accessor to the specified function and returns
        this ribbon generator.

        Parameters
        ----------
        end_angle : Callable[..., float] | float
            End angle function or constant value

        Returns
        -------
        Ribbon
            Itself
        """
        if callable(end_angle):
            self._end_angle = end_angle
        else:
            self._end_angle = constant(end_angle)
        self._end_angle = argpass(self._end_angle)
        return self

    def set_pad_angle(self, pad_angle: Callable[..., float] | float) -> Ribbon:
        """
        Sets the pad angle accessor to the specified function and returns this
        ribbon generator

        Parameters
        ----------
        pad_angle : Callable[..., float] | float
            Pad angle function or constant value

        Returns
        -------
        Ribbon
            Itself
        """
        if callable(pad_angle):
            self._pad_angle = pad_angle
        else:
            self._pad_angle = constant(pad_angle)
        self._pad_angle = argpass(self._pad_angle)
        return self

    def set_source(self, source: Callable[..., float] | float) -> Ribbon:
        """
        Sets the source accessor to the specified function and returns this
        ribbon generator.

        Parameters
        ----------
        source : Callable[..., float] | float
            Source function or constant function

        Returns
        -------
        Ribbon
            Itself
        """
        self._source = argpass(source)
        return self

    def set_target(self, target: Callable[..., float] | float) -> Ribbon:
        """
        Sets the target accessor to the specified function and returns this
        ribbon generator.

        Parameters
        ----------
        target : Callable[..., float] | float
            Target function or constant function

        Returns
        -------
        Ribbon
            Itself
        """
        self._target = argpass(target)
        return self

    def set_context(self, context: Any | None = None) -> Ribbon:
        """
        If context is specified, sets the context and returns this ribbon
        generator. If the context is not :code:`None`, then the generated
        ribbon is rendered to this context as a sequence of path method calls.
        Otherwise, a path data string representing the generated ribbon is
        returned.

        Parameters
        ----------
        context : Any | None
            Context value

        Returns
        -------
        Ribbon
            Itself
        """
        self._context = context
        return self

    def get_head_radius(self) -> Callable[..., float]:
        return self._head_radius

    def get_radius(self) -> Callable[..., float]:
        return self._source_radius

    def get_source_radius(self) -> Callable[..., float]:
        return self._source_radius

    def get_target_radius(self) -> Callable[..., float]:
        return self._target_radius

    def get_start_angle(self) -> Callable[..., float]:
        return self._start_angle

    def get_end_angle(self) -> Callable[..., float]:
        return self._end_angle

    def get_pad_angle(self) -> Callable[..., float]:
        return self._pad_angle

    def get_source(self) -> Callable[..., float]:
        return self._source

    def get_target(self) -> Callable[..., float]:
        return self._target

    def get_context(self) -> Any | None:
        return self._context


def ribbon() -> Ribbon:
    """
    Creates a new ribbon generator with the default settings.

    Returns
    -------
    Ribbon
        Ribbon generator
    """
    return Ribbon(None)


def ribbon_arrow() -> Ribbon:
    """
    Creates a new arrow ribbon generator with the default settings.
    See also :func:`d3.chord_directed <detroit.chord_directed>`.

    Returns
    -------
    Ribbon
        Ribbon generator
    """
    return Ribbon(default_arrow_head_radius)
