//go:build pluto && linux

package plutosdr

/*
#cgo pkg-config: libiio
#include <iio.h>
#include <stdlib.h>
#include <stdint.h>
*/
import "C"

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

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

func updateMaxDuration(dst *atomic.Uint64, d time.Duration) {
	v := uint64(d)
	for {
		cur := dst.Load()
		if v <= cur {
			return
		}
		if dst.CompareAndSwap(cur, v) {
			return
		}
	}
}

func durationMs(ns uint64) float64 {
	return float64(ns) / float64(time.Millisecond)
}

type PlutoDriver struct {
	mu  sync.Mutex
	cfg platform.SoapyConfig

	ctx     *C.struct_iio_context
	txDev   *C.struct_iio_device
	phyDev  *C.struct_iio_device
	chanI   *C.struct_iio_channel
	chanQ   *C.struct_iio_channel
	chanLO  *C.struct_iio_channel
	buf     *C.struct_iio_buffer
	bufSize int

	started        bool
	configured     bool
	framesWritten  atomic.Uint64
	samplesWritten atomic.Uint64
	underruns      atomic.Uint64
	slowWrites     atomic.Uint64
	slowFills      atomic.Uint64
	slowPushes     atomic.Uint64
	maxWriteNs     atomic.Uint64
	maxFillNs      atomic.Uint64
	maxPushNs      atomic.Uint64
	lastError      string
	lastErrorAt    string
	layoutLogged   bool
}

func NewPlutoDriver() platform.SoapyDriver {
	return &PlutoDriver{}
}

func (d *PlutoDriver) Name() string { return "pluto-iio" }

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

	d.cleanup()
	d.cfg = cfg

	uri := "usb:"
	if cfg.Device != "" && cfg.Device != "plutosdr" {
		uri = cfg.Device
	}
	if v, ok := cfg.DeviceArgs["uri"]; ok && v != "" {
		uri = v
	}

	cURI := C.CString(uri)
	defer C.free(unsafe.Pointer(cURI))
	ctx := C.iio_create_context_from_uri(cURI)
	if ctx == nil {
		return fmt.Errorf("pluto: failed to create IIO context (uri=%s)", uri)
	}
	d.ctx = ctx

	txDev := d.findDevice("cf-ad9361-dds-core-lpc")
	if txDev == nil {
		return fmt.Errorf("pluto: TX device 'cf-ad9361-dds-core-lpc' not found")
	}
	d.txDev = txDev

	phyDev := d.findDevice("ad9361-phy")
	if phyDev == nil {
		return fmt.Errorf("pluto: PHY device 'ad9361-phy' not found")
	}
	d.phyDev = phyDev

	phyChanTX := d.findChannel(phyDev, "voltage3", true)
	if phyChanTX == nil {
		phyChanTX = d.findChannel(phyDev, "voltage0", true)
	}
	if phyChanTX == nil {
		return fmt.Errorf("pluto: PHY TX channel not found (tried voltage3, voltage0)")
	}

	rate := int64(cfg.SampleRateHz)
	if rate < 2084000 {
		rate = 2084000
	}
	d.cfg.SampleRateHz = float64(rate)
	if err := d.writeChanAttrLL(phyChanTX, "sampling_frequency", rate); err != nil {
		return err
	}

	bw := rate
	if bw > 2000000 {
		bw = 2000000
	}
	if err := d.writeChanAttrLL(phyChanTX, "rf_bandwidth", bw); err != nil {
		return err
	}

	phyChanLO := d.findChannel(phyDev, "altvoltage1", true)
	d.chanLO = phyChanLO
	if phyChanLO != nil {
		freqHz := int64(cfg.CenterFreqHz)
		if freqHz <= 0 {
			freqHz = 100000000
		}
		if err := d.writeChanAttrLL(phyChanLO, "frequency", freqHz); err != nil {
			return err
		}
	}

	attenDB := int64(0)
	if cfg.GainDB > 0 {
		attenDB = -int64(89 - cfg.GainDB)
		if attenDB > 0 {
			attenDB = 0
		}
		if attenDB < -89 {
			attenDB = -89
		}
	}
	_ = d.writeChanAttrLL(phyChanTX, "hardwaregain", attenDB*1000)

	chanI := d.findChannel(txDev, "voltage0", true)
	chanQ := d.findChannel(txDev, "voltage1", true)
	if chanI == nil || chanQ == nil {
		return fmt.Errorf("pluto: TX I/Q channels not found on streaming device")
	}
	C.iio_channel_enable(chanI)
	C.iio_channel_enable(chanQ)
	d.chanI = chanI
	d.chanQ = chanQ

	d.bufSize = int(rate) / 20
	if d.bufSize < 4096 {
		d.bufSize = 4096
	}
	buf := C.iio_device_create_buffer(txDev, C.size_t(d.bufSize), C.bool(false))
	if buf == nil {
		return fmt.Errorf("pluto: failed to create TX buffer (size=%d)", d.bufSize)
	}
	d.buf = buf
	d.configured = true
	return nil
}

