//go:build windows

package rdp

import (
	"fmt"
	"sync"
	"syscall"
	"time"
	"unsafe"
)

var (
	libfreerdp       = syscall.NewLazyDLL("libfreerdp3.dll")
	libfreerdpClient = syscall.NewLazyDLL("libfreerdp-client3.dll")

	// Instance lifecycle
	procFreerdpNew        = libfreerdp.NewProc("freerdp_new")
	procFreerdpFree       = libfreerdp.NewProc("freerdp_free")
	procFreerdpConnect    = libfreerdp.NewProc("freerdp_connect")
	procFreerdpDisconnect = libfreerdp.NewProc("freerdp_disconnect")

	// Context
	procContextNew = libfreerdp.NewProc("freerdp_context_new")

	// Settings
	procSettingsSetString = libfreerdp.NewProc("freerdp_settings_set_string")
	procSettingsSetUint32 = libfreerdp.NewProc("freerdp_settings_set_uint32")
	procSettingsSetBool   = libfreerdp.NewProc("freerdp_settings_set_bool")

	// Input
	procInputSendMouse    = libfreerdp.NewProc("freerdp_input_send_mouse_event")
	procInputSendKeyboard = libfreerdp.NewProc("freerdp_input_send_keyboard_event")

	// Event loop — NOTE: takes rdpContext*, not freerdp*
	procCheckEventHandles = libfreerdp.NewProc("freerdp_check_event_handles")

	// GDI subsystem (libfreerdp3.dll exports these)
	procGdiInit = libfreerdp.NewProc("gdi_init")
	procGdiFree = libfreerdp.NewProc("gdi_free")

	// Client helpers
	procClientNew  = libfreerdpClient.NewProc("freerdp_client_context_new")
	procClientFree = libfreerdpClient.NewProc("freerdp_client_context_free")
)

// ============================================================================
// FreeRDP3 settings IDs
//
// Source: FreeRDP 3.10.3 — include/freerdp/settings_types_private.h
// Each ID is the ALIGN64 slot index from the rdp_settings struct.
// ============================================================================
const (
	FreeRDP_ServerHostname     = 20
	FreeRDP_ServerPort         = 21
	FreeRDP_Username           = 22
	FreeRDP_Password           = 23
	FreeRDP_Domain             = 24
	FreeRDP_DesktopWidth       = 1025
	FreeRDP_DesktopHeight      = 1026
	FreeRDP_ColorDepth         = 1027
	FreeRDP_FullscreenMode     = 1028
	FreeRDP_SoftwareGdi        = 1601 // BOOL — enable software GDI rendering
	FreeRDP_IgnoreCertificate  = 4556
	FreeRDP_AuthenticationOnly = 4554
	FreeRDP_NlaSecurity        = 4560
	FreeRDP_TlsSecurity        = 4561
	FreeRDP_RdpSecurity        = 4562
)

// Keyboard event flags for FreeRDP input calls.
const (
	KBD_FLAGS_EXTENDED = 0x0100
	KBD_FLAGS_DOWN     = 0x4000
	KBD_FLAGS_RELEASE  = 0x8000
)

