//go:build windows

package main

import (
	_ "embed"
	"encoding/binary"
	"fmt"
	"net/url"
	"os"
	"os/exec"
	"runtime"
	"strings"
	"sync"
	"syscall"
	"unsafe"
)

const (
	WS_OVERLAPPED       = 0x00000000
	WS_CAPTION          = 0x00C00000
	WS_SYSMENU          = 0x00080000
	WS_MINIMIZEBOX      = 0x00020000
	WS_VISIBLE          = 0x10000000
	WS_CHILD            = 0x40000000
	WS_TABSTOP          = 0x00010000
	ES_AUTOHSCROLL      = 0x0080
	ES_MULTILINE        = 0x0004
	ES_AUTOVSCROLL      = 0x0040
	ES_READONLY         = 0x0800
	WS_VSCROLL          = 0x00200000
	BS_PUSHBUTTON       = 0x00000000
	BS_AUTOCHECKBOX     = 0x00000003
	SS_LEFT             = 0
	SS_CENTER           = 1
	SS_RIGHT            = 2
	SW_SHOW             = 5
	SW_HIDE             = 0
	SW_RESTORE          = 9
	WM_DESTROY          = 0x0002
	WM_CLOSE            = 0x0010
	WM_SETICON          = 0x0080
	WM_COMMAND          = 0x0111
	WM_TIMER            = 0x0113
	WM_PAINT            = 0x000F
	WM_CTLCOLORSTATIC   = 0x0138
	WM_CTLCOLOREDIT     = 0x0133
	WM_SETFONT          = 0x0030
	WM_APP              = 0x8000
	BN_CLICKED          = 0
	BM_GETCHECK         = 0x00F0
	BM_SETCHECK         = 0x00F1
	BST_CHECKED         = 1
	COLOR_WINDOW        = 5
	TRANSPARENT         = 1
	NIM_ADD             = 0
	NIM_MODIFY          = 1
	NIM_DELETE          = 2
	NIF_MESSAGE         = 1
	NIF_ICON            = 2
	NIF_TIP             = 4
	NIF_INFO            = 0x10
	NIIF_INFO           = 1
	NIIF_WARNING        = 2
	NIIF_ERROR          = 3
	WM_LBUTTONDBLCLK    = 0x0203
	WM_RBUTTONUP        = 0x0205
	MF_STRING           = 0
	TPM_RIGHTBUTTON     = 0x0002
	IDI_APPLICATION     = 32512
	IDC_ARROW           = 32512
	ICON_SMALL          = 0
	ICON_BIG            = 1
	LR_DEFAULTCOLOR     = 0
	FW_NORMAL           = 400
	FW_SEMIBOLD         = 600
	FW_BOLD             = 700
	DEFAULT_CHARSET     = 1
	OUT_DEFAULT_PRECIS  = 0
	CLIP_DEFAULT_PRECIS = 0
	CLEARTYPE_QUALITY   = 5
	DEFAULT_PITCH       = 0
	NULL_BRUSH          = 5
	MB_OK               = 0
	MB_ICONINFORMATION  = 0x40
	MB_ICONERROR        = 0x10
	CF_UNICODETEXT      = 13
	GMEM_MOVEABLE       = 0x0002
	REG_SZ              = 1
)

const (
	idDiag           = 1001
	idSettings       = 1002
	idExit           = 1003
	idSaveSettings   = 1101
	idCancelSettings = 1102
	idCopyLog        = 1201
	idServerValue    = 2001
	idVmixValue      = 2002
	idDeviceValue    = 2003
	idAccountValue   = 2004
	idMatchValue     = 2005
	idMappingValue   = 2006
	idMainStatus     = 2007
	idLastCommand    = 2008
	idDeviceID       = 2009
	trayMessage      = WM_APP + 21
	settingsDoneMsg  = WM_APP + 22
)

type POINT struct{ X, Y int32 }
type RECT struct{ Left, Top, Right, Bottom int32 }
type MSG struct {
	HWnd           uintptr
	Message        uint32
	WParam, LParam uintptr
	Time           uint32
	Pt             POINT
	LPrivate       uint32
}
type PAINTSTRUCT struct {
	Hdc                  uintptr
	FErase               int32
	RcPaint              RECT
	FRestore, FIncUpdate int32
	RgbReserved          [32]byte
}
type WNDCLASSEX struct {
	CbSize                                   uint32
	Style                                    uint32
	LpfnWndProc                              uintptr
	CbClsExtra, CbWndExtra                   int32
	HInstance, HIcon, HCursor, HbrBackground uintptr
	LpszMenuName, LpszClassName              *uint16
	HIconSm                                  uintptr
}
type NOTIFYICONDATA struct {
	CbSize                        uint32
	HWnd                          uintptr
	UID, UFlags, UCallbackMessage uint32
	HIcon                         uintptr
	SzTip                         [128]uint16
	DwState, DwStateMask          uint32
	SzInfo                        [256]uint16
	UTimeoutOrVersion             uint32
	SzInfoTitle                   [64]uint16
	DwInfoFlags                   uint32
	GuidItem                      [16]byte
	HBalloonIcon                  uintptr
}

type WinApp struct {
	agent                                          *Agent
	hwnd                                           uintptr
	settingsHwnd                                   uintptr
	diagHwnd                                       uintptr
	controls                                       map[int]uintptr
	settingsControls                               map[int]uintptr
	diagEdit                                       uintptr
	font, fontSmall, fontBold, fontTitle, fontDiag uintptr
	bgBrush, cardBrush, editBrush                  uintptr
	tray                                           NOTIFYICONDATA
	iconBig, iconSmall                             uintptr
	lastTrayStatus                                 string
	lastDiagLog                                    string
	lastTexts                                      map[int]string
	configCache                                    Config
	stateCache                                     AppState
	uiMu                                           sync.Mutex
	settingsResult                                 string
	settingsResultError                            bool
	reallyExit                                     bool
}

