"""Secant root solver."""

from __future__ import annotations

from collections.abc import Callable

from ..errors import ConvergenceFailed, DivisionByZero, InvalidInput
from ..utils import assert_finite
from .types import SolverConfig, SolverResult


def secant(
    f: Callable[[float], float],
    x0: float,
    x1: float,
    config: SolverConfig = SolverConfig(),
) -> SolverResult:
    x0 = assert_finite(x0, name="x0")
    x1 = assert_finite(x1, name="x1")
    if config.max_iterations <= 0:
        raise InvalidInput("max_iterations must be positive.")
    if config.tolerance <= 0:
        raise InvalidInput("tolerance must be positive.")

    f0 = assert_finite(f(x0), name="f(x0)")
    f1 = assert_finite(f(x1), name="f(x1)")
    if abs(f0) <= config.tolerance:
        return SolverResult(root=x0, iterations=0, residual=abs(f0), converged=True)
    if abs(f1) <= config.tolerance:
        return SolverResult(root=x1, iterations=0, residual=abs(f1), converged=True)

    for it in range(1, config.max_iterations + 1):
        denom = f1 - f0
        if denom == 0.0:
            raise DivisionByZero(value=denom)
        x2 = x1 - f1 * (x1 - x0) / denom
        f2 = assert_finite(f(x2), name="f(x2)")
        residual = abs(f2)
        if residual <= config.tolerance:
            return SolverResult(root=x2, iterations=it, residual=residual, converged=True)
        x0, f0, x1, f1 = x1, f1, x2, f2

    residual = abs(assert_finite(f(x1), name="f(last)"))
    raise ConvergenceFailed(iterations=config.max_iterations, residual=residual)


class SecantSolver:
    def __init__(self, *, config: SolverConfig = SolverConfig()) -> None:
        self._config = config

    def find_root(self, f: Callable[[float], float], x0: float, x1: float) -> SolverResult:
        return secant(f, x0, x1, config=self._config)