// ============================================================================
// FreeRDP3 struct offsets — x86_64 / Windows amd64
//
// IMPORTANT: These offsets are specific to FreeRDP 3.10.3 compiled with
// WITH_FREERDP_DEPRECATED=OFF (the default, and what our CI uses).
//
// Every ALIGN64 slot is 8 bytes. The slot number from the FreeRDP headers
// gives the byte offset as: slot * 8.
//
// Source: FreeRDP 3.10.3 — include/freerdp/freerdp.h, include/freerdp/update.h
// ============================================================================
const (
	// rdp_freerdp struct (ALIGN64 slots, 8 bytes each)
	//   slot 0:  context (rdpContext*)
	//   slot 48: PreConnect callback (pConnectCallback)
	//   slot 49: PostConnect callback (pConnectCallback)
	//   slot 55: PostDisconnect callback (pPostDisconnect)
	freerdpOffsetContext        = 0 * 8  // rdpContext* — slot 0
	freerdpOffsetPreConnect     = 48 * 8 // pConnectCallback — slot 48
	freerdpOffsetPostConnect    = 49 * 8 // pConnectCallback — slot 49
	freerdpOffsetPostDisconnect = 55 * 8 // pPostDisconnect — slot 55

	// rdp_context struct (ALIGN64 slots, 8 bytes each)
	//   slot 33: gdi (rdpGdi*)
	//   slot 38: input (rdpInput*)
	//   slot 39: update (rdpUpdate*)
	//   slot 40: settings (rdpSettings*)
	contextOffsetGdi      = 33 * 8 // rdpGdi*
	contextOffsetInput    = 38 * 8 // rdpInput*
	contextOffsetUpdate   = 39 * 8 // rdpUpdate*
	contextOffsetSettings = 40 * 8 // rdpSettings*

	// rdp_update struct (ALIGN64 slots, 8 bytes each)
	//   slot 21: BitmapUpdate callback (pBitmapUpdate)
	updateOffsetBitmapUpdate = 21 * 8

	// rdpGdi struct (NOT ALIGN64 — regular C struct with natural alignment)
	// On x86_64:
	//   offset  0: rdpContext* context   (8 bytes)
	//   offset  8: INT32 width           (4 bytes)
	//   offset 12: INT32 height          (4 bytes)
	//   offset 16: UINT32 stride         (4 bytes)
	//   offset 20: UINT32 dstFormat      (4 bytes)
	//   offset 24: UINT32 cursor_x       (4 bytes)
	//   offset 28: UINT32 cursor_y       (4 bytes)
	//   offset 32: HGDI_DC hdc           (8 bytes, pointer)
	//   offset 40: gdiBitmap* primary    (8 bytes)
	//   offset 48: gdiBitmap* drawing    (8 bytes)
	//   offset 56: UINT32 bitmap_size    (4 bytes)
	//   offset 60: UINT32 bitmap_stride  (4 bytes)
	//   offset 64: BYTE* primary_buffer  (8 bytes)
	gdiOffsetWidth         = 8
	gdiOffsetHeight        = 12
	gdiOffsetStride        = 16
	gdiOffsetPrimaryBuffer = 64
)

// Pixel format constants from FreeRDP3 codec/color.h.
// Formula: (bpp << 24) | (type << 16) | (a << 12) | (r << 8) | (g << 4) | b
// BGRA type = 4, RGBA type = 3
const (
	PIXEL_FORMAT_BGRA32 = 0x20040888
)

// ============================================================================
// Instance-to-backend registry
//
// syscall.NewCallback produces a bare C function pointer — it cannot capture
// Go closures. We use a global map keyed by the freerdp instance pointer to
// recover the FreeRDPBackend from inside callbacks.
// ============================================================================
var (
	instanceRegistry   = make(map[uintptr]*FreeRDPBackend)
	instanceRegistryMu sync.RWMutex
)

func registerInstance(instance uintptr, backend *FreeRDPBackend) {
	instanceRegistryMu.Lock()
	instanceRegistry[instance] = backend
	instanceRegistryMu.Unlock()
}

func unregisterInstance(instance uintptr) {
	instanceRegistryMu.Lock()
	delete(instanceRegistry, instance)
	instanceRegistryMu.Unlock()
}

func lookupInstance(instance uintptr) *FreeRDPBackend {
	instanceRegistryMu.RLock()
	b := instanceRegistry[instance]
	instanceRegistryMu.RUnlock()
	return b
}

// ============================================================================
// Unsafe pointer helpers
// ============================================================================

// readPtr reads a pointer-sized value at the given byte offset from base.
func readPtr(base uintptr, offsetBytes uintptr) uintptr {
	return *(*uintptr)(unsafe.Pointer(base + offsetBytes))
}