func (d *PlutoDriver) Capabilities(_ context.Context) (platform.DeviceCaps, error) {
	return platform.DeviceCaps{
		MinSampleRate: 521e3,
		MaxSampleRate: 61.44e6,
		HasGain:       true,
		GainMinDB:     -89,
		GainMaxDB:     0,
		Channels:      []int{0},
	}, nil
}

func (d *PlutoDriver) Start(_ context.Context) error {
	d.mu.Lock()
	defer d.mu.Unlock()
	if !d.configured {
		return fmt.Errorf("pluto: not configured")
	}
	if d.started {
		return fmt.Errorf("pluto: already started")
	}
	d.started = true
	return nil
}

func (d *PlutoDriver) Write(_ context.Context, frame *output.CompositeFrame) (int, error) {
	d.mu.Lock()
	buf := d.buf
	chanI := d.chanI
	chanQ := d.chanQ
	started := d.started
	bufSize := d.bufSize
	d.mu.Unlock()

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

	written := 0
	total := len(frame.Samples)

	for written < total {
		chunk := total - written
		if chunk > bufSize {
			chunk = bufSize
		}

		step := uintptr(C.iio_buffer_step(buf))
		if step == 0 {
			return written, fmt.Errorf("pluto: buffer step is 0")
		}

		ptrI := uintptr(C.iio_buffer_first(buf, chanI))
		ptrQ := uintptr(C.iio_buffer_first(buf, chanQ))
		if ptrI == 0 || ptrQ == 0 {
			return written, fmt.Errorf("pluto: buffer_first returned null")
		}

		end := uintptr(C.iio_buffer_end(buf))
		d.mu.Lock()
		if !d.layoutLogged {
			delta := int64(ptrQ) - int64(ptrI)
			span := int64(0)
			if end > ptrI {
				span = int64(end - ptrI)
			}
			log.Printf("pluto-linux: buffer layout step=%d ptrI=%#x ptrQ=%#x delta=%d end=%#x span=%d bufSize=%d", step, ptrI, ptrQ, delta, end, span, bufSize)
			d.layoutLogged = true
		}
		d.mu.Unlock()
		if end > 0 {
			bufSamples := int((end - ptrI) / step)
			if bufSamples > 0 && chunk > bufSamples {
				chunk = bufSamples
			}
		}

		fillStart := time.Now()
		for i := 0; i < chunk; i++ {
			s := frame.Samples[written+i]
			*(*int16)(unsafe.Pointer(ptrI)) = int16(s.I * 32767)
			*(*int16)(unsafe.Pointer(ptrQ)) = int16(s.Q * 32767)
			ptrI += step
			ptrQ += step
		}
		fillDur := time.Since(fillStart)

		pushStart := time.Now()
		pushed := int(C.iio_buffer_push(buf))
		pushDur := time.Since(pushStart)

		chunkBudget := time.Duration(0)
		if d.cfg.SampleRateHz > 0 {
			chunkBudget = time.Duration(float64(chunk) / d.cfg.SampleRateHz * float64(time.Second))
		}
		writeDur := fillDur + pushDur

		updateMaxDuration(&d.maxFillNs, fillDur)
		updateMaxDuration(&d.maxPushNs, pushDur)
		updateMaxDuration(&d.maxWriteNs, writeDur)

		slow := false
		if chunkBudget > 0 {
			if fillDur > chunkBudget/4 {
				d.slowFills.Add(1)
				slow = true
			}
			if pushDur > chunkBudget/2 {
				d.slowPushes.Add(1)
				slow = true
			}
			if writeDur > chunkBudget {
				d.slowWrites.Add(1)
				slow = true
			}
		}
		if slow {
			sw := d.slowWrites.Load()
			sf := d.slowFills.Load()
			sp := d.slowPushes.Load()
			if sw <= 5 || sf <= 5 || sp <= 5 || sw%20 == 0 || sf%20 == 0 || sp%20 == 0 {
				log.Printf("pluto-linux slow write: chunk=%d budget=%s fill=%s push=%s total=%s",
					chunk, chunkBudget, fillDur, pushDur, writeDur)
			}
		}

		if pushed < 0 {
			d.mu.Lock()
			d.lastError = fmt.Sprintf("buffer_push: %d", pushed)
			d.lastErrorAt = time.Now().UTC().Format(time.RFC3339)
			d.underruns.Add(1)
			d.mu.Unlock()
			return written, fmt.Errorf("pluto: buffer_push returned %d", pushed)
		}

		written += chunk
	}

	d.framesWritten.Add(1)
	d.samplesWritten.Add(uint64(written))
	return written, nil
}

