//go:build windows

package wintray

import (
	"fmt"
	"log/slog"
	"sync"
	"unsafe"

	"golang.org/x/sys/windows"
)

var (
	quitOnce            sync.Once
	UI_REQUEST_MSG_ID   = WM_USER + 2
	FOCUS_WINDOW_MSG_ID = WM_USER + 3
)

func (t *winTray) TrayRun() {
	// Main message pump.
	slog.Debug("starting event handling loop")
	m := &struct {
		WindowHandle windows.Handle
		Message      uint32
		Wparam       uintptr
		Lparam       uintptr
		Time         uint32
		Pt           point
		LPrivate     uint32
	}{}
	for {
		ret, _, err := pGetMessage.Call(uintptr(unsafe.Pointer(m)), 0, 0, 0)

		// Ignore WM_QUIT messages from the UI window, which shouldn't exit the main app
		if m.Message == WM_QUIT && t.app.UIRunning() {
			if t.app != nil {
				slog.Debug("converting WM_QUIT to terminate call on webview")
				t.app.UITerminate()
			}
			// Drain any other WM_QUIT messages
			for {
				ret, _, err = pGetMessage.Call(uintptr(unsafe.Pointer(m)), 0, 0, 0)
				if m.Message != WM_QUIT {
					break
				}
			}
		}

		// If the function retrieves a message other than WM_QUIT, the return value is nonzero.
		// If the function retrieves the WM_QUIT message, the return value is zero.
		// If there is an error, the return value is -1
		// https://msdn.microsoft.com/en-us/library/windows/desktop/ms644936(v=vs.85).aspx
		switch int32(ret) {
		case -1:
			slog.Error(fmt.Sprintf("get message failure: %v", err))
			return
		case 0:
			// slog.Debug("XXX tray run loop exiting from handling", "message", fmt.Sprintf("0x%x", m.Message), "wParam", fmt.Sprintf("0x%x", m.Wparam), "lParam", fmt.Sprintf("0x%x", m.Lparam))
			return
		default:
			pTranslateMessage.Call(uintptr(unsafe.Pointer(m))) //nolint:errcheck
			pDispatchMessage.Call(uintptr(unsafe.Pointer(m)))  //nolint:errcheck
		}
	}
}