// writePtr writes a pointer-sized value at the given byte offset from base.
func writePtr(base uintptr, offsetBytes uintptr, val uintptr) {
	*(*uintptr)(unsafe.Pointer(base + offsetBytes)) = val
}

// readU32 reads a uint32 at the given byte offset from base.
func readU32(base uintptr, offsetBytes uintptr) uint32 {
	return *(*uint32)(unsafe.Pointer(base + offsetBytes))
}

// ============================================================================
// Callbacks — registered via syscall.NewCallback (stdcall on Windows)
//
// FreeRDP callback signatures (from freerdp.h):
//   typedef BOOL (*pConnectCallback)(freerdp* instance);
//   typedef BOOL (*pBitmapUpdate)(rdpContext* context, const BITMAP_UPDATE* bitmap);
// ============================================================================

// postConnectCallback is the global PostConnect handler. FreeRDP calls this
// after the RDP connection is fully established. We use it to:
//   1. Initialize the GDI subsystem (software rendering into a memory buffer)
//   2. Extract the GDI primary_buffer pointer for frame capture
//   3. Register the BitmapUpdate callback for partial screen updates
var postConnectCallbackPtr = syscall.NewCallback(postConnectCallback)

func postConnectCallback(instance uintptr) uintptr {
	backend := lookupInstance(instance)
	if backend == nil {
		return 0 // FALSE — unknown instance
	}

	// Read context from instance->context (slot 0).
	context := readPtr(instance, freerdpOffsetContext)
	if context == 0 {
		return 0
	}

	// Initialize the GDI subsystem with BGRA32 pixel format.
	// gdi_init(freerdp* instance, UINT32 format) -> BOOL
	// This allocates the primary surface and registers internal paint callbacks.
	ret, _, _ := procGdiInit.Call(instance, uintptr(PIXEL_FORMAT_BGRA32))
	if ret == 0 {
		return 0 // gdi_init failed
	}

	// Read the rdpGdi pointer from context->gdi (slot 33).
	gdi := readPtr(context, contextOffsetGdi)
	if gdi == 0 {
		return 0
	}

	// Extract frame dimensions and primary buffer pointer from rdpGdi.
	gdiWidth := readU32(gdi, gdiOffsetWidth)
	gdiHeight := readU32(gdi, gdiOffsetHeight)
	gdiStride := readU32(gdi, gdiOffsetStride)
	primaryBuf := readPtr(gdi, gdiOffsetPrimaryBuffer)

	if primaryBuf == 0 {
		return 0
	}

	// Store the GDI surface info on the backend for frame reads.
	backend.gdiPrimaryBuf = primaryBuf
	backend.gdiWidth = int(gdiWidth)
	backend.gdiHeight = int(gdiHeight)
	backend.gdiStride = int(gdiStride)

	// Re-create the pixel buffer if GDI negotiated a different resolution
	// (the server may reject our requested size).
	if int(gdiWidth) != backend.buffer.Width || int(gdiHeight) != backend.buffer.Height {
		backend.buffer = NewPixelBuffer(int(gdiWidth), int(gdiHeight))
	}

	// Register the BitmapUpdate callback on update->BitmapUpdate (slot 21).
	// With GDI mode enabled, FreeRDP's internal GDI layer handles most
	// rendering via BeginPaint/EndPaint. The BitmapUpdate callback fires for
	// uncompressed bitmap transfers that bypass GDI. We register it as a
	// safety net to capture any frames that arrive through this path.
	update := readPtr(context, contextOffsetUpdate)
	if update != 0 {
		writePtr(update, updateOffsetBitmapUpdate, bitmapUpdateCallbackPtr)
	}

	backend.gdiReady = true
	return 1 // TRUE — success
}

// preConnectCallback is called before the connection is fully established.
// We use it to enable SoftwareGdi mode so FreeRDP handles bitmap decoding.
var preConnectCallbackPtr = syscall.NewCallback(preConnectCallback)