var app *WinApp

// smith.ico is embedded directly into agent.exe.
//
//go:embed smith.ico
var smithIcon []byte

func iconFromICO(data []byte, desired int) uintptr {
	if len(data) < 6 || binary.LittleEndian.Uint16(data[0:2]) != 0 || binary.LittleEndian.Uint16(data[2:4]) != 1 {
		return 0
	}
	count := int(binary.LittleEndian.Uint16(data[4:6]))
	if count <= 0 || len(data) < 6+count*16 {
		return 0
	}
	bestOff, bestSize, bestScore := 0, 0, int(^uint(0)>>1)
	for i := 0; i < count; i++ {
		e := 6 + i*16
		w := int(data[e])
		h := int(data[e+1])
		if w == 0 {
			w = 256
		}
		if h == 0 {
			h = 256
		}
		sz := int(binary.LittleEndian.Uint32(data[e+8 : e+12]))
		off := int(binary.LittleEndian.Uint32(data[e+12 : e+16]))
		if sz <= 0 || off < 0 || off+sz > len(data) {
			continue
		}
		score := absInt(w-desired) + absInt(h-desired)
		if score < bestScore {
			bestScore = score
			bestOff = off
			bestSize = sz
		}
	}
	if bestSize == 0 {
		return 0
	}
	h, _, _ := pCreateIconFromResourceEx.Call(uintptr(unsafe.Pointer(&data[bestOff])), uintptr(bestSize), 1, 0x00030000, uintptr(desired), uintptr(desired), LR_DEFAULTCOLOR)
	return h
}
func absInt(v int) int {
	if v < 0 {
		return -v
	}
	return v
}

var (
	user32                    = syscall.NewLazyDLL("user32.dll")
	gdi32                     = syscall.NewLazyDLL("gdi32.dll")
	shell32                   = syscall.NewLazyDLL("shell32.dll")
	kernel32                  = syscall.NewLazyDLL("kernel32.dll")
	pRegisterClassEx          = user32.NewProc("RegisterClassExW")
	pCreateWindowEx           = user32.NewProc("CreateWindowExW")
	pDefWindowProc            = user32.NewProc("DefWindowProcW")
	pShowWindow               = user32.NewProc("ShowWindow")
	pUpdateWindow             = user32.NewProc("UpdateWindow")
	pGetMessage               = user32.NewProc("GetMessageW")
	pTranslateMessage         = user32.NewProc("TranslateMessage")
	pDispatchMessage          = user32.NewProc("DispatchMessageW")
	pPostQuitMessage          = user32.NewProc("PostQuitMessage")
	pDestroyWindow            = user32.NewProc("DestroyWindow")
	pSetTimer                 = user32.NewProc("SetTimer")
	pSetWindowText            = user32.NewProc("SetWindowTextW")
	pSendMessage              = user32.NewProc("SendMessageW")
	pLoadIcon                 = user32.NewProc("LoadIconW")
	pLoadCursor               = user32.NewProc("LoadCursorW")
	pCreateIconFromResourceEx = user32.NewProc("CreateIconFromResourceEx")
	pDestroyIcon              = user32.NewProc("DestroyIcon")
	pAdjustWindowRectEx       = user32.NewProc("AdjustWindowRectEx")
	pBeginPaint               = user32.NewProc("BeginPaint")
	pEndPaint                 = user32.NewProc("EndPaint")
	pInvalidateRect           = user32.NewProc("InvalidateRect")
	pMessageBox               = user32.NewProc("MessageBoxW")
	pGetWindowTextLength      = user32.NewProc("GetWindowTextLengthW")
	pGetWindowText            = user32.NewProc("GetWindowTextW")
	pSetForegroundWindow      = user32.NewProc("SetForegroundWindow")
	pGetCursorPos             = user32.NewProc("GetCursorPos")
	pIsWindowVisible          = user32.NewProc("IsWindowVisible")
	pPostMessage              = user32.NewProc("PostMessageW")
	pCreatePopupMenu          = user32.NewProc("CreatePopupMenu")
	pAppendMenu               = user32.NewProc("AppendMenuW")
	pTrackPopupMenu           = user32.NewProc("TrackPopupMenu")
	pDestroyMenu              = user32.NewProc("DestroyMenu")
	pOpenClipboard            = user32.NewProc("OpenClipboard")
	pEmptyClipboard           = user32.NewProc("EmptyClipboard")
	pSetClipboardData         = user32.NewProc("SetClipboardData")
	pCloseClipboard           = user32.NewProc("CloseClipboard")
	pSetBkMode                = gdi32.NewProc("SetBkMode")
	pSetBkColor               = gdi32.NewProc("SetBkColor")
	pSetTextColor             = gdi32.NewProc("SetTextColor")
	pCreateSolidBrush         = gdi32.NewProc("CreateSolidBrush")
	pCreateFont               = gdi32.NewProc("CreateFontW")
	pGetStockObject           = gdi32.NewProc("GetStockObject")
	pSelectObject             = gdi32.NewProc("SelectObject")
	pCreatePen                = gdi32.NewProc("CreatePen")
	pRoundRect                = gdi32.NewProc("RoundRect")
	pDeleteObject             = gdi32.NewProc("DeleteObject")
	pShellNotifyIcon          = shell32.NewProc("Shell_NotifyIconW")
	pGetModuleHandle          = kernel32.NewProc("GetModuleHandleW")
	pGlobalAlloc              = kernel32.NewProc("GlobalAlloc")
	pGlobalLock               = kernel32.NewProc("GlobalLock")
	pGlobalUnlock             = kernel32.NewProc("GlobalUnlock")
	pRtlMoveMemory            = kernel32.NewProc("RtlMoveMemory")
	pSetProcessDPIAware       = user32.NewProc("SetProcessDPIAware")
)