// WindowProc callback function that processes messages sent to a window.
// https://msdn.microsoft.com/en-us/library/windows/desktop/ms633573(v=vs.85).aspx
func (t *winTray) wndProc(hWnd windows.Handle, message uint32, wParam, lParam uintptr) (lResult uintptr) {
	// slog.Debug("XXX in winTray.wndProc", "message", fmt.Sprintf("0x%x", message), "wParam", fmt.Sprintf("0x%x", wParam), "lParam", fmt.Sprintf("0x%x", lParam))
	switch message {
	case WM_COMMAND:
		menuItemId := int32(wParam)
		// https://docs.microsoft.com/en-us/windows/win32/menurc/wm-command#menus
		switch menuItemId {
		case quitMenuID:
			t.app.Quit()
		case updateMenuID:
			t.app.DoUpdate()
		case openUIMenuID:
			// UI must be initialized on this thread so don't use the callbacks
			t.app.UIShow()
		case settingsUIMenuID:
			// UI must be initialized on this thread so don't use the callbacks
			t.app.UIRun("/settings")
		case diagLogsMenuID:
			t.showLogs()
		default:
			slog.Debug(fmt.Sprintf("Unexpected menu item id: %d", menuItemId))
			lResult, _, _ = pDefWindowProc.Call(
				uintptr(hWnd),
				uintptr(message),
				wParam,
				lParam,
			)
		}
	case WM_CLOSE:
		// TODO - does this need adjusting?
		// slog.Debug("XXX WM_CLOSE triggered")
		boolRet, _, err := pDestroyWindow.Call(uintptr(t.window))
		if boolRet == 0 {
			slog.Error(fmt.Sprintf("failed to destroy window: %s", err))
		}
		err = t.wcex.unregister()
		if err != nil {
			slog.Error(fmt.Sprintf("failed to uregister windo %s", err))
		}
	case WM_DESTROY:
		// slog.Debug("XXX WM_DESTROY triggered")
		// TODO - does this need adjusting?
		// same as WM_ENDSESSION, but throws 0 exit code after all
		defer pPostQuitMessage.Call(uintptr(int32(0))) //nolint:errcheck
		fallthrough
	case WM_ENDSESSION:
		// slog.Debug("XXX WM_ENDSESSION triggered")
		t.muNID.Lock()
		if t.nid != nil {
			err := t.nid.delete()
			if err != nil {
				slog.Error(fmt.Sprintf("failed to delete nid: %s", err))
			}
		}
		t.muNID.Unlock()
	case t.wmSystrayMessage:
		switch lParam {
		case WM_MOUSEMOVE, WM_LBUTTONDOWN:
			// Ignore these...
		case WM_RBUTTONUP, WM_LBUTTONUP:
			err := t.showMenu()
			if err != nil {
				slog.Error(fmt.Sprintf("failed to show menu: %s", err))
			}
		case 0x405: // TODO - how is this magic value derived for the notification left click
			if t.pendingUpdate {
				// TODO - revamp how detecting an update is notified to the user
				t.app.DoUpdate()
			}
		case 0x404: // Middle click or close notification
			// slog.Debug("doing nothing on close of first time notification")
		default:
			// 0x402 also seems common - what is it?
			slog.Debug(fmt.Sprintf("unmanaged app message, lParm: 0x%x", lParam))
			lResult, _, _ = pDefWindowProc.Call(
				uintptr(hWnd),
				uintptr(message),
				wParam,
				lParam,
			)
		}
	case t.wmTaskbarCreated: // on explorer.exe restarts
		t.muNID.Lock()
		err := t.nid.add()
		if err != nil {
			slog.Error(fmt.Sprintf("failed to refresh the taskbar on explorer restart: %s", err))
		}
		t.muNID.Unlock()
	case uint32(UI_REQUEST_MSG_ID):
		// Requests for the UI must always come from the main event thread
		l := int(wParam)
		path := unsafe.String((*byte)(unsafe.Pointer(lParam)), l)
		t.app.UIRun(path)
	case WM_COPYDATA:
		// Handle URL scheme requests from other instances
		if lParam != 0 {
			cds := (*COPYDATASTRUCT)(unsafe.Pointer(lParam))
			if cds.DwData == 1 { // Our identifier for URL scheme messages
				// Convert the data back to string
				data := make([]byte, cds.CbData)
				copy(data, (*[1 << 30]byte)(unsafe.Pointer(cds.LpData))[:cds.CbData:cds.CbData])
				urlScheme := string(data)
				handleURLSchemeRequest(urlScheme)
				lResult = 1 // Return non-zero to indicate success
			}
		}
	case uint32(FOCUS_WINDOW_MSG_ID):
		// Handle focus window request from another instance
		if t.app.UIRunning() {
			// If UI is already running, just show it
			t.app.UIShow()
		} else {
			// If UI is not running, start it
			t.app.UIRun("/")
		}
		lResult = 1 // Return non-zero to indicate success
	default:
		// Calls the default window procedure to provide default processing for any window messages that an application does not process.
		// https://msdn.microsoft.com/en-us/library/windows/desktop/ms633572(v=vs.85).aspx
		// slog.Debug("XXX passing through", "message", fmt.Sprintf("0x%x", message), "wParam", fmt.Sprintf("0x%x", wParam), "lParam", fmt.Sprintf("0x%x", lParam))
		lResult, _, _ = pDefWindowProc.Call(
			uintptr(hWnd),
			uintptr(message),
			wParam,
			lParam,
		)
	}
	return
}