func preConnectCallback(instance uintptr) uintptr {
	backend := lookupInstance(instance)
	if backend == nil {
		return 0
	}

	// Enable software GDI — FreeRDP will decode bitmaps into a memory surface.
	backend.setBool(FreeRDP_SoftwareGdi, true)

	return 1 // TRUE
}

// bitmapUpdateCallback handles BITMAP_UPDATE messages. With GDI mode active,
// most rendering goes through the GDI pipeline and lands in primary_buffer
// automatically. This callback catches any bitmap updates that come through
// the legacy path and blits them into our pixel buffer.
//
// BITMAP_UPDATE layout (FreeRDP 3.10.3):
//   UINT32 number            — offset 0, number of rectangles
//   BITMAP_DATA* rectangles  — offset 8 (pointer, 8-byte aligned)
//
// BITMAP_DATA layout (packed, not ALIGN64):
//   UINT32 destLeft          — offset 0
//   UINT32 destTop           — offset 4
//   UINT32 destRight         — offset 8
//   UINT32 destBottom        — offset 12
//   UINT32 width             — offset 16
//   UINT32 height            — offset 20
//   UINT32 bitsPerPixel      — offset 24
//   UINT32 flags             — offset 28
//   UINT32 bitmapLength      — offset 32
//   UINT32 cbCompFirstRowSize — offset 36
//   UINT32 cbCompMainBodySize — offset 40
//   UINT32 cbScanWidth       — offset 44
//   UINT32 cbUncompressedSize — offset 48
//   BYTE*  bitmapDataStream  — offset 56 (pointer, 8-byte aligned after padding)
//   BOOL   compressed        — offset 64
var bitmapUpdateCallbackPtr = syscall.NewCallback(bitmapUpdateCallback)

func bitmapUpdateCallback(context uintptr, bitmapUpdate uintptr) uintptr {
	if context == 0 || bitmapUpdate == 0 {
		return 1 // TRUE — don't break the pipeline
	}

	// Recover the instance from context->instance (slot 0 of rdpContext).
	instance := readPtr(context, 0)
	backend := lookupInstance(instance)
	if backend == nil || backend.buffer == nil {
		return 1
	}

	// With GDI mode enabled, the primary_buffer is already updated by FreeRDP's
	// internal GDI renderer before this callback fires. We just need to mark
	// the pixel buffer as dirty so the next GetFrame picks up changes.
	//
	// For the bitmap update path specifically, we parse the rectangles and
	// copy each one from the GDI primary buffer into our PixelBuffer.
	if backend.gdiReady && backend.gdiPrimaryBuf != 0 {
		numRects := readU32(bitmapUpdate, 0)
		if numRects > 0 {
			backend.copyGdiToBuffer()
		}
	}

	return 1 // TRUE
}

// ============================================================================
// FreeRDPBackend
// ============================================================================

// FreeRDPBackend implements RDPBackend using the FreeRDP3 library loaded at
// runtime via syscall.NewLazyDLL. This avoids any CGO dependency and allows
// cross-compilation from Linux while the DLLs are resolved at runtime on
// Windows.
type FreeRDPBackend struct {
	instance  uintptr // freerdp*
	context   uintptr // rdpContext* (extracted from instance->context)
	settings  uintptr // rdpSettings* (extracted from context->settings)
	input     uintptr // rdpInput* (extracted from context->input)
	buffer    *PixelBuffer
	connected bool
	config    RDPConfig
	mu        sync.Mutex
	stopCh    chan struct{}

	// GDI surface state — populated by PostConnect callback.
	gdiPrimaryBuf uintptr // BYTE* — FreeRDP's GDI framebuffer
	gdiWidth      int
	gdiHeight     int
	gdiStride     int     // bytes per row (may include padding)
	gdiReady      bool
}

// NewFreeRDPBackend creates a new FreeRDP-backed RDP backend. The underlying
// DLLs are not loaded until Connect is called.
func NewFreeRDPBackend() *FreeRDPBackend {
	return &FreeRDPBackend{
		stopCh: make(chan struct{}),
	}
}