func utf16(s string) *uint16   { p, _ := syscall.UTF16PtrFromString(s); return p }
func rgb(r, g, b byte) uintptr { return uintptr(uint32(r) | uint32(g)<<8 | uint32(b)<<16) }
func loword(v uintptr) int     { return int(v & 0xffff) }
func hiword(v uintptr) int     { return int((v >> 16) & 0xffff) }

func main() {
	// Win32 windows and their message queue belong to the OS thread that created
	// them. A Go goroutine may otherwise migrate between OS threads, which can
	// manifest as an intermittent "Not responding" GUI. Keep the complete GUI
	// lifetime pinned to one Windows thread.
	runtime.LockOSThread()
	defer runtime.UnlockOSThread()

	pSetProcessDPIAware.Call()
	path, err := configPath()
	if err != nil {
		return
	}
	cfg, err := loadConfig(path)
	if err != nil {
		return
	}
	agent := NewAgent(path, cfg)
	app = &WinApp{
		agent:            agent,
		controls:         map[int]uintptr{},
		settingsControls: map[int]uintptr{},
		lastTexts:        map[int]string{},
		configCache:      cfg,
	}
	if err := app.init(); err != nil {
		return
	}
	// Make the GUI visible before starting any network work. This keeps first
	// launch deterministic even when DNS, the server, or vMix are unavailable.
	if !cfg.StartMinimized {
		pShowWindow.Call(app.hwnd, SW_SHOW)
		pUpdateWindow.Call(app.hwnd)
	}
	app.addTray()
	agent.Start()
	app.refresh()
	if cfg.RunWithWindows {
		// Registry maintenance is not part of startup-critical GUI work.
		go func() { _ = setRunAtStartup(true) }()
	}
	var msg MSG
	for {
		r, _, _ := pGetMessage.Call(uintptr(unsafe.Pointer(&msg)), 0, 0, 0)
		if int32(r) <= 0 {
			break
		}
		pTranslateMessage.Call(uintptr(unsafe.Pointer(&msg)))
		pDispatchMessage.Call(uintptr(unsafe.Pointer(&msg)))
	}
	agent.Stop()
	app.removeTray()
}

func (w *WinApp) init() error {
	inst, _, _ := pGetModuleHandle.Call(0)
	fallback, _, _ := pLoadIcon.Call(0, IDI_APPLICATION)
	cursor, _, _ := pLoadCursor.Call(0, IDC_ARROW)
	w.iconBig = iconFromICO(smithIcon, 32)
	w.iconSmall = iconFromICO(smithIcon, 16)
	if w.iconBig == 0 {
		w.iconBig = fallback
	}
	if w.iconSmall == 0 {
		w.iconSmall = w.iconBig
	}
	w.bgBrush = createBrush(15, 20, 28)
	w.cardBrush = createBrush(23, 30, 41)
	w.editBrush = createBrush(30, 38, 51)
	w.font = createFont(21, FW_NORMAL)
	w.fontSmall = createFont(18, FW_NORMAL)
	w.fontBold = createFont(22, FW_SEMIBOLD)
	w.fontTitle = createFont(32, FW_BOLD)
	w.fontDiag = createFontFace(18, FW_NORMAL, "Consolas")
	class := utf16("HockeyVmixAgent123")
	wc := WNDCLASSEX{CbSize: uint32(unsafe.Sizeof(WNDCLASSEX{})), LpfnWndProc: syscall.NewCallback(mainWndProc), HInstance: inst, HIcon: w.iconBig, HCursor: cursor, HbrBackground: w.bgBrush, LpszClassName: class, HIconSm: w.iconSmall}
	if r, _, e := pRegisterClassEx.Call(uintptr(unsafe.Pointer(&wc))); r == 0 {
		return fmt.Errorf("RegisterClassEx: %v", e)
	}
	// Keep the main window deliberately roomy. Dimensions below are CLIENT area
	// dimensions; AdjustWindowRectEx adds the actual Windows frame/title bar.
	style := uintptr(WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX)
	rect := RECT{Right: 850, Bottom: 760}
	pAdjustWindowRectEx.Call(uintptr(unsafe.Pointer(&rect)), style, 0, 0)
	outerW := rect.Right - rect.Left
	outerH := rect.Bottom - rect.Top
	hwnd, _, e := pCreateWindowEx.Call(0, uintptr(unsafe.Pointer(class)), uintptr(unsafe.Pointer(utf16("HOCKEY vMix AGENT"))), style, 0x80000000, 0x80000000, uintptr(outerW), uintptr(outerH), 0, 0, inst, 0)
	if hwnd == 0 {
		return fmt.Errorf("CreateWindow: %v", e)
	}
	w.hwnd = hwnd
	pSendMessage.Call(hwnd, WM_SETICON, ICON_BIG, w.iconBig)
	pSendMessage.Call(hwnd, WM_SETICON, ICON_SMALL, w.iconSmall)
	w.buildMain()
	// Auxiliary windows are created lazily on the locked GUI thread. Keeping
	// startup to one main window avoids unnecessary Win32 work on first launch.
	pSetTimer.Call(hwnd, 1, 1000, 0)
	return nil
}

func createBrush(r, g, b byte) uintptr { h, _, _ := pCreateSolidBrush.Call(rgb(r, g, b)); return h }
func createFont(size int, weight int) uintptr {
	return createFontFace(size, weight, "Segoe UI")
}
func createFontFace(size int, weight int, face string) uintptr {
	h, _, _ := pCreateFont.Call(uintptr(int32(-size)), 0, 0, 0, uintptr(weight), 0, 0, 0, DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, CLEARTYPE_QUALITY, DEFAULT_PITCH, uintptr(unsafe.Pointer(utf16(face))))
	return h
}

