package dsp

import (
	"math"

	"github.com/jan/fm-rds-tx/internal/output"
)

// FMPhaseUpsampler performs FM modulation + upsampling via phase-domain
// interpolation. Maintains continuous phase across successive calls.
type FMPhaseUpsampler struct {
	srcRate      float64
	dstRate      float64
	maxDeviation float64
	ratio        float64
	phase        float64 // persistent across calls
}

func NewFMPhaseUpsampler(srcRate, dstRate, maxDeviation float64) *FMPhaseUpsampler {
	return &FMPhaseUpsampler{
		srcRate:      srcRate,
		dstRate:      dstRate,
		maxDeviation: maxDeviation,
		ratio:        dstRate / srcRate,
	}
}

func (u *FMPhaseUpsampler) Process(frame *output.CompositeFrame) *output.CompositeFrame {
	if frame == nil || len(frame.Samples) == 0 {
		return frame
	}

	srcLen := len(frame.Samples)

	// Accumulate phase at source rate, continuing from previous chunk
	phases := make([]float64, srcLen)
	for i, s := range frame.Samples {
		u.phase += 2 * math.Pi * float64(s.I) * u.maxDeviation / u.srcRate
		phases[i] = u.phase
	}

	// Keep phase bounded
	if u.phase > 1e9 {
		offset := math.Floor(u.phase/(2*math.Pi)) * 2 * math.Pi
		u.phase -= offset
		for i := range phases {
			phases[i] -= offset
		}
	}

	// Interpolate phase to target rate
	dstLen := int(float64(srcLen) * u.ratio)
	dst := make([]output.IQSample, dstLen)
	step := 1.0 / u.ratio

	pos := 0.0
	for i := 0; i < dstLen; i++ {
		idx := int(pos)
		frac := pos - float64(idx)

		var p float64
		if idx+1 < srcLen {
			p = phases[idx]*(1-frac) + phases[idx+1]*frac
		} else if idx < srcLen {
			p = phases[idx]
		}

		dst[i] = output.IQSample{
			I: float32(math.Cos(p)),
			Q: float32(math.Sin(p)),
		}
		pos += step
	}

	return &output.CompositeFrame{
		Samples:      dst,
		SampleRateHz: u.dstRate,
		Timestamp:    frame.Timestamp,
		GeneratedAt:  frame.GeneratedAt,
		Sequence:     frame.Sequence,
	}
}

// UpsampleFMPhase is the stateless version for offline/test use.
func UpsampleFMPhase(frame *output.CompositeFrame, targetRate, maxDeviation float64) *output.CompositeFrame {
	u := NewFMPhaseUpsampler(frame.SampleRateHz, targetRate, maxDeviation)
	return u.Process(frame)
}