// Connect establishes an RDP session using FreeRDP3. It creates a new FreeRDP
// instance, configures connection settings, and starts the event loop.
//
// Initialization order (required by FreeRDP3):
//  1. freerdp_new()           — allocate instance
//  2. Register callbacks      — PreConnect, PostConnect on the instance struct
//  3. freerdp_context_new()   — allocate context (also allocates settings, input, update)
//  4. Configure settings      — via freerdp_settings_set_* on context->settings
//  5. freerdp_connect()       — triggers PreConnect -> TCP -> PostConnect
//  6. Event loop              — freerdp_check_event_handles in a goroutine
func (f *FreeRDPBackend) Connect(config RDPConfig) error {
	f.mu.Lock()
	defer f.mu.Unlock()

	if f.connected {
		return fmt.Errorf("already connected")
	}

	f.config = config

	// ── Step 1: Create a bare FreeRDP instance ──
	ret, _, err := procFreerdpNew.Call()
	if ret == 0 {
		return fmt.Errorf("freerdp_new failed: %v", err)
	}
	f.instance = ret

	// Register this instance in the global map so callbacks can find us.
	registerInstance(f.instance, f)

	// ── Step 2: Register callbacks on the instance struct ──
	// PreConnect (slot 48): called before RDP negotiation, used to enable GDI.
	writePtr(f.instance, freerdpOffsetPreConnect, preConnectCallbackPtr)
	// PostConnect (slot 49): called after connection, sets up GDI rendering.
	writePtr(f.instance, freerdpOffsetPostConnect, postConnectCallbackPtr)

	// ── Step 3: Create the context ──
	// freerdp_context_new(freerdp* instance) -> BOOL
	// This allocates rdpContext and populates instance->context, including
	// the settings, input, and update sub-objects.
	ret, _, err = procContextNew.Call(f.instance)
	if ret == 0 {
		unregisterInstance(f.instance)
		procFreerdpFree.Call(f.instance)
		f.instance = 0
		return fmt.Errorf("freerdp_context_new failed: %v", err)
	}

	// ── Extract context, settings, and input pointers ──
	f.context = readPtr(f.instance, freerdpOffsetContext)
	if f.context == 0 {
		unregisterInstance(f.instance)
		procFreerdpFree.Call(f.instance)
		f.instance = 0
		return fmt.Errorf("freerdp context is null after context_new")
	}

	f.settings = readPtr(f.context, contextOffsetSettings)
	if f.settings == 0 {
		unregisterInstance(f.instance)
		procFreerdpFree.Call(f.instance)
		f.instance = 0
		return fmt.Errorf("freerdp settings is null after context_new")
	}

	f.input = readPtr(f.context, contextOffsetInput)
	if f.input == 0 {
		unregisterInstance(f.instance)
		procFreerdpFree.Call(f.instance)
		f.instance = 0
		return fmt.Errorf("freerdp input is null after context_new")
	}

	// ── Step 4: Configure connection settings ──
	f.setString(FreeRDP_ServerHostname, config.Hostname)
	f.setUint32(FreeRDP_ServerPort, uint32(config.Port))
	f.setString(FreeRDP_Username, config.Username)
	f.setString(FreeRDP_Password, config.Password)
	if config.Domain != "" {
		f.setString(FreeRDP_Domain, config.Domain)
	}

	// Display settings with sensible defaults.
	width := config.Width
	if width == 0 {
		width = 1920
	}
	height := config.Height
	if height == 0 {
		height = 1080
	}
	f.setUint32(FreeRDP_DesktopWidth, uint32(width))
	f.setUint32(FreeRDP_DesktopHeight, uint32(height))

	colorDepth := config.ColorDepth
	if colorDepth == 0 {
		colorDepth = 32
	}
	f.setUint32(FreeRDP_ColorDepth, uint32(colorDepth))

	// Security mode selection.
	switch config.Security {
	case "nla":
		f.setBool(FreeRDP_NlaSecurity, true)
	case "tls":
		f.setBool(FreeRDP_TlsSecurity, true)
	case "rdp":
		f.setBool(FreeRDP_RdpSecurity, true)
	default:
		f.setBool(FreeRDP_NlaSecurity, true)
	}

	// Accept all server certificates. Per-host pinning is a future enhancement.
	f.setBool(FreeRDP_IgnoreCertificate, true)

	// Enable software GDI rendering. This tells FreeRDP to decode all bitmap
	// data into a memory framebuffer that we can read directly. The PreConnect
	// callback also sets this, but we set it here as well for clarity —
	// FreeRDP reads it during connection negotiation.
	f.setBool(FreeRDP_SoftwareGdi, true)

	// Allocate the pixel buffer for frame capture.
	f.buffer = NewPixelBuffer(width, height)

	// ── Step 5: Initiate the RDP connection ──
	// freerdp_connect calls PreConnect -> TCP/TLS/NLA -> PostConnect internally.
	ret, _, err = procFreerdpConnect.Call(f.instance)
	if ret == 0 {
		unregisterInstance(f.instance)
		procFreerdpFree.Call(f.instance)
		f.instance = 0
		return fmt.Errorf("freerdp_connect failed: %v", err)
	}

	f.connected = true

	// ── Step 6: Start the event processing loop ──
	go f.eventLoop()

	return nil
}

