//go:build pluto && windows

// Package plutosdr provides a direct libiio-based TX driver for ADALM-Pluto.
// Pure Go on Windows — loads libiio.dll via syscall.LoadLibrary at runtime.
// No CGO, no C compiler required.
//
// Build: go build -tags pluto ./cmd/fmrtx
// Requires: libiio installed (https://github.com/analogdevicesinc/libiio/releases)
package plutosdr

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

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

// iioLib holds function pointers loaded from libiio.dll
type iioLib struct {
	dll *syscall.DLL

	// Context
	pCreateCtxFromURI  *syscall.Proc
	pDestroyCtx        *syscall.Proc

	// Device
	pCtxFindDevice     *syscall.Proc
	pDeviceFindChannel *syscall.Proc
	pChannelEnable     *syscall.Proc
	pChannelDisable    *syscall.Proc
	pChannelIsEnabled  *syscall.Proc

	// Attributes
	pChannelAttrWriteLongLong *syscall.Proc
	pChannelAttrWriteBool     *syscall.Proc
	pDeviceAttrWriteLongLong  *syscall.Proc

	// Buffer
	pCreateBuffer  *syscall.Proc
	pDestroyBuffer *syscall.Proc
	pBufferPush    *syscall.Proc
	pBufferStep    *syscall.Proc
	pBufferStart   *syscall.Proc
	pBufferEnd     *syscall.Proc
	pBufferFirst   *syscall.Proc
}

var dllSearchPaths = []string{
	"libiio",
	"iio",
	`C:\Program Files\libiio\libiio.dll`,
	`C:\Program Files (x86)\libiio\libiio.dll`,
}

func loadIIOLib() (*iioLib, error) {
	var dll *syscall.DLL
	var lastErr error
	for _, path := range dllSearchPaths {
		dll, lastErr = syscall.LoadDLL(path)
		if dll != nil {
			break
		}
	}
	if dll == nil {
		return nil, fmt.Errorf("cannot load libiio.dll: %v", lastErr)
	}

	p := func(name string) *syscall.Proc {
		proc, _ := dll.FindProc(name)
		return proc
	}

	return &iioLib{
		dll:                       dll,
		pCreateCtxFromURI:         p("iio_create_context_from_uri"),
		pDestroyCtx:               p("iio_context_destroy"),
		pCtxFindDevice:            p("iio_context_find_device"),
		pDeviceFindChannel:        p("iio_device_find_channel"),
		pChannelEnable:            p("iio_channel_enable"),
		pChannelDisable:           p("iio_channel_disable"),
		pChannelIsEnabled:         p("iio_channel_is_enabled"),
		pChannelAttrWriteLongLong: p("iio_channel_attr_write_longlong"),
		pChannelAttrWriteBool:     p("iio_channel_attr_write_bool"),
		pDeviceAttrWriteLongLong:  p("iio_device_attr_write_longlong"),
		pCreateBuffer:             p("iio_device_create_buffer"),
		pDestroyBuffer:            p("iio_buffer_destroy"),
		pBufferPush:               p("iio_buffer_push"),
		pBufferStep:               p("iio_buffer_step"),
		pBufferStart:              p("iio_buffer_start"),
		pBufferEnd:                p("iio_buffer_end"),
		pBufferFirst:              p("iio_buffer_first"),
	}, nil
}

// --- Driver ---

