//go:build windows

// Denkovi USB 8-channel relay board (FT245RL). Despite enumerating as a virtual
// COM port, this board is NOT driven by serial/ASCII: the FT245's 8 data lines
// each drive a relay, controlled through FTDI's D2XX "bit-bang" mode. One byte
// written = the 8 relays at once (bit 0 = relay 1). We load ftd2xx.dll at runtime
// (no CGO) and call the D2XX API directly, exactly as the vendor's tool does
// (which addresses the board by its FTDI serial, e.g. "DAE0006K").
package relaydev

import (
	"context"
	"fmt"
	"strings"
	"sync"
	"syscall"
	"unsafe"
)

var (
	ftdll             = syscall.NewLazyDLL("ftd2xx.dll")
	procOpenEx        = ftdll.NewProc("FT_OpenEx")
	procClose         = ftdll.NewProc("FT_Close")
	procSetBitMode    = ftdll.NewProc("FT_SetBitMode")
	procSetBaudRate   = ftdll.NewProc("FT_SetBaudRate")
	procWrite         = ftdll.NewProc("FT_Write")
	procPurge         = ftdll.NewProc("FT_Purge")
	procCreateInfo    = ftdll.NewProc("FT_CreateDeviceInfoList")
	procGetInfoDetail = ftdll.NewProc("FT_GetDeviceInfoDetail")
)

const (
	ftOpenBySerial  = 1    // FT_OPEN_BY_SERIAL_NUMBER
	ftBitModeSyncBB = 0x04 // FT_BITMODE_SYNC_BITBANG (the mode Denkovi documents)
	ftPurgeRX       = 1    // FT_PURGE_RX
)

func ftOK(r uintptr) bool { return r == 0 } // FT_OK == 0

type denkovi struct {
	serial string
	count  int
	mu     sync.Mutex
	shadow byte    // last output byte (bit n = relay n+1); authoritative state
	h      uintptr // FT handle
	opened bool
}

// NewDenkovi builds a driver for a Denkovi USB relay board (4 or 8 relays)
// identified by its FTDI serial number (shown by the vendor tool / FT_PROG,
// e.g. "DAE0006K"). count defaults to 8; a 4-relay board just uses the low 4 bits.
func NewDenkovi(serial string, count int) Device {
	if count != 4 && count != 8 {
		count = 8
	}
	return &denkovi{serial: strings.TrimSpace(serial), count: count}
}

func (d *denkovi) Count() int { return d.count }

// ensureOpen opens the board and puts it in synchronous bit-bang mode with all 8
// lines as outputs. Idempotent.
func (d *denkovi) ensureOpen() error {
	if d.opened {
		return nil
	}
	if err := ftdll.Load(); err != nil {
		return fmt.Errorf("FTDI D2XX driver (ftd2xx.dll) not found — install the FTDI D2XX driver / Denkovi software: %w", err)
	}
	if d.serial == "" {
		return fmt.Errorf("no FTDI serial number set for the Denkovi board")
	}
	ser, err := syscall.BytePtrFromString(d.serial)
	if err != nil {
		return err
	}
	var h uintptr
	if r, _, _ := procOpenEx.Call(uintptr(unsafe.Pointer(ser)), ftOpenBySerial, uintptr(unsafe.Pointer(&h))); !ftOK(r) {
		return fmt.Errorf("cannot open Denkovi board %q (FT_OpenEx status %d) — is it connected and not in use by another app?", d.serial, r)
	}
	// All 8 lines output, synchronous bit-bang.
	if r, _, _ := procSetBitMode.Call(h, 0xFF, ftBitModeSyncBB); !ftOK(r) {
		procClose.Call(h)
		return fmt.Errorf("FT_SetBitMode failed (status %d)", r)
	}
	procSetBaudRate.Call(h, 9600) // bit-bang pin-update clock; relays don't need speed
	d.h = h
	d.opened = true
	// Make the hardware match our shadow (starts all-off on first open).
	return d.writeLocked()
}

// writeLocked pushes the shadow byte to the relays. Caller holds d.mu.
func (d *denkovi) writeLocked() error {
	var written uint32
	b := d.shadow
	if r, _, _ := procWrite.Call(d.h, uintptr(unsafe.Pointer(&b)), 1, uintptr(unsafe.Pointer(&written))); !ftOK(r) {
		return fmt.Errorf("FT_Write failed (status %d)", r)
	}
	// Synchronous bit-bang echoes each written byte into the RX buffer; drop it so
	// it doesn't fill over the life of the connection.
	procPurge.Call(d.h, ftPurgeRX)
	return nil
}

func (d *denkovi) Set(ctx context.Context, relay int, on bool) error {
	if relay < 1 || relay > d.count {
		return fmt.Errorf("relay %d out of range 1..%d", relay, d.count)
	}
	d.mu.Lock()
	defer d.mu.Unlock()
	if err := d.ensureOpen(); err != nil {
		return err
	}
	bit := byte(1) << uint(relay-1) // relay 1 → bit 0
	if on {
		d.shadow |= bit
	} else {
		d.shadow &^= bit
	}
	return d.writeLocked()
}

// Close releases the FTDI handle so the board can be reopened later (only one
// handle may hold a D2XX device at a time).
func (d *denkovi) Close() error {
	d.mu.Lock()
	defer d.mu.Unlock()
	if d.opened {
		procClose.Call(d.h)
		d.opened = false
		d.h = 0
	}
	return nil
}

func (d *denkovi) Status(ctx context.Context) ([]bool, error) {
	d.mu.Lock()
	defer d.mu.Unlock()
	if err := d.ensureOpen(); err != nil {
		return nil, err
	}
	out := make([]bool, d.count)
	for i := 0; i < d.count; i++ {
		out[i] = d.shadow&(byte(1)<<uint(i)) != 0
	}
	return out, nil
}

// ListDenkovi returns the FTDI serial numbers of connected devices, for the
// settings UI to pick from. Requires ftd2xx.dll.
func ListDenkovi() ([]string, error) {
	if err := ftdll.Load(); err != nil {
		return nil, fmt.Errorf("FTDI D2XX driver (ftd2xx.dll) not found — install the FTDI D2XX driver / Denkovi software")
	}
	var n uint32
	if r, _, _ := procCreateInfo.Call(uintptr(unsafe.Pointer(&n))); !ftOK(r) {
		return nil, fmt.Errorf("FT_CreateDeviceInfoList failed (status %d)", r)
	}
	var out []string
	for i := uint32(0); i < n; i++ {
		var flags, typ, id, loc uint32
		serial := make([]byte, 16)
		desc := make([]byte, 64)
		var h uintptr
		r, _, _ := procGetInfoDetail.Call(
			uintptr(i),
			uintptr(unsafe.Pointer(&flags)), uintptr(unsafe.Pointer(&typ)),
			uintptr(unsafe.Pointer(&id)), uintptr(unsafe.Pointer(&loc)),
			uintptr(unsafe.Pointer(&serial[0])), uintptr(unsafe.Pointer(&desc[0])),
			uintptr(unsafe.Pointer(&h)),
		)
		if !ftOK(r) {
			continue
		}
		if s := cstr(serial); s != "" {
			out = append(out, s)
		}
	}
	return out, nil
}

// cstr trims a C string (up to the first NUL) from a fixed buffer.
func cstr(b []byte) string {
	if i := indexByte(b, 0); i >= 0 {
		b = b[:i]
	}
	return strings.TrimSpace(string(b))
}

func indexByte(b []byte, c byte) int {
	for i := range b {
		if b[i] == c {
			return i
		}
	}
	return -1
}