func (w *WinApp) label(id int, text string, x, y, width, height int, font uintptr) uintptr {
	return w.control(id, "STATIC", text, WS_CHILD|WS_VISIBLE|SS_LEFT, x, y, width, height, font)
}
func (w *WinApp) labelRight(id int, text string, x, y, width, height int, font uintptr) uintptr {
	return w.control(id, "STATIC", text, WS_CHILD|WS_VISIBLE|SS_RIGHT, x, y, width, height, font)
}
func (w *WinApp) button(id int, text string, x, y, width, height int) uintptr {
	return w.control(id, "BUTTON", text, WS_CHILD|WS_VISIBLE|WS_TABSTOP|BS_PUSHBUTTON, x, y, width, height, w.fontSmall)
}
func (w *WinApp) control(id int, class, text string, style uintptr, x, y, width, height int, font uintptr) uintptr {
	h, _, _ := pCreateWindowEx.Call(0, uintptr(unsafe.Pointer(utf16(class))), uintptr(unsafe.Pointer(utf16(text))), style, uintptr(x), uintptr(y), uintptr(width), uintptr(height), w.hwnd, uintptr(id), 0, 0)
	if h != 0 {
		pSendMessage.Call(h, WM_SETFONT, font, 1)
		w.controls[id] = h
	}
	return h
}

func (w *WinApp) buildMain() {
	w.label(0, "HOCKEY vMix AGENT", 36, 24, 470, 46, w.fontTitle)
	w.label(0, "универсальный мост Web ↔ vMix", 38, 68, 470, 26, w.fontSmall)
	w.labelRight(idMainStatus, "● STARTING", 540, 30, 280, 38, w.fontBold)

	w.label(0, "СЕРВЕР", 48, 120, 130, 24, w.fontSmall)
	w.label(idServerValue, "Подключение…", 48, 149, 330, 36, w.fontBold)
	w.label(0, "vMix", 468, 120, 130, 24, w.fontSmall)
	w.label(idVmixValue, "Проверка…", 468, 149, 340, 36, w.fontBold)

	w.label(0, "УСТРОЙСТВО", 48, 232, 160, 24, w.fontSmall)
	w.label(idDeviceValue, "—", 48, 263, 760, 38, w.fontBold)
	w.label(idDeviceID, "Device ID: —", 48, 304, 760, 30, w.fontSmall)

	w.label(0, "АККАУНТ", 48, 376, 130, 24, w.fontSmall)
	w.label(idAccountValue, "Не прикреплён", 48, 405, 330, 36, w.fontBold)
	w.label(0, "ТЕКУЩИЙ МАТЧ", 468, 376, 190, 24, w.fontSmall)
	w.label(idMatchValue, "—", 468, 405, 340, 36, w.fontBold)

	w.label(0, "MAPPING", 48, 488, 130, 24, w.fontSmall)
	w.label(idMappingValue, "Будет назначаться сервером", 48, 517, 760, 36, w.fontBold)

	w.label(0, "ПОСЛЕДНЯЯ КОМАНДА", 48, 590, 220, 24, w.fontSmall)
	w.label(idLastCommand, "Пока команд не было", 48, 619, 760, 32, w.font)

	w.label(0, "v"+agentVersion+"  •  Protocol "+fmt.Sprint(protocolVersion), 38, 716, 320, 26, w.fontSmall)
	w.button(idDiag, "Диагностика", 548, 700, 132, 44)
	w.button(idSettings, "Настройки", 694, 700, 126, 44)
}

func mainWndProc(hwnd uintptr, msg uint32, wparam, lparam uintptr) uintptr {
	if app == nil {
		r, _, _ := pDefWindowProc.Call(hwnd, uintptr(msg), wparam, lparam)
		return r
	}
	switch msg {
	case WM_TIMER:
		app.refresh()
		// Do not rewrite a hidden diagnostics edit control. The previous build
		// replaced the entire log every 500 ms forever after Diagnostics had
		// been opened once, which could make the Win32 UI appear frozen.
		if app.diagEdit != 0 && app.diagHwnd != 0 {
			visible, _, _ := pIsWindowVisible.Call(app.diagHwnd)
			if visible != 0 {
				app.refreshDiagnostics()
			}
		}
		return 0
	case settingsDoneMsg:
		app.uiMu.Lock()
		text := app.settingsResult
		isErr := app.settingsResultError
		app.settingsResult = ""
		app.settingsResultError = false
		app.uiMu.Unlock()
		// Successful saves are intentionally silent: the Settings window closes
		// immediately and the Agent reconnects in the background. Only errors use
		// a modal dialog, avoiding another source of perceived UI "freezes".
		if text != "" && isErr {
			message(app.hwnd, "Ошибка настроек", text, MB_ICONERROR)
		}
		return 0
	case WM_COMMAND:
		if hiword(wparam) == BN_CLICKED {
			switch loword(wparam) {
			case idDiag:
				app.openDiagnostics()
			case idSettings:
				app.openSettings()
			}
		}
		return 0
	case WM_PAINT:
		app.paint(hwnd)
		return 0
	case WM_CTLCOLORSTATIC:
		pSetBkMode.Call(wparam, TRANSPARENT)
		id := app.controlID(lparam)
		color := rgb(203, 213, 225)
		if id == idMainStatus {
			status, _ := app.statusTextFor(app.stateCache)
			color = status
			pSetBkMode.Call(wparam, 0)
			pSetBkColor.Call(wparam, rgb(15, 20, 28))
			pSetTextColor.Call(wparam, color)
			return app.bgBrush
		}
		if id == idServerValue {
			if app.stateCache.ServerConnected {
				color = rgb(74, 222, 128)
			} else {
				color = rgb(248, 113, 113)
			}
		}
		if id == idVmixValue {
			if app.stateCache.Vmix.Connected {
				color = rgb(74, 222, 128)
			} else {
				color = rgb(248, 113, 113)
			}
		}
		pSetTextColor.Call(wparam, color)
		b, _, _ := pGetStockObject.Call(NULL_BRUSH)
		return b
	case WM_CLOSE:
		if app.reallyExit {
			pDestroyWindow.Call(hwnd)
		} else {
			pShowWindow.Call(hwnd, SW_HIDE)
		}
		return 0
	case WM_DESTROY:
		app.removeTray()
		pPostQuitMessage.Call(0)
		return 0
	case trayMessage:
		switch uint32(lparam) {
		case WM_LBUTTONDBLCLK:
			app.showMain()
		case WM_RBUTTONUP:
			app.trayMenu()
		}
		return 0
	}
	r, _, _ := pDefWindowProc.Call(hwnd, uintptr(msg), wparam, lparam)
	return r
}