type PlutoDriver struct {
	mu  sync.Mutex
	lib *iioLib
	cfg platform.SoapyConfig

	ctx      uintptr // iio_context*
	txDev    uintptr // iio_device* (cf-ad9361-dds-core-lpc)
	phyDev   uintptr // iio_device* (ad9361-phy)
	chanI    uintptr // iio_channel* TX I
	chanQ    uintptr // iio_channel* TX Q
	chanLO   uintptr // iio_channel* TX LO (altvoltage1), cached for Tune()
	buf      uintptr // iio_buffer*
	bufSize  int     // samples per buffer push

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

func NewPlutoDriver() platform.SoapyDriver {
	lib, err := loadIIOLib()
	if err != nil {
		return &PlutoDriver{initError: err.Error()}
	}
	return &PlutoDriver{lib: lib}
}

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()

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

	// Cleanup existing
	d.cleanup()
	d.cfg = cfg

	// Create IIO context via USB
	uri := "usb:"
	if cfg.Device != "" && cfg.Device != "plutosdr" {
		uri = cfg.Device // allow "ip:192.168.2.1" or specific USB
	}
	ctx, err := d.createContext(uri)
	if err != nil {
		return err
	}
	d.ctx = ctx

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

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

	// --- AD9361 PHY configuration ---
	// The AD9364 (PlutoSDR) has TX on voltage3 (output), not voltage0
	// voltage0 = RX input, voltage3 = TX output on single-channel AD9364

	// Find TX PHY output channel
	phyChanTX := d.findChannel(phyDev, "voltage3", true) // output=true
	if phyChanTX == 0 {
		// Fallback for dual-channel AD9361: try voltage0 output
		phyChanTX = d.findChannel(phyDev, "voltage0", true)
	}
	if phyChanTX == 0 {
		return fmt.Errorf("pluto: PHY TX channel not found (tried voltage3, voltage0)")
	}

	// Sample rate — AD9361 minimum is ~2.084 MHz.
	// We set the hardware to this minimum and resample from composite rate.
	rate := int64(cfg.SampleRateHz)
	if rate < 2084000 {
		rate = 2084000 // AD9361 minimum
	}
	// Update effective rate so the engine knows the real device rate
	d.cfg.SampleRateHz = float64(rate)

	d.writeChanAttrLL(phyChanTX, "sampling_frequency", rate)

	// RF bandwidth — set to match our signal bandwidth (wider than composite)
	bw := rate
	if bw > 2000000 {
		bw = 2000000 // 2 MHz BW is enough for FM broadcast
	}
	d.writeChanAttrLL(phyChanTX, "rf_bandwidth", bw)

	// TX LO frequency
	phyChanLO := d.findChannel(phyDev, "altvoltage1", true) // TX LO
	d.chanLO = phyChanLO // cache for Tune()
	if phyChanLO != 0 {
		freqHz := int64(cfg.CenterFreqHz)
		if freqHz <= 0 {
			freqHz = 100000000 // 100 MHz default
		}
		d.writeChanAttrLL(phyChanLO, "frequency", freqHz)
	}

	// TX gain/attenuation
	// PlutoSDR TX hardwaregain: 0 dB = max power, -89.75 dB = min
	// Value is in dB (not millidB) as a negative number
	// For max power: set to 0. For safety: set to -20 or so.
	// cfg.GainDB from our config is 0..89 positive, we negate it and subtract from 0
	attenDB := int64(0) // default = max power
	if cfg.GainDB > 0 {
		// GainDB=89 means full attenuation, GainDB=0 means max power
		attenDB = -int64(89 - cfg.GainDB)
		if attenDB > 0 {
			attenDB = 0
		}
		if attenDB < -89 {
			attenDB = -89
		}
	}
	d.writeChanAttrLL(phyChanTX, "hardwaregain", attenDB*1000) // millidB

	// --- TX streaming channels on cf-ad9361-dds-core-lpc ---
	// voltage0 (I) and voltage1 (Q) are output channels
	chanI := d.findChannel(txDev, "voltage0", true)
	chanQ := d.findChannel(txDev, "voltage1", true)
	if chanI == 0 || chanQ == 0 {
		return fmt.Errorf("pluto: TX I/Q channels not found on streaming device")
	}
	d.enableChannel(chanI)
	d.enableChannel(chanQ)
	d.chanI = chanI
	d.chanQ = chanQ

	// Create buffer — samples per push (per channel)
	// At 2.084 MHz with 50ms chunks = 104200 samples. Buffer must fit this.
	d.bufSize = int(rate) / 20 // 50ms worth
	if d.bufSize < 4096 {
		d.bufSize = 4096
	}
	// libiio can handle large buffers; no artificial cap needed
	buf := d.createBuffer(txDev, d.bufSize, false)
	if buf == 0 {
		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()
	lib := d.lib
	buf := d.buf
	chanI := d.chanI
	chanQ := d.chanQ
	started := d.started
	bufSize := d.bufSize
	d.mu.Unlock()

	if !started || buf == 0 || lib == 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 := d.bufferStep(buf)
		if step == 0 {
			return written, fmt.Errorf("pluto: buffer step is 0")
		}

		// iio_buffer_first gives the pointer to the first sample for each channel.
		// For interleaved buffers (step=4 with 2x int16), ptrI and ptrQ may
		// differ by 2 bytes (I at offset 0, Q at offset 2 within each step).
		// For non-interleaved, they point to separate memory regions.
		ptrI := d.bufferFirst(buf, chanI)
		ptrQ := d.bufferFirst(buf, chanQ)
		if ptrI == 0 || ptrQ == 0 {
			return written, fmt.Errorf("pluto: buffer_first returned null (I=%d Q=%d)", ptrI, ptrQ)
		}

		end := d.bufferEnd(buf)
		if end > 0 {
			bufSamples := int((end - ptrI) / uintptr(step))
			if bufSamples > 0 && chunk > bufSamples {
				chunk = bufSamples
			}
		}

		for i := 0; i < chunk; i++ {
			s := frame.Samples[written+i]
			// Scale float32 [-1,+1] to int16 [-32767,+32767]
			*(*int16)(unsafe.Pointer(ptrI)) = int16(s.I * 32767)
			*(*int16)(unsafe.Pointer(ptrQ)) = int16(s.Q * 32767)
			ptrI += uintptr(step)
			ptrQ += uintptr(step)
		}

		pushed := d.bufferPush(buf)
		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 == 0 {
		return fmt.Errorf("pluto: not configured or LO channel not available")
	}
	if d.lib.pChannelAttrWriteLongLong == nil {
		return fmt.Errorf("pluto: iio_channel_attr_write_longlong not loaded")
	}
	cAttr, _ := syscall.BytePtrFromString("frequency")
	ret, _, _ := d.lib.pChannelAttrWriteLongLong.Call(
		d.chanLO,
		uintptr(unsafe.Pointer(cAttr)),
		uintptr(int64(freqHz)),
	)
	if int32(ret) < 0 {
		return fmt.Errorf("pluto: LO tune to %.0f Hz failed (iio rc=%d)", freqHz, int32(ret))
	}
	return nil
}

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 != 0,
		FramesWritten:  d.framesWritten.Load(),
		SamplesWritten: d.samplesWritten.Load(),
		Underruns:      d.underruns.Load(),
		LastError:      d.lastError,
		LastErrorAt:    d.lastErrorAt,
		EffectiveRate:  d.cfg.SampleRateHz,
	}
}

