//go:build windows

// Package audio drives Windows audio endpoints via WASAPI (through go-ole /
// go-wca) — pure Go, no CGO, the same COM stack OmniRig already uses. It
// powers the Digital Voice Keyer (record/play voice messages to the rig) and
// the QSO recorder (rolling-buffer capture saved as WAV).
package audio

import (
	"fmt"
	"runtime"
	"sort"

	"github.com/go-ole/go-ole"
	"github.com/moutend/go-wca/pkg/wca"
)

// Device is one audio endpoint (a capture input or a render output).
type Device struct {
	ID      string `json:"id"`      // stable WASAPI endpoint id (persisted)
	Name    string `json:"name"`    // friendly name shown in dropdowns
	Default bool   `json:"default"` // is this the system default endpoint
}

// ListInputDevices returns the active capture endpoints — microphones,
// line-in, and the soundcard input wired to the rig's audio out ("From Radio").
func ListInputDevices() ([]Device, error) { return listEndpoints(wca.ECapture) }

// ListOutputDevices returns the active render endpoints — speakers and the
// soundcard output wired to the rig's mic/data input ("To Radio").
func ListOutputDevices() ([]Device, error) { return listEndpoints(wca.ERender) }

// listEndpoints enumerates active endpoints for a data-flow direction. COM is
// thread-affine, so we lock the OS thread and Co(Un)Initialize around the work
// — this is a one-shot call from a Wails binding, not a long-lived session.
func listEndpoints(flow uint32) (out []Device, err error) {
	runtime.LockOSThread()
	defer runtime.UnlockOSThread()

	if e := ole.CoInitializeEx(0, ole.COINIT_APARTMENTTHREADED); e != nil {
		// 0x1 = S_FALSE → already initialised on this thread, fine.
		if oe, ok := e.(*ole.OleError); !ok || oe.Code() != 0x00000001 {
			return nil, fmt.Errorf("CoInitializeEx: %w", e)
		}
	}
	defer ole.CoUninitialize()

	var mmde *wca.IMMDeviceEnumerator
	if e := wca.CoCreateInstance(wca.CLSID_MMDeviceEnumerator, 0, wca.CLSCTX_ALL,
		wca.IID_IMMDeviceEnumerator, &mmde); e != nil {
		return nil, fmt.Errorf("create MMDeviceEnumerator: %w", e)
	}
	defer mmde.Release()

	// Record the default endpoint id so the UI can flag it.
	var defID string
	var defDev *wca.IMMDevice
	if e := mmde.GetDefaultAudioEndpoint(flow, wca.EConsole, &defDev); e == nil && defDev != nil {
		_ = defDev.GetId(&defID)
		defDev.Release()
	}

	var coll *wca.IMMDeviceCollection
	if e := mmde.EnumAudioEndpoints(flow, wca.DEVICE_STATE_ACTIVE, &coll); e != nil {
		return nil, fmt.Errorf("enum endpoints: %w", e)
	}
	defer coll.Release()

	var count uint32
	if e := coll.GetCount(&count); e != nil {
		return nil, fmt.Errorf("count endpoints: %w", e)
	}
	for i := uint32(0); i < count; i++ {
		var dev *wca.IMMDevice
		if coll.Item(i, &dev) != nil || dev == nil {
			continue
		}
		var id string
		_ = dev.GetId(&id)
		name := endpointName(dev, id)
		dev.Release()
		out = append(out, Device{ID: id, Name: name, Default: id != "" && id == defID})
	}
	sort.SliceStable(out, func(i, j int) bool { return out[i].Name < out[j].Name })
	return out, nil
}

// endpointName reads PKEY_Device_FriendlyName, falling back to the raw id.
func endpointName(dev *wca.IMMDevice, fallback string) string {
	var ps *wca.IPropertyStore
	if dev.OpenPropertyStore(wca.STGM_READ, &ps) != nil || ps == nil {
		return fallback
	}
	defer ps.Release()
	var pv wca.PROPVARIANT
	if ps.GetValue(&wca.PKEY_Device_FriendlyName, &pv) != nil {
		return fallback
	}
	if s := pv.String(); s != "" {
		return s
	}
	return fallback
}

// DeviceName resolves an endpoint id to its friendly name.
//
// Diagnostics quote the id that was CONFIGURED, which is a GUID — an operator
// told "no audio at all from {0.0.1.00000000}.{6a27abfd…}" learns nothing they
// can act on, while "no audio at all from DAX RX 1 (FlexRadio DAX)" points
// straight at the DAX panel.
//
// Falls back to the id when the endpoint cannot be found, which is itself worth
// seeing: a device that has disappeared explains an empty recording too.
func DeviceName(id string) string {
	if id == "" {
		return "(none)"
	}
	for _, list := range []func() ([]Device, error){ListInputDevices, ListOutputDevices} {
		devs, err := list()
		if err != nil {
			continue
		}
		for _, d := range devs {
			if d.ID == id {
				return d.Name
			}
		}
	}
	return id
}