func (w *WinApp) controlID(hwnd uintptr) int {
	for id, h := range w.controls {
		if h == hwnd {
			return id
		}
	}
	return 0
}
func (w *WinApp) paint(hwnd uintptr) {
	var ps PAINTSTRUCT
	hdc, _, _ := pBeginPaint.Call(hwnd, uintptr(unsafe.Pointer(&ps)))
	pen, _, _ := pCreatePen.Call(0, 1, rgb(48, 59, 75))
	oldPen, _, _ := pSelectObject.Call(hdc, pen)
	oldBrush, _, _ := pSelectObject.Call(hdc, w.cardBrush)
	for _, r := range []RECT{{30, 104, 415, 202}, {445, 104, 830, 202}, {30, 216, 830, 350}, {30, 364, 415, 460}, {445, 364, 830, 460}, {30, 474, 830, 566}, {30, 578, 830, 676}} {
		pRoundRect.Call(hdc, uintptr(r.Left), uintptr(r.Top), uintptr(r.Right), uintptr(r.Bottom), 14, 14)
	}
	pSelectObject.Call(hdc, oldPen)
	pSelectObject.Call(hdc, oldBrush)
	pDeleteObject.Call(pen)
	pEndPaint.Call(hwnd, uintptr(unsafe.Pointer(&ps)))
}

func (w *WinApp) statusTextFor(s AppState) (uintptr, string) {
	if !s.ServerConnected {
		if s.ServerConnecting {
			return rgb(251, 191, 36), "● CONNECTING"
		}
		return rgb(248, 113, 113), "● SERVER OFFLINE"
	}
	if !s.Vmix.Connected {
		return rgb(248, 113, 113), "● VMIX NOT FOUND"
	}
	if !s.Paired {
		return rgb(251, 191, 36), "● NOT PAIRED"
	}
	if !s.Active {
		return rgb(148, 163, 184), "● STANDBY"
	}
	if s.MatchID == "" {
		return rgb(251, 191, 36), "● WAITING FOR MATCH"
	}
	return rgb(74, 222, 128), "● READY"
}

func (w *WinApp) refresh() {
	// Snapshot shared state once per timer tick. Painting and colour callbacks
	// use this GUI-owned cache and never wait on networking locks.
	s := w.agent.Snapshot()
	cfg := w.agent.Config()
	w.stateCache = s
	w.configCache = cfg
	_, status := w.statusTextFor(s)
	w.setControlText(idMainStatus, status)
	server := "Не подключён"
	if s.ServerConnected {
		server = "Подключён"
		if host := serverHost(s.ConnectedServer); host != "" {
			server += " · " + host
		}
	} else if s.ServerConnecting {
		server = "Подключение…"
	}
	w.setControlText(idServerValue, server)
	vm := "Не найден"
	if s.Vmix.Connected {
		vm = "Подключён"
		if s.Vmix.Version != "" {
			vm += " · " + s.Vmix.Version
		}
	}
	w.setControlText(idVmixValue, vm)
	w.setControlText(idDeviceValue, cfg.DeviceName)
	w.setControlText(idDeviceID, "Device ID: "+cfg.DeviceID)
	account := "Не прикреплён"
	if s.Paired {
		account = s.PairedLogin
		if account == "" {
			account = "Прикреплён"
		}
		if !s.Active {
			account += " · standby"
		}
	}
	w.setControlText(idAccountValue, account)
	match := "—"
	if s.MatchID != "" {
		match = s.MatchID
	}
	w.setControlText(idMatchValue, match)
	mapping := "Не настроен (следующий этап)"
	if s.MappingName != "" {
		mapping = s.MappingName
		if s.MappingVersion != "" {
			mapping += " · v" + s.MappingVersion
		}
	}
	w.setControlText(idMappingValue, mapping)
	cmd := "Пока команд не было"
	if s.LastCommand != "" {
		icon := "✓"
		if !s.LastCommandOK {
			icon = "✕"
		}
		cmd = icon + " " + s.LastCommand + " · " + s.LastCommandAt.Format("15:04:05")
	}
	w.setControlText(idLastCommand, cmd)
	w.updateTray(status, s)
}

func (w *WinApp) setControlText(id int, text string) {
	if w.lastTexts[id] == text {
		return
	}
	w.lastTexts[id] = text
	setText(w.controls[id], text)
	// Repaint only when the visible value actually changed.
	if h := w.controls[id]; h != 0 {
		pInvalidateRect.Call(h, 0, 1)
	}
	pInvalidateRect.Call(w.hwnd, 0, 0)
}

func serverHost(raw string) string {
	u, err := url.Parse(strings.TrimSpace(raw))
	if err == nil && u.Host != "" {
		return u.Host
	}
	return ""
}
func shortDevice(v string) string {
	if len(v) <= 22 {
		return v
	}
	return v[:10] + " … " + v[len(v)-8:]
}
func setText(hwnd uintptr, text string) {
	if hwnd != 0 {
		pSetWindowText.Call(hwnd, uintptr(unsafe.Pointer(utf16(text))))
	}
}