// eventLoop polls FreeRDP for incoming events at roughly 60 fps. It runs
// until the stop channel is closed or the connection is marked as inactive.
func (f *FreeRDPBackend) eventLoop() {
	for {
		select {
		case <-f.stopCh:
			return
		default:
			f.mu.Lock()
			if !f.connected {
				f.mu.Unlock()
				return
			}
			// freerdp_check_event_handles takes rdpContext*, NOT freerdp*.
			procCheckEventHandles.Call(f.context)

			// After processing events, copy the GDI framebuffer into our
			// PixelBuffer so GetFrame returns current data.
			if f.gdiReady {
				f.copyGdiToBuffer()
			}
			f.mu.Unlock()
			time.Sleep(16 * time.Millisecond) // ~60 fps
		}
	}
}

// copyGdiToBuffer copies the FreeRDP GDI primary surface into the PixelBuffer.
// The GDI buffer is BGRA32; our PixelBuffer expects RGBA. We do the channel
// swap during the copy.
//
// Must be called with f.mu held.
func (f *FreeRDPBackend) copyGdiToBuffer() {
	if f.gdiPrimaryBuf == 0 || f.buffer == nil {
		return
	}

	w := f.gdiWidth
	h := f.gdiHeight
	stride := f.gdiStride
	if stride == 0 {
		stride = w * 4 // fallback: assume tightly packed BGRA32
	}

	// Total bytes in the GDI surface.
	totalBytes := stride * h
	if totalBytes <= 0 {
		return
	}

	// Read the raw GDI framebuffer. This is a direct memory read from the
	// FreeRDP-managed buffer. The GDI layer updates this buffer during
	// BeginPaint/EndPaint cycles triggered by freerdp_check_event_handles.
	src := unsafe.Slice((*byte)(unsafe.Pointer(f.gdiPrimaryBuf)), totalBytes)

	// Build the RGBA frame, swapping B and R channels (BGRA -> RGBA).
	buf := f.buffer
	buf.mu.Lock()
	defer buf.mu.Unlock()

	dstLen := w * h * 4
	if len(buf.Data) != dstLen {
		buf.Data = make([]byte, dstLen)
		buf.Width = w
		buf.Height = h
	}

	for y := 0; y < h; y++ {
		srcRow := y * stride
		dstRow := y * w * 4
		for x := 0; x < w; x++ {
			si := srcRow + x*4
			di := dstRow + x*4
			if si+3 >= totalBytes || di+3 >= dstLen {
				break
			}
			// BGRA -> RGBA
			buf.Data[di+0] = src[si+2] // R <- B
			buf.Data[di+1] = src[si+1] // G <- G
			buf.Data[di+2] = src[si+0] // B <- R
			buf.Data[di+3] = src[si+3] // A <- A
		}
	}

	buf.dirty = true
}

