//go:build soapy

// Package soapysdr provides a pure-Go SoapySDR driver that loads the
// SoapySDR shared library at runtime via dlopen/LoadLibrary.
// No CGO required. No C compiler required.
//
// Build with: go build -tags soapy
// Requires: SoapySDR shared library installed on the system.
//   Windows: SoapySDR.dll (via PothosSDR)
//   Linux:   libSoapySDR.so (via package manager)
//   macOS:   libSoapySDR.dylib (via brew)
package soapysdr

import (
	"context"
	"fmt"
	"math"
	"sync"
	"sync/atomic"
	"time"
	"unsafe"

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

// nativeDriver implements platform.SoapyDriver using runtime-loaded SoapySDR.
type nativeDriver struct {
	mu     sync.Mutex
	lib    *soapyLib
	cfg    platform.SoapyConfig
	dev    uintptr // SoapySDRDevice*
	stream uintptr // SoapySDRStream*
	mtu    int

	started        bool
	configured     bool
	framesWritten  atomic.Uint64
	samplesWritten atomic.Uint64
	underruns      atomic.Uint64
	lastError      string
	lastErrorAt    string
}

// NewNativeDriver creates an uninitialized SoapySDR native driver.
func NewNativeDriver() platform.SoapyDriver {
	lib, err := loadSoapyLib()
	if err != nil {
		// Return a driver that will fail on Configure with a clear message
		return &nativeDriver{lastError: fmt.Sprintf("load SoapySDR library: %v", err)}
	}
	return &nativeDriver{lib: lib}
}

// Enumerate lists available SoapySDR devices.
func Enumerate() ([]map[string]string, error) {
	lib, err := loadSoapyLib()
	if err != nil {
		return nil, fmt.Errorf("load SoapySDR: %w", err)
	}
	return lib.enumerate()
}

func (d *nativeDriver) Name() string { return "soapy-native" }

func (d *nativeDriver) Configure(_ context.Context, cfg platform.SoapyConfig) error {
	d.mu.Lock()
	defer d.mu.Unlock()

	if d.lib == nil {
		return fmt.Errorf("soapy: library not loaded: %s", d.lastError)
	}

	// Close existing
	if d.dev != 0 {
		if d.stream != 0 {
			d.lib.deactivateStream(d.dev, d.stream)
			d.lib.closeStream(d.dev, d.stream)
			d.stream = 0
		}
		d.lib.unmakeDevice(d.dev)
		d.dev = 0
	}
	d.cfg = cfg

	// Open device
	dev, err := d.lib.makeDevice(cfg.Driver, cfg.Device, cfg.DeviceArgs)
	if err != nil {
		return err
	}
	d.dev = dev

	// Sample rate
	rate := cfg.SampleRateHz
	if rate <= 0 {
		rate = 528000
	}
	if err := d.lib.setSampleRate(d.dev, dirTX, 0, rate); err != nil {
		return err
	}

	// Frequency
	if cfg.CenterFreqHz > 0 {
		if err := d.lib.setFrequency(d.dev, dirTX, 0, cfg.CenterFreqHz); err != nil {
			return err
		}
	}

	// Gain
	if cfg.GainDB != 0 {
		_ = d.lib.setGain(d.dev, dirTX, 0, cfg.GainDB)
	}

	// Setup TX stream (CF32)
	stream, err := d.lib.setupStream(d.dev, dirTX, "CF32", []uint{0})
	if err != nil {
		return err
	}
	d.stream = stream

	d.mtu = d.lib.getStreamMTU(d.dev, d.stream)
	if d.mtu <= 0 {
		d.mtu = 4096
	}

	d.configured = true
	return nil
}

func (d *nativeDriver) Capabilities(_ context.Context) (platform.DeviceCaps, error) {
	d.mu.Lock()
	defer d.mu.Unlock()
	if d.dev == 0 || d.lib == nil {
		return platform.DeviceCaps{}, fmt.Errorf("device not opened")
	}
	gMin, gMax := d.lib.getGainRange(d.dev, dirTX, 0)
	return platform.DeviceCaps{
		MinSampleRate: 521e3, MaxSampleRate: 61.44e6,
		HasGain: true, GainMinDB: gMin, GainMaxDB: gMax,
		Channels: []int{0},
	}, nil
}

func (d *nativeDriver) Start(_ context.Context) error {
	d.mu.Lock()
	defer d.mu.Unlock()
	if !d.configured || d.dev == 0 || d.stream == 0 {
		return fmt.Errorf("soapy: not configured")
	}
	if d.started {
		return fmt.Errorf("soapy: already started")
	}
	if err := d.lib.activateStream(d.dev, d.stream); err != nil {
		return err
	}
	d.started = true
	return nil
}

func (d *nativeDriver) Write(_ context.Context, frame *output.CompositeFrame) (int, error) {
	d.mu.Lock()
	lib, dev, stream, started, mtu := d.lib, d.dev, d.stream, d.started, d.mtu
	d.mu.Unlock()

	if !started || dev == 0 || stream == 0 {
		return 0, fmt.Errorf("soapy: stream not active")
	}
	if frame == nil || len(frame.Samples) == 0 {
		return 0, nil
	}

	total := len(frame.Samples)
	written := 0
	for written < total {
		chunk := total - written
		if chunk > mtu {
			chunk = mtu
		}
		// IQSample is {I float32, Q float32} — contiguous CF32 in memory
		ptr := unsafe.Pointer(&frame.Samples[written])
		n, err := lib.writeStream(dev, stream, ptr, chunk)
		if err != nil {
			d.mu.Lock()
			d.lastError = err.Error()
			d.lastErrorAt = time.Now().UTC().Format(time.RFC3339)
			d.underruns.Add(1)
			d.mu.Unlock()
			return written, err
		}
		written += n
	}
	d.framesWritten.Add(1)
	d.samplesWritten.Add(uint64(written))
	return written, nil
}

func (d *nativeDriver) Stop(_ context.Context) error {
	d.mu.Lock()
	defer d.mu.Unlock()
	if !d.started {
		return nil
	}
	if d.dev != 0 && d.stream != 0 {
		d.lib.deactivateStream(d.dev, d.stream)
	}
	d.started = false
	return nil
}

func (d *nativeDriver) Flush(_ context.Context) error { return nil }

func (d *nativeDriver) Tune(_ context.Context, freqHz float64) error {
	d.mu.Lock()
	defer d.mu.Unlock()
	if d.dev == 0 || d.lib == nil {
		return fmt.Errorf("soapy: not configured")
	}
	return d.lib.setFrequency(d.dev, dirTX, 0, freqHz)
}

func (d *nativeDriver) Close(_ context.Context) error {
	d.mu.Lock()
	defer d.mu.Unlock()
	if d.stream != 0 && d.dev != 0 {
		if d.started {
			d.lib.deactivateStream(d.dev, d.stream)
			d.started = false
		}
		d.lib.closeStream(d.dev, d.stream)
		d.stream = 0
	}
	if d.dev != 0 {
		d.lib.unmakeDevice(d.dev)
		d.dev = 0
	}
	d.configured = false
	return nil
}

func (d *nativeDriver) Stats() platform.RuntimeStats {
	d.mu.Lock()
	defer d.mu.Unlock()
	return platform.RuntimeStats{
		TXEnabled: d.started, StreamActive: d.started,
		FramesWritten: d.framesWritten.Load(), SamplesWritten: d.samplesWritten.Load(),
		Underruns: d.underruns.Load(), LastError: d.lastError, LastErrorAt: d.lastErrorAt,
		EffectiveRate: d.cfg.SampleRateHz,
	}
}

// --- helper constants ---
const dirTX = 1 // SOAPY_SDR_TX

// float64 from raw bits
func f64FromPtr(p uintptr) float64 {
	return math.Float64frombits(uint64(p))
}