func (w *WinApp) addTray() {
	icon := w.iconSmall
	if icon == 0 {
		icon, _, _ = pLoadIcon.Call(0, IDI_APPLICATION)
	}
	w.tray = NOTIFYICONDATA{CbSize: uint32(unsafe.Sizeof(NOTIFYICONDATA{})), HWnd: w.hwnd, UID: 1, UFlags: NIF_MESSAGE | NIF_ICON | NIF_TIP, UCallbackMessage: trayMessage, HIcon: icon}
	copyUTF16(w.tray.SzTip[:], "Hockey vMix Agent")
	pShellNotifyIcon.Call(NIM_ADD, uintptr(unsafe.Pointer(&w.tray)))
}
func (w *WinApp) removeTray() {
	if w.tray.CbSize != 0 {
		pShellNotifyIcon.Call(NIM_DELETE, uintptr(unsafe.Pointer(&w.tray)))
		w.tray.CbSize = 0
	}
}
func (w *WinApp) updateTray(status string, s AppState) {
	if status == w.lastTrayStatus {
		return
	}
	w.lastTrayStatus = status
	w.tray.UFlags = NIF_MESSAGE | NIF_ICON | NIF_TIP
	tip := "Hockey Agent · " + strings.TrimPrefix(status, "● ")
	if s.MatchID != "" {
		tip += " · Match " + s.MatchID
	}
	copyUTF16(w.tray.SzTip[:], tip)
	pShellNotifyIcon.Call(NIM_MODIFY, uintptr(unsafe.Pointer(&w.tray)))
}
func copyUTF16(dst []uint16, s string) {
	u, _ := syscall.UTF16FromString(s)
	for i := range dst {
		dst[i] = 0
	}
	copy(dst, u)
}
func (w *WinApp) showMain() { pShowWindow.Call(w.hwnd, SW_RESTORE); pSetForegroundWindow.Call(w.hwnd) }
func (w *WinApp) trayMenu() {
	menu, _, _ := pCreatePopupMenu.Call()
	pAppendMenu.Call(menu, MF_STRING, 1, uintptr(unsafe.Pointer(utf16("Открыть Hockey Agent"))))
	pAppendMenu.Call(menu, MF_STRING, 2, uintptr(unsafe.Pointer(utf16("Диагностика"))))
	pAppendMenu.Call(menu, MF_STRING, 3, uintptr(unsafe.Pointer(utf16("Настройки"))))
	pAppendMenu.Call(menu, MF_STRING, 4, uintptr(unsafe.Pointer(utf16("Выход"))))
	var pt POINT
	pGetCursorPos.Call(uintptr(unsafe.Pointer(&pt)))
	pSetForegroundWindow.Call(w.hwnd)
	cmd, _, _ := pTrackPopupMenu.Call(menu, TPM_RIGHTBUTTON|0x0100, uintptr(pt.X), uintptr(pt.Y), 0, w.hwnd, 0)
	pDestroyMenu.Call(menu)
	switch cmd {
	case 1:
		w.showMain()
	case 2:
		w.openDiagnostics()
	case 3:
		w.openSettings()
	case 4:
		w.reallyExit = true
		pDestroyWindow.Call(w.hwnd)
	}
}

func (w *WinApp) createSettingsWindow() {
	if w.settingsHwnd != 0 {
		return
	}
	w.settingsHwnd = w.simpleWindow("Настройки Hockey Agent", 840, 700, syscall.NewCallback(settingsWndProc))
	y := 34
	w.settingsLabel("Основной Hockey Server URL", 40, y)
	w.settingsEdit(3001, "", 40, y+30, 760)
	y += 108
	w.settingsLabel("Резервный Server URL (необязательно)", 40, y)
	w.settingsEdit(3006, "", 40, y+30, 760)
	y += 108
	w.settingsLabel("vMix API URL", 40, y)
	w.settingsEdit(3002, "", 40, y+30, 760)
	y += 108
	w.settingsLabel("Device name", 40, y)
	w.settingsEdit(3003, "", 40, y+30, 760)

	w.settingsNote("Для удалённого сервера можно указать https://адрес — Agent сам подключится по WSS.\r\nПри переносе также можно положить server_url.txt рядом с agent.exe: этот адрес будет иметь приоритет.", 40, 470, 760, 58)
	w.settingsCheck(3004, "Запускать вместе с Windows", false, 40, 548)
	w.settingsCheck(3005, "Запускать свёрнутым в tray", false, 40, 590)
	w.settingsButton(idSaveSettings, "Сохранить", 526, 632, 130)
	w.settingsButton(idCancelSettings, "Отмена", 670, 632, 130)
	w.populateSettings()
}

func (w *WinApp) populateSettings() {
	cfg := w.configCache
	setText(w.settingsControls[3001], cfg.ServerURL)
	setText(w.settingsControls[3006], cfg.FallbackServerURL)
	setText(w.settingsControls[3002], cfg.VmixURL)
	setText(w.settingsControls[3003], cfg.DeviceName)
	check := func(id int, value bool) {
		state := uintptr(0)
		if value {
			state = BST_CHECKED
		}
		pSendMessage.Call(w.settingsControls[id], BM_SETCHECK, state, 0)
	}
	check(3004, cfg.RunWithWindows)
	check(3005, cfg.StartMinimized)
}

func (w *WinApp) openSettings() {
	if w.settingsHwnd == 0 {
		w.createSettingsWindow()
	}
	w.populateSettings()
	pShowWindow.Call(w.settingsHwnd, SW_SHOW)
	pSetForegroundWindow.Call(w.settingsHwnd)
}