// Disconnect tears down the RDP session and frees the FreeRDP instance.
func (f *FreeRDPBackend) Disconnect() error {
	f.mu.Lock()
	defer f.mu.Unlock()

	if !f.connected {
		return nil
	}

	close(f.stopCh)
	f.connected = false
	f.gdiReady = false

	// Free GDI resources before disconnecting.
	if f.gdiPrimaryBuf != 0 {
		procGdiFree.Call(f.instance)
		f.gdiPrimaryBuf = 0
	}

	procFreerdpDisconnect.Call(f.instance)

	unregisterInstance(f.instance)
	procFreerdpFree.Call(f.instance)
	f.instance = 0
	f.context = 0
	f.settings = 0
	f.input = 0

	return nil
}

// SendMouseEvent forwards a mouse event to the remote RDP session.
func (f *FreeRDPBackend) SendMouseEvent(x, y int, flags uint32) error {
	f.mu.Lock()
	defer f.mu.Unlock()

	if !f.connected || f.input == 0 {
		return fmt.Errorf("not connected")
	}
	procInputSendMouse.Call(f.input, uintptr(flags), uintptr(x), uintptr(y))
	return nil
}

// SendKeyEvent forwards a keyboard event to the remote RDP session. The
// scancode uses the same format as the ScancodeMap in input.go — extended
// keys have the 0xE0 prefix in the high byte.
func (f *FreeRDPBackend) SendKeyEvent(scancode uint32, pressed bool) error {
	f.mu.Lock()
	defer f.mu.Unlock()

	if !f.connected || f.input == 0 {
		return fmt.Errorf("not connected")
	}

	var flags uint32
	if pressed {
		flags = KBD_FLAGS_DOWN
	} else {
		flags = KBD_FLAGS_RELEASE
	}
	if scancode > 0xFF {
		flags |= KBD_FLAGS_EXTENDED
	}
	procInputSendKeyboard.Call(f.input, uintptr(flags), uintptr(scancode&0xFF))
	return nil
}

// SendClipboard sends clipboard text to the remote session.
// TODO: Implement via the FreeRDP cliprdr channel.
func (f *FreeRDPBackend) SendClipboard(data string) error {
	return nil
}

// GetFrame returns the current full-frame RGBA pixel buffer. The frame is
// populated from the FreeRDP GDI primary surface during the event loop and
// via the PostConnect/BitmapUpdate callbacks.
func (f *FreeRDPBackend) GetFrame() ([]byte, error) {
	if f.buffer == nil {
		return nil, fmt.Errorf("no frame buffer")
	}
	return f.buffer.GetFrame(), nil
}

// IsConnected reports whether the backend has an active RDP connection.
func (f *FreeRDPBackend) IsConnected() bool {
	f.mu.Lock()
	defer f.mu.Unlock()
	return f.connected
}

// setString sets a string setting on the FreeRDP settings object.
func (f *FreeRDPBackend) setString(id int, value string) {
	b, _ := syscall.BytePtrFromString(value)
	procSettingsSetString.Call(f.settings, uintptr(id), uintptr(unsafe.Pointer(b)))
}

// setUint32 sets a uint32 setting on the FreeRDP settings object.
func (f *FreeRDPBackend) setUint32(id int, value uint32) {
	procSettingsSetUint32.Call(f.settings, uintptr(id), uintptr(value))
}

// setBool sets a boolean setting on the FreeRDP settings object.
func (f *FreeRDPBackend) setBool(id int, value bool) {
	v := uintptr(0)
	if value {
		v = 1
	}
	procSettingsSetBool.Call(f.settings, uintptr(id), v)
}

// Ensure FreeRDPBackend satisfies the RDPBackend interface at compile time.
var _ RDPBackend = (*FreeRDPBackend)(nil)