func (t *winTray) Quit() {
	// slog.Debug("XXX in winTray.Quit")
	t.quitting = true
	quitOnce.Do(quit)
}

func SendUIRequestMessage(path string) {
	boolRet, _, err := pPostMessage.Call(
		uintptr(wt.window),
		uintptr(UI_REQUEST_MSG_ID),
		uintptr(len(path)),
		uintptr(unsafe.Pointer(unsafe.StringData(path))),
	)
	if boolRet == 0 {
		slog.Error(fmt.Sprintf("failed to post UI request message %s", err))
	}
}

func quit() {
	boolRet, _, err := pPostMessage.Call(
		uintptr(wt.window),
		WM_CLOSE,
		0,
		0,
	)
	if boolRet == 0 {
		slog.Error(fmt.Sprintf("failed to post close message on shutdown %s", err))
	}
}

// findExistingInstance attempts to find an existing Ollama instance window
// Returns the window handle if found, 0 if not found
func findExistingInstance() uintptr {
	classNamePtr, err := windows.UTF16PtrFromString(ClassName)
	if err != nil {
		slog.Error("failed to convert class name to UTF16", "error", err)
		return 0
	}

	hwnd, _, _ := pFindWindow.Call(
		uintptr(unsafe.Pointer(classNamePtr)),
		0, // window name (null = any)
	)

	return hwnd
}

// CheckAndSendToExistingInstance attempts to send a URL scheme to an existing instance
// Returns true if successfully sent to existing instance, false if no instance found
func CheckAndSendToExistingInstance(urlScheme string) bool {
	hwnd := findExistingInstance()
	if hwnd == 0 {
		// No existing window found
		return false
	}

	data := []byte(urlScheme)
	cds := COPYDATASTRUCT{
		DwData: 1, // 1 to identify URL scheme messages
		CbData: uint32(len(data)),
		LpData: uintptr(unsafe.Pointer(&data[0])),
	}

	result, _, err := pSendMessage.Call(
		hwnd,
		uintptr(WM_COPYDATA),
		0, // wParam is handle to sending window (0 is ok)
		uintptr(unsafe.Pointer(&cds)),
	)

	// SendMessage returns the result from the window procedure
	// For WM_COPYDATA, non-zero means success
	if result == 0 {
		slog.Error("failed to send URL scheme message to existing instance", "error", err)
		return false
	}
	return true
}

// handleURLSchemeRequest processes a URL scheme request
func handleURLSchemeRequest(urlScheme string) {
	if urlScheme == "" {
		slog.Warn("empty URL scheme request")
		return
	}

	// Call the app callback to handle URL scheme requests
	// This will delegate to the main app logic
	if wt.app != nil {
		if urlHandler, ok := wt.app.(URLSchemeHandler); ok {
			urlHandler.HandleURLScheme(urlScheme)
		} else {
			slog.Warn("app does not implement URLSchemeHandler interface")
		}
	} else {
		slog.Warn("wt.app is nil")
	}
}

// CheckAndFocusExistingInstance attempts to find an existing instance and optionally focus it
// Returns true if an existing instance was found, false otherwise
func CheckAndFocusExistingInstance(shouldFocus bool) bool {
	hwnd := findExistingInstance()
	if hwnd == 0 {
		// No existing window found
		return false
	}

	if !shouldFocus {
		slog.Info("existing instance found, not focusing due to startHidden")
		return true
	}

	// Send focus message to existing instance
	result, _, err := pSendMessage.Call(
		hwnd,
		uintptr(FOCUS_WINDOW_MSG_ID),
		0, // wParam not used
		0, // lParam not used
	)

	// SendMessage returns the result from the window procedure
	// For our custom message, non-zero means success
	if result == 0 {
		slog.Error("failed to send focus message to existing instance", "error", err)
		return false
	}

	slog.Info("sent focus request to existing instance")

	return true
}