func (w *WinApp) simpleWindow(title string, clientWidth, clientHeight int, proc uintptr) uintptr {
	inst, _, _ := pGetModuleHandle.Call(0)
	className := utf16(title + "Class123")
	cursor, _, _ := pLoadCursor.Call(0, IDC_ARROW)
	wc := WNDCLASSEX{CbSize: uint32(unsafe.Sizeof(WNDCLASSEX{})), LpfnWndProc: proc, HInstance: inst, HIcon: w.iconBig, HCursor: cursor, HbrBackground: w.bgBrush, LpszClassName: className, HIconSm: w.iconSmall}
	pRegisterClassEx.Call(uintptr(unsafe.Pointer(&wc)))
	style := uintptr(WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU)
	rect := RECT{Right: int32(clientWidth), Bottom: int32(clientHeight)}
	pAdjustWindowRectEx.Call(uintptr(unsafe.Pointer(&rect)), style, 0, 0)
	outerW := rect.Right - rect.Left
	outerH := rect.Bottom - rect.Top
	h, _, _ := pCreateWindowEx.Call(0, uintptr(unsafe.Pointer(className)), uintptr(unsafe.Pointer(utf16(title))), style, 0x80000000, 0x80000000, uintptr(outerW), uintptr(outerH), w.hwnd, 0, inst, 0)
	if h != 0 {
		pSendMessage.Call(h, WM_SETICON, ICON_BIG, w.iconBig)
		pSendMessage.Call(h, WM_SETICON, ICON_SMALL, w.iconSmall)
	}
	return h
}
func (w *WinApp) settingsLabel(text string, x, y int) {
	h, _, _ := pCreateWindowEx.Call(0, uintptr(unsafe.Pointer(utf16("STATIC"))), uintptr(unsafe.Pointer(utf16(text))), WS_CHILD|WS_VISIBLE, uintptr(x), uintptr(y), 760, 28, w.settingsHwnd, 0, 0, 0)
	pSendMessage.Call(h, WM_SETFONT, w.fontSmall, 1)
}
func (w *WinApp) settingsNote(text string, x, y, width, height int) {
	h, _, _ := pCreateWindowEx.Call(0, uintptr(unsafe.Pointer(utf16("STATIC"))), uintptr(unsafe.Pointer(utf16(text))), WS_CHILD|WS_VISIBLE, uintptr(x), uintptr(y), uintptr(width), uintptr(height), w.settingsHwnd, 0, 0, 0)
	pSendMessage.Call(h, WM_SETFONT, w.fontSmall, 1)
}
func (w *WinApp) settingsEdit(id int, text string, x, y, width int) {
	h, _, _ := pCreateWindowEx.Call(0, uintptr(unsafe.Pointer(utf16("EDIT"))), uintptr(unsafe.Pointer(utf16(text))), WS_CHILD|WS_VISIBLE|WS_TABSTOP|ES_AUTOHSCROLL, uintptr(x), uintptr(y), uintptr(width), 46, w.settingsHwnd, uintptr(id), 0, 0)
	pSendMessage.Call(h, WM_SETFONT, w.font, 1)
	w.settingsControls[id] = h
}
func (w *WinApp) settingsCheck(id int, text string, checked bool, x, y int) {
	h, _, _ := pCreateWindowEx.Call(0, uintptr(unsafe.Pointer(utf16("BUTTON"))), uintptr(unsafe.Pointer(utf16(text))), WS_CHILD|WS_VISIBLE|WS_TABSTOP|BS_AUTOCHECKBOX, uintptr(x), uintptr(y), 600, 34, w.settingsHwnd, uintptr(id), 0, 0)
	pSendMessage.Call(h, WM_SETFONT, w.font, 1)
	if checked {
		pSendMessage.Call(h, BM_SETCHECK, BST_CHECKED, 0)
	}
	w.settingsControls[id] = h
}
func (w *WinApp) settingsButton(id int, text string, x, y, width int) {
	h, _, _ := pCreateWindowEx.Call(0, uintptr(unsafe.Pointer(utf16("BUTTON"))), uintptr(unsafe.Pointer(utf16(text))), WS_CHILD|WS_VISIBLE|WS_TABSTOP|BS_PUSHBUTTON, uintptr(x), uintptr(y), uintptr(width), 46, w.settingsHwnd, uintptr(id), 0, 0)
	pSendMessage.Call(h, WM_SETFONT, w.font, 1)
}
func settingsWndProc(hwnd uintptr, msg uint32, wparam, lparam uintptr) uintptr {
	if app == nil {
		r, _, _ := pDefWindowProc.Call(hwnd, uintptr(msg), wparam, lparam)
		return r
	}
	switch msg {
	case WM_COMMAND:
		if hiword(wparam) == BN_CLICKED {
			switch loword(wparam) {
			case idSaveSettings:
				app.saveSettings()
			case idCancelSettings:
				pShowWindow.Call(hwnd, SW_HIDE)
			}
		}
		return 0
	case WM_CLOSE:
		pShowWindow.Call(hwnd, SW_HIDE)
		return 0
	case WM_CTLCOLORSTATIC:
		pSetBkMode.Call(wparam, TRANSPARENT)
		pSetTextColor.Call(wparam, rgb(203, 213, 225))
		b, _, _ := pGetStockObject.Call(NULL_BRUSH)
		return b
	case WM_CTLCOLOREDIT:
		pSetTextColor.Call(wparam, rgb(226, 232, 240))
		pSetBkMode.Call(wparam, TRANSPARENT)
		return app.editBrush
	}
	r, _, _ := pDefWindowProc.Call(hwnd, uintptr(msg), wparam, lparam)
	return r
}
func (w *WinApp) saveSettings() {
	cfg := w.configCache
	cfg.ServerURL = getText(w.settingsControls[3001])
	cfg.FallbackServerURL = getText(w.settingsControls[3006])
	cfg.VmixURL = getText(w.settingsControls[3002])
	cfg.DeviceName = getText(w.settingsControls[3003])
	cfg.RunWithWindows = checked(w.settingsControls[3004])
	cfg.StartMinimized = checked(w.settingsControls[3005])
	w.configCache = cfg
	pShowWindow.Call(w.settingsHwnd, SW_HIDE)

	// Saving may close a stale WebSocket and update Windows startup settings.
	// Those operations are intentionally kept off the GUI thread.
	go func(cfg Config) {
		var result string
		var isErr bool
		if err := w.agent.UpdateConfig(cfg); err != nil {
			result = err.Error()
			isErr = true
		} else if err := setRunAtStartup(cfg.RunWithWindows); err != nil {
			result = "Настройки сохранены, но не удалось обновить автозапуск: " + err.Error()
			isErr = true
		} else {
			result = "Настройки сохранены. Соединение будет обновлено автоматически."
		}
		w.uiMu.Lock()
		w.settingsResult = result
		w.settingsResultError = isErr
		w.uiMu.Unlock()
		pPostMessage.Call(w.hwnd, settingsDoneMsg, 0, 0)
	}(cfg)
}

