package odometry

import (
	"math"
	"math/cmplx"
)

// EstimateRotation estimates the rotation angle (in degrees) between two
// grayscale matrices using the log-polar phase correlation method:
//
//  1. Compute the magnitude spectra |F1|, |F2| (rotation in spatial domain =
//     rotation in frequency domain).
//  2. Apply a high-pass filter to suppress the DC-dominated center.
//  3. Resample to log-polar coordinates: rotation becomes a shift along the
//     angular axis, scale becomes a shift along the log-radius axis.
//  4. Phase-correlate in log-polar space → the peak along the angular axis
//     gives the rotation angle.
//
// Returns the rotation angle in degrees (positive = counter-clockwise rotation
// of the scene from g1 to g2), and a confidence in [0,1].
func EstimateRotation(g1, g2 [][]float64, rows, cols int) (angleDeg float64, conf float64) {
	// 1. FFT both images.
	a := windowedComplex(g1, rows, cols)
	b := windowedComplex(g2, rows, cols)
	fft2(a, false)
	fft2(b, false)

	// 2. Magnitude spectra with high-pass filtering.
	magA := make([][]float64, rows)
	magB := make([][]float64, rows)
	cy, cx := float64(rows)/2, float64(cols)/2
	maxR := math.Min(cy, cx)
	for i := 0; i < rows; i++ {
		magA[i] = make([]float64, cols)
		magB[i] = make([]float64, cols)
		for j := 0; j < cols; j++ {
			// Shift zero-frequency to center (fftshift).
			si := i
			sj := j
			if i < rows/2 {
				si = i + rows/2
			} else {
				si = i - rows/2
			}
			if j < cols/2 {
				sj = j + cols/2
			} else {
				sj = j - cols/2
			}
			ma := cmplx.Abs(a[si][sj])
			mb := cmplx.Abs(b[si][sj])
			// High-pass: suppress frequencies near DC.
			dist := math.Sqrt(float64((float64(i)-cy)*(float64(i)-cy)) + float64((float64(j)-cx)*(float64(j)-cx)))
			hp := 1.0 - math.Exp(-(dist*dist)/(2*(maxR*0.1)*(maxR*0.1)))
			magA[i][j] = ma * hp
			magB[i][j] = mb * hp
		}
	}

	// 3. Resample magnitude spectra to log-polar coordinates.
	nAngles := 256   // angular resolution
	nRadii := 128    // radial resolution
	lpA := logPolarResample(magA, rows, cols, nAngles, nRadii, maxR)
	lpB := logPolarResample(magB, rows, cols, nAngles, nRadii, maxR)

	// 4. Phase-correlate in log-polar space.
	dx, dy, confRaw := phaseCorrelationFloat(lpA, lpB, nAngles, nRadii)

	// dy is the shift along the angular axis → rotation angle.
	// Each row in lpA/lpB corresponds to 360/nAngles degrees.
	angleDeg = -float64(dy) * 360.0 / float64(nAngles)
	// Wrap to [-180, 180)
	for angleDeg < -180 {
		angleDeg += 360
	}
	for angleDeg >= 180 {
		angleDeg -= 360
	}

	_ = dx // log-radius shift (scale change) — not used for rotation-only estimation
	conf = confRaw
	return angleDeg, conf
}

// logPolarResample converts a Cartesian matrix to log-polar coordinates.
// The output has nAngles rows (angular samples) and nRadii columns (log-radius
// samples). This transforms a rotation in Cartesian space into a cyclic shift
// along the angular (row) axis.
func logPolarResample(mag [][]float64, rows, cols, nAngles, nRadii int, maxR float64) [][]float64 {
	out := make([][]float64, nAngles)
	cy, cx := float64(rows)/2, float64(cols)/2

	logMinR := math.Log(2.0)
	logMaxR := math.Log(maxR)
	rStep := (logMaxR - logMinR) / float64(nRadii-1)

	for a := 0; a < nAngles; a++ {
		theta := 2 * math.Pi * float64(a) / float64(nAngles)
		out[a] = make([]float64, nRadii)
		for r := 0; r < nRadii; r++ {
			radius := math.Exp(logMinR + float64(r)*rStep)
			y := cy + radius*math.Sin(theta)
			x := cx + radius*math.Cos(theta)
			// Bilinear interpolation.
			y0 := int(math.Floor(y))
			x0 := int(math.Floor(x))
			fy := y - float64(y0)
			fx := x - float64(x0)
			y0 = clampInt(y0, 0, rows-1)
			x0 = clampInt(x0, 0, cols-1)
			y1 := clampInt(y0+1, 0, rows-1)
			x1 := clampInt(x0+1, 0, cols-1)
			v := (1-fy)*(1-fx)*mag[y0][x0] +
				(1-fy)*fx*mag[y0][x1] +
				fy*(1-fx)*mag[y1][x0] +
				fy*fx*mag[y1][x1]
			out[a][r] = v
		}
	}
	return out
}

// phaseCorrelationFloat is a float-based phase correlation (same algorithm as
// phaseCorrelation but operates on float64 matrices instead of complex, using
// its own internal FFT buffers).
func phaseCorrelationFloat(g1, g2 [][]float64, rows, cols int) (dx, dy, conf float64) {
	a := windowedComplex(g1, rows, cols)
	b := windowedComplex(g2, rows, cols)

	fft2(a, false)
	fft2(b, false)

	r := make([][]complex128, rows)
	for i := 0; i < rows; i++ {
		r[i] = make([]complex128, cols)
		for j := 0; j < cols; j++ {
			cross := cmplx.Conj(a[i][j]) * b[i][j]
			mag := cmplx.Abs(cross)
			if mag > 1e-12 {
				r[i][j] = cross / complex(mag, 0)
			}
		}
	}

	fft2(r, true)

	px, py, peak := 0, 0, math.Inf(-1)
	mean := 0.0
	for i := 0; i < rows; i++ {
		for j := 0; j < cols; j++ {
			v := real(r[i][j])
			mean += v
			if v > peak {
				peak = v
				px, py = j, i
			}
		}
	}
	mean /= float64(rows * cols)

	dx = float64(signedShift(px, cols))
	dy = float64(signedShift(py, rows))

	// Sub-pixel refinement.
	dx += parabola(at2(r, py, px-1, cols), peak, at2(r, py, px+1, cols))
	dy += parabola(at2(r, py-1, px, rows), peak, at2(r, py+1, px, rows))

	denom := peak - mean
	if denom > 1 {
		denom = 1
	}
	if denom < 0 {
		denom = 0
	}
	conf = denom
	return dx, dy, conf
}

func clampInt(v, lo, hi int) int {
	if v < lo {
		return lo
	}
	if v > hi {
		return hi
	}
	return v
}