// --- internal helpers ---

func (d *PlutoDriver) cleanup() {
	if d.buf != 0 && d.lib.pDestroyBuffer != nil {
		d.lib.pDestroyBuffer.Call(d.buf)
		d.buf = 0
	}
	if d.chanI != 0 {
		d.disableChannel(d.chanI)
		d.chanI = 0
	}
	if d.chanQ != 0 {
		d.disableChannel(d.chanQ)
		d.chanQ = 0
	}
	d.chanLO = 0 // config-only channel, no disable needed
	if d.ctx != 0 && d.lib.pDestroyCtx != nil {
		d.lib.pDestroyCtx.Call(d.ctx)
		d.ctx = 0
	}
	d.configured = false
}

func (d *PlutoDriver) createContext(uri string) (uintptr, error) {
	if d.lib.pCreateCtxFromURI == nil {
		return 0, fmt.Errorf("iio_create_context_from_uri not found")
	}
	cURI, _ := syscall.BytePtrFromString(uri)
	ret, _, _ := d.lib.pCreateCtxFromURI.Call(uintptr(unsafe.Pointer(cURI)))
	if ret == 0 {
		return 0, fmt.Errorf("pluto: failed to create IIO context (uri=%s)", uri)
	}
	return ret, nil
}

func (d *PlutoDriver) findDevice(name string) uintptr {
	if d.lib.pCtxFindDevice == nil || d.ctx == 0 {
		return 0
	}
	cName, _ := syscall.BytePtrFromString(name)
	ret, _, _ := d.lib.pCtxFindDevice.Call(d.ctx, uintptr(unsafe.Pointer(cName)))
	return ret
}

