import numpy as np
from finRav.stoch._fbm_base import fBMBase
from finRav.stoch._fractional_gaussian_noise import fGN

"""
_davies_harte_method.py
----------------------

Implements the Davies–Harte algorithm for simulating fractional Gaussian noise (fGn)
and fractional Brownian motion (fBM) using circulant embedding and FFT.
"""

class DaviesHarte(fBMBase):
    """
   Simulate fractional Brownian motion using the Davies–Harte algorithm.

   Parameters
   ----------
   N : int
       Number of samples.
   dt : float
       Time increment.
   H : float
       Hurst exponent.
   """

    def __init__(self, N: int, dt: float, H: float, rng: np.random.Generator | None = None):
        super().__init__(N=N, dt=dt, H=H, method='DaviesHarte')

        self.rng = rng or np.random.default_rng()

    def simulate(self):
        """
        Simulate an fBM path using the Davies–Harte algorithm.

        Steps
        -----
        1. Compute γ(k) for k=0,...,N using fGn class.
        2. Construct circulant vector c = [γ(0..N), γ(N-1..1)].
        3. Compute eigenvalues λ_j = Re(FFT(c)).
        4. Generate complex Gaussian Z_j according to λ_j.
        5. Compute IFFT(Z) → fGn.
        6. Return cumulative sum → fBM.

        Returns
        -------
        np.ndarray
            Simulated fractional Brownian motion path.
        """

        fgn = fGN(N=self.N, dt=self.dt, H=self.H)
        fgn.simulate()
        self.gamma = fgn.gamma

        m = 2 * (self.N - 1)
        c = np.empty(m, dtype=float)
        c[:self.N] = self.gamma
        c[self.N:] = self.gamma[1:-1][::-1]

        lam = np.fft.rfft(c).real
        lam[lam < 0] = 0.0

        L = lam.shape[0]
        a = self.rng.normal(size=L)
        b = self.rng.normal(size=L)
        z = a + 1j * b
        z[0] = self.rng.normal()
        if m % 2 == 0 and L > 1:
            z[-1] = self.rng.normal()

        z *= np.sqrt(lam / 2.0)
        z[0] = np.sqrt(lam[0]) * z[0]
        if m % 2 == 0 and L > 1:
            z[-1] = np.sqrt(lam[-1]) * z[-1]

        w = np.fft.irfft(z, n=m) * np.sqrt(m)

        dB = w[:self.N - 1]
        B = np.empty(self.N, dtype=float)
        B[0] = 0.0
        B[1:] = np.cumsum(dB)

        return B