func checked(hwnd uintptr) bool {
	r, _, _ := pSendMessage.Call(hwnd, BM_GETCHECK, 0, 0)
	return r == BST_CHECKED
}
func getText(hwnd uintptr) string {
	n, _, _ := pGetWindowTextLength.Call(hwnd)
	buf := make([]uint16, n+1)
	pGetWindowText.Call(hwnd, uintptr(unsafe.Pointer(&buf[0])), n+1)
	return syscall.UTF16ToString(buf)
}
func message(hwnd uintptr, title, text string, flags uintptr) {
	pMessageBox.Call(hwnd, uintptr(unsafe.Pointer(utf16(text))), uintptr(unsafe.Pointer(utf16(title))), MB_OK|flags)
}
func setRunAtStartup(enabled bool) error {
	exe, err := os.Executable()
	if err != nil {
		return err
	}
	key := `HKCU\Software\Microsoft\Windows\CurrentVersion\Run`
	if enabled {
		return exec.Command("reg", "add", key, "/v", "HockeyVmixAgent", "/t", "REG_SZ", "/d", `"`+exe+`"`, "/f").Run()
	}
	_ = exec.Command("reg", "delete", key, "/v", "HockeyVmixAgent", "/f").Run()
	return nil
}

func (w *WinApp) createDiagnosticsWindow() {
	if w.diagHwnd != 0 {
		return
	}
	w.diagHwnd = w.simpleWindow("Диагностика Hockey Agent", 1080, 760, syscall.NewCallback(diagWndProc))
	w.diagEdit = w.diagControl("EDIT", "", WS_CHILD|WS_VISIBLE|ES_MULTILINE|ES_AUTOVSCROLL|ES_READONLY|WS_VSCROLL, 28, 28, 1024, 640, 0)
	w.diagControl("BUTTON", "Копировать лог", WS_CHILD|WS_VISIBLE|WS_TABSTOP|BS_PUSHBUTTON, 842, 690, 210, 46, idCopyLog)
}

func (w *WinApp) refreshDiagnostics() {
	logs := w.agent.Logs()
	if logs == w.lastDiagLog {
		return
	}
	w.lastDiagLog = logs
	setText(w.diagEdit, logs)
}

func (w *WinApp) openDiagnostics() {
	if w.diagHwnd == 0 {
		w.createDiagnosticsWindow()
	}
	w.refreshDiagnostics()
	pShowWindow.Call(w.diagHwnd, SW_SHOW)
	pSetForegroundWindow.Call(w.diagHwnd)
}

func (w *WinApp) diagControl(class, text string, style uintptr, x, y, width, height, id int) uintptr {
	h, _, _ := pCreateWindowEx.Call(0, uintptr(unsafe.Pointer(utf16(class))), uintptr(unsafe.Pointer(utf16(text))), style, uintptr(x), uintptr(y), uintptr(width), uintptr(height), w.diagHwnd, uintptr(id), 0, 0)
	font := w.font
	if class == "EDIT" {
		font = w.fontDiag
	}
	pSendMessage.Call(h, WM_SETFONT, font, 1)
	return h
}
func diagWndProc(hwnd uintptr, msg uint32, wparam, lparam uintptr) uintptr {
	if app == nil {
		r, _, _ := pDefWindowProc.Call(hwnd, uintptr(msg), wparam, lparam)
		return r
	}
	switch msg {
	case WM_COMMAND:
		if hiword(wparam) == BN_CLICKED && loword(wparam) == idCopyLog {
			copyClipboard(app.agent.Logs())
			message(hwnd, "Диагностика", "Лог скопирован в буфер обмена.", MB_ICONINFORMATION)
		}
		return 0
	case WM_CLOSE:
		pShowWindow.Call(hwnd, SW_HIDE)
		return 0
	case WM_CTLCOLORSTATIC:
		if lparam == app.diagEdit {
			pSetTextColor.Call(wparam, rgb(226, 232, 240))
			pSetBkMode.Call(wparam, TRANSPARENT)
			return app.editBrush
		}
		pSetBkMode.Call(wparam, TRANSPARENT)
		pSetTextColor.Call(wparam, rgb(203, 213, 225))
		b, _, _ := pGetStockObject.Call(NULL_BRUSH)
		return b
	case WM_CTLCOLOREDIT:
		pSetTextColor.Call(wparam, rgb(226, 232, 240))
		pSetBkMode.Call(wparam, TRANSPARENT)
		return app.editBrush
	}
	r, _, _ := pDefWindowProc.Call(hwnd, uintptr(msg), wparam, lparam)
	return r
}
func copyClipboard(text string) {
	u, _ := syscall.UTF16FromString(text)
	size := uintptr(len(u) * 2)
	h, _, _ := pGlobalAlloc.Call(GMEM_MOVEABLE, size)
	if h == 0 {
		return
	}
	ptr, _, _ := pGlobalLock.Call(h)
	if ptr == 0 {
		return
	}
	if len(u) > 0 {
		pRtlMoveMemory.Call(ptr, uintptr(unsafe.Pointer(&u[0])), size)
	}
	pGlobalUnlock.Call(h)
	if r, _, _ := pOpenClipboard.Call(app.hwnd); r == 0 {
		return
	}
	pEmptyClipboard.Call()
	pSetClipboardData.Call(CF_UNICODETEXT, h)
	pCloseClipboard.Call()
}