func (d *PlutoDriver) Stop(_ context.Context) error {
	d.mu.Lock()
	defer d.mu.Unlock()
	d.started = false
	return nil
}

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

func (d *PlutoDriver) Tune(_ context.Context, freqHz float64) error {
	d.mu.Lock()
	defer d.mu.Unlock()
	if !d.configured || d.chanLO == nil {
		return fmt.Errorf("pluto: not configured or LO channel not available")
	}
	return d.writeChanAttrLL(d.chanLO, "frequency", int64(freqHz))
}

func (d *PlutoDriver) Close(_ context.Context) error {
	d.mu.Lock()
	defer d.mu.Unlock()
	d.started = false
	d.cleanup()
	return nil
}

func (d *PlutoDriver) Stats() platform.RuntimeStats {
	d.mu.Lock()
	defer d.mu.Unlock()
	return platform.RuntimeStats{
		TXEnabled:      d.started,
		StreamActive:   d.started && d.buf != nil,
		FramesWritten:  d.framesWritten.Load(),
		SamplesWritten: d.samplesWritten.Load(),
		Underruns:      d.underruns.Load(),
		SlowWrites:     d.slowWrites.Load(),
		SlowFills:      d.slowFills.Load(),
		SlowPushes:     d.slowPushes.Load(),
		LastError:      d.lastError,
		LastErrorAt:    d.lastErrorAt,
		EffectiveRate:  d.cfg.SampleRateHz,
		MaxWriteMs:     durationMs(d.maxWriteNs.Load()),
		MaxFillMs:      durationMs(d.maxFillNs.Load()),
		MaxPushMs:      durationMs(d.maxPushNs.Load()),
	}
}

func (d *PlutoDriver) cleanup() {
	if d.buf != nil {
		C.iio_buffer_destroy(d.buf)
		d.buf = nil
	}
	if d.chanI != nil {
		C.iio_channel_disable(d.chanI)
		d.chanI = nil
	}
	if d.chanQ != nil {
		C.iio_channel_disable(d.chanQ)
		d.chanQ = nil
	}
	d.chanLO = nil
	if d.ctx != nil {
		C.iio_context_destroy(d.ctx)
		d.ctx = nil
	}
	d.txDev = nil
	d.phyDev = nil
	d.configured = false
	d.layoutLogged = false
}

func (d *PlutoDriver) findDevice(name string) *C.struct_iio_device {
	if d.ctx == nil {
		return nil
	}
	cName := C.CString(name)
	defer C.free(unsafe.Pointer(cName))
	return C.iio_context_find_device(d.ctx, cName)
}

func (d *PlutoDriver) findChannel(dev *C.struct_iio_device, name string, isOutput bool) *C.struct_iio_channel {
	if dev == nil {
		return nil
	}
	cName := C.CString(name)
	defer C.free(unsafe.Pointer(cName))
	if isOutput {
		return C.iio_device_find_channel(dev, cName, C.bool(true))
	}
	return C.iio_device_find_channel(dev, cName, C.bool(false))
}

func (d *PlutoDriver) writeChanAttrLL(ch *C.struct_iio_channel, attr string, val int64) error {
	if ch == nil {
		return fmt.Errorf("pluto: channel missing for attr %s", attr)
	}
	cAttr := C.CString(attr)
	defer C.free(unsafe.Pointer(cAttr))
	ret := C.iio_channel_attr_write_longlong(ch, cAttr, C.longlong(val))
	if ret < 0 {
		return fmt.Errorf("pluto: write attr %s failed (rc=%d)", attr, int(ret))
	}
	return nil
}