func (d *PlutoDriver) findChannel(dev uintptr, name string, isOutput bool) uintptr {
	if d.lib.pDeviceFindChannel == nil || dev == 0 {
		return 0
	}
	cName, _ := syscall.BytePtrFromString(name)
	out := uintptr(0)
	if isOutput {
		out = 1
	}
	ret, _, _ := d.lib.pDeviceFindChannel.Call(dev, uintptr(unsafe.Pointer(cName)), out)
	return ret
}

func (d *PlutoDriver) enableChannel(ch uintptr) {
	if d.lib.pChannelEnable != nil && ch != 0 {
		d.lib.pChannelEnable.Call(ch)
	}
}

func (d *PlutoDriver) disableChannel(ch uintptr) {
	if d.lib.pChannelDisable != nil && ch != 0 {
		d.lib.pChannelDisable.Call(ch)
	}
}

func (d *PlutoDriver) writeChanAttrLL(ch uintptr, attr string, val int64) {
	if d.lib.pChannelAttrWriteLongLong == nil || ch == 0 {
		return
	}
	cAttr, _ := syscall.BytePtrFromString(attr)
	d.lib.pChannelAttrWriteLongLong.Call(ch, uintptr(unsafe.Pointer(cAttr)), uintptr(val))
}

func (d *PlutoDriver) writeDevAttrLL(dev uintptr, attr string, val int64) {
	if d.lib.pDeviceAttrWriteLongLong == nil || dev == 0 {
		return
	}
	cAttr, _ := syscall.BytePtrFromString(attr)
	d.lib.pDeviceAttrWriteLongLong.Call(dev, uintptr(unsafe.Pointer(cAttr)), uintptr(val))
}

func (d *PlutoDriver) createBuffer(dev uintptr, sampleCount int, cyclic bool) uintptr {
	if d.lib.pCreateBuffer == nil || dev == 0 {
		return 0
	}
	c := uintptr(0)
	if cyclic {
		c = 1
	}
	ret, _, _ := d.lib.pCreateBuffer.Call(dev, uintptr(sampleCount), c)
	return ret
}

func (d *PlutoDriver) bufferPush(buf uintptr) int {
	if d.lib.pBufferPush == nil || buf == 0 {
		return -1
	}
	ret, _, _ := d.lib.pBufferPush.Call(buf)
	return int(int32(ret))
}

func (d *PlutoDriver) bufferStart(buf uintptr) uintptr {
	if d.lib.pBufferStart == nil {
		return 0
	}
	ret, _, _ := d.lib.pBufferStart.Call(buf)
	return ret
}

func (d *PlutoDriver) bufferEnd(buf uintptr) uintptr {
	if d.lib.pBufferEnd == nil {
		return 0
	}
	ret, _, _ := d.lib.pBufferEnd.Call(buf)
	return ret
}

func (d *PlutoDriver) bufferStep(buf uintptr) uintptr {
	if d.lib.pBufferStep == nil {
		return 0
	}
	ret, _, _ := d.lib.pBufferStep.Call(buf)
	return ret
}

func (d *PlutoDriver) bufferFirst(buf uintptr, ch uintptr) uintptr {
	if d.lib.pBufferFirst == nil {
		return 0
	}
	ret, _, _ := d.lib.pBufferFirst.Call(buf, ch)
	return ret
}
