//go:build windows

package platform

import (
	"bytes"
	"fmt"
	"image/png"
	"strings"
	"unsafe"

	"github.com/stevec/fancywin/internal/assets"
	"golang.org/x/sys/windows"
)

const (
	wmAppTray       = 0x8001
	wmRButtonUp     = 0x0205
	wmContextMenu   = 0x007B
	wmNull          = 0x0000
	nimAdd          = 0x00000000
	nimModify       = 0x00000001
	nimDelete       = 0x00000002
	nifMessage      = 0x00000001
	nifIcon         = 0x00000002
	nifTip          = 0x00000004
	mfString        = 0x00000000
	mfGray          = 0x00000001
	mfChecked       = 0x00000008
	mfSeparator     = 0x00000800
	tpmRightButton  = 0x00000002
	tpmReturnCmd    = 0x00000100
	tpmNonotify     = 0x00000080
	trayCommandExit = 1001
	trayLayoutBase  = 2000
	idiApplication  = 32512
	biRGB           = 0
	dibRGBColors    = 0
)

var (
	shell32                   = windows.NewLazySystemDLL("shell32.dll")
	procShellNotifyIcon       = shell32.NewProc("Shell_NotifyIconW")
	procLoadIcon              = user32.NewProc("LoadIconW")
	procCreatePopupMenu       = user32.NewProc("CreatePopupMenu")
	procAppendMenu            = user32.NewProc("AppendMenuW")
	procTrackPopupMenu        = user32.NewProc("TrackPopupMenu")
	procDestroyMenu           = user32.NewProc("DestroyMenu")
	procSetForegroundWindow   = user32.NewProc("SetForegroundWindow")
	procPostMessage           = user32.NewProc("PostMessageW")
	procPostQuitMessage       = user32.NewProc("PostQuitMessage")
	procRegisterWindowMessage = user32.NewProc("RegisterWindowMessageW")
	procCreateIconIndirect    = user32.NewProc("CreateIconIndirect")
	procDestroyIcon           = user32.NewProc("DestroyIcon")
	procCreateDIBSection      = gdi32.NewProc("CreateDIBSection")
	procCreateBitmap          = gdi32.NewProc("CreateBitmap")
	trayClassName, _          = windows.UTF16PtrFromString("FancyWinTrayWindow")
	trayWindowProc            = windows.NewCallback(trayWndProc)
	activeTray                *trayIcon
)

type notifyIconData struct {
	Size             uint32
	Window           windows.Handle
	ID               uint32
	Flags            uint32
	CallbackMessage  uint32
	Icon             windows.Handle
	Tip              [128]uint16
	State            uint32
	StateMask        uint32
	Info             [256]uint16
	TimeoutOrVersion uint32
	InfoTitle        [64]uint16
	InfoFlags        uint32
	GUID             [16]byte
	BalloonIcon      windows.Handle
}

type trayIcon struct {
	hwnd           windows.Handle
	icon           windows.Handle
	activeLayout   string
	layouts        []string
	taskbarMessage uint32
	ownsIcon       bool
}

type bitmapInfoHeader struct {
	Size          uint32
	Width         int32
	Height        int32
	Planes        uint16
	BitCount      uint16
	Compression   uint32
	SizeImage     uint32
	XPelsPerMeter int32
	YPelsPerMeter int32
	ClrUsed       uint32
	ClrImportant  uint32
}

type iconInfo struct {
	Icon     int32
	XHotspot uint32
	YHotspot uint32
	Mask     windows.Handle
	Color    windows.Handle
}

func newTrayIcon(activeLayout string, layouts []string) (*trayIcon, error) {
	instance, _, instanceErr := procGetModuleHandle.Call(0)
	if instance == 0 {
		return nil, fmt.Errorf("tray GetModuleHandleW: %v", instanceErr)
	}
	wc := windowClassEx{
		Size:      uint32(unsafe.Sizeof(windowClassEx{})),
		WndProc:   trayWindowProc,
		Instance:  windows.Handle(instance),
		ClassName: trayClassName,
	}
	if atom, _, err := procRegisterClassEx.Call(uintptr(unsafe.Pointer(&wc))); atom == 0 {
		return nil, fmt.Errorf("register tray window class: %v", err)
	}

	raw, _, createErr := procCreateWindowEx.Call(
		0,
		uintptr(unsafe.Pointer(trayClassName)),
		uintptr(unsafe.Pointer(trayClassName)),
		0, 0, 0, 0, 0, 0, 0, instance, 0,
	)
	if raw == 0 {
		return nil, fmt.Errorf("create tray message window: %v", createErr)
	}
	icon, iconErr := loadFancyIcon()
	ownsIcon := icon != 0
	if icon == 0 {
		fallback, _, fallbackErr := procLoadIcon.Call(0, idiApplication)
		if fallback == 0 {
			procDestroyWindow.Call(raw)
			return nil, fmt.Errorf("load tray icon: generated icon: %v; fallback: %v", iconErr, fallbackErr)
		}
		icon = windows.Handle(fallback)
	}
	taskbarName, _ := windows.UTF16PtrFromString("TaskbarCreated")
	taskbarMessage, _, _ := procRegisterWindowMessage.Call(uintptr(unsafe.Pointer(taskbarName)))

	t := &trayIcon{
		hwnd: windows.Handle(raw), icon: windows.Handle(icon),
		activeLayout: activeLayout, layouts: append([]string(nil), layouts...),
		taskbarMessage: uint32(taskbarMessage), ownsIcon: ownsIcon,
	}
	activeTray = t
	if err := t.notify(nimAdd); err != nil {
		activeTray = nil
		procDestroyWindow.Call(raw)
		return nil, err
	}
	return t, nil
}

func (t *trayIcon) close() {
	if t == nil || t.hwnd == 0 {
		return
	}
	_ = t.notify(nimDelete)
	procDestroyWindow.Call(uintptr(t.hwnd))
	if t.ownsIcon && t.icon != 0 {
		procDestroyIcon.Call(uintptr(t.icon))
	}
	if activeTray == t {
		activeTray = nil
	}
	t.hwnd = 0
}

func (t *trayIcon) setLayouts(active string, layouts []string) {
	if t == nil {
		return
	}
	t.activeLayout = active
	t.layouts = append(t.layouts[:0], layouts...)
	_ = t.notify(nimModify)
}

func (t *trayIcon) notify(operation uintptr) error {
	nid := notifyIconData{
		Size: uint32(unsafe.Sizeof(notifyIconData{})), Window: t.hwnd, ID: 1,
		Flags: nifMessage | nifIcon | nifTip, CallbackMessage: wmAppTray, Icon: t.icon,
	}
	tip, _ := windows.UTF16FromString("FancyWin — " + t.activeLayout)
	copy(nid.Tip[:len(nid.Tip)-1], tip)
	if ok, _, err := procShellNotifyIcon.Call(operation, uintptr(unsafe.Pointer(&nid))); ok == 0 {
		return fmt.Errorf("update notification-area icon: %v", err)
	}
	return nil
}

func trayWndProc(rawHWND, message, wParam, lParam uintptr) uintptr {
	t := activeTray
	if t != nil && t.taskbarMessage != 0 && uint32(message) == t.taskbarMessage {
		_ = t.notify(nimAdd) // Explorer was restarted; restore the icon.
		return 0
	}
	if message == wmAppTray && (uint32(lParam) == wmRButtonUp || uint32(lParam) == wmContextMenu) {
		if t != nil {
			t.showMenu()
		}
		return 0
	}
	r, _, _ := procDefWindowProc.Call(rawHWND, message, wParam, lParam)
	return r
}

func (t *trayIcon) showMenu() {
	menu, _, _ := procCreatePopupMenu.Call()
	if menu == 0 {
		return
	}
	defer procDestroyMenu.Call(menu)
	heading, _ := windows.UTF16PtrFromString("Layouts")
	exit, _ := windows.UTF16PtrFromString("Exit")
	procAppendMenu.Call(menu, mfString|mfGray, 0, uintptr(unsafe.Pointer(heading)))
	for i, name := range t.layouts {
		label, _ := windows.UTF16PtrFromString(name)
		flags := uintptr(mfString)
		if strings.EqualFold(name, t.activeLayout) {
			flags |= mfChecked
		}
		procAppendMenu.Call(menu, flags, uintptr(trayLayoutBase+i), uintptr(unsafe.Pointer(label)))
	}
	procAppendMenu.Call(menu, mfSeparator, 0, 0)
	procAppendMenu.Call(menu, mfString, trayCommandExit, uintptr(unsafe.Pointer(exit)))

	var p point
	if ok, _, _ := procGetCursorPos.Call(uintptr(unsafe.Pointer(&p))); ok == 0 {
		return
	}
	procSetForegroundWindow.Call(uintptr(t.hwnd))
	command, _, _ := procTrackPopupMenu.Call(menu, tpmRightButton|tpmReturnCmd|tpmNonotify, uintptr(p.X), uintptr(p.Y), 0, uintptr(t.hwnd), 0)
	procPostMessage.Call(uintptr(t.hwnd), wmNull, 0, 0)
	if command == trayCommandExit {
		procPostQuitMessage.Call(0)
	} else if command >= trayLayoutBase && command < uintptr(trayLayoutBase+len(t.layouts)) {
		if e := activeEngine; e != nil {
			e.selectLayout(t.layouts[int(command)-trayLayoutBase])
		}
	}
}

func loadFancyIcon() (windows.Handle, error) {
	img, err := png.Decode(bytes.NewReader(assets.TrayIconPNG))
	if err != nil {
		return 0, err
	}
	bounds := img.Bounds()
	w, h := bounds.Dx(), bounds.Dy()
	if w < 1 || h < 1 {
		return 0, fmt.Errorf("embedded icon is empty")
	}
	header := bitmapInfoHeader{
		Size: uint32(unsafe.Sizeof(bitmapInfoHeader{})), Width: int32(w), Height: -int32(h),
		Planes: 1, BitCount: 32, Compression: biRGB, SizeImage: uint32(w * h * 4),
	}
	var bits unsafe.Pointer
	color, _, dibErr := procCreateDIBSection.Call(0, uintptr(unsafe.Pointer(&header)), dibRGBColors, uintptr(unsafe.Pointer(&bits)), 0, 0)
	if color == 0 || bits == nil {
		return 0, fmt.Errorf("CreateDIBSection: %v", dibErr)
	}
	defer procDeleteObject.Call(color)
	pixels := unsafe.Slice((*byte)(bits), w*h*4)
	for y := 0; y < h; y++ {
		for x := 0; x < w; x++ {
			r, g, b, a := img.At(bounds.Min.X+x, bounds.Min.Y+y).RGBA()
			i := (y*w + x) * 4
			pixels[i+0] = byte(b >> 8)
			pixels[i+1] = byte(g >> 8)
			pixels[i+2] = byte(r >> 8)
			pixels[i+3] = byte(a >> 8)
		}
	}
	maskStride := ((w + 15) / 16) * 2
	maskBits := make([]byte, maskStride*h)
	mask, _, maskErr := procCreateBitmap.Call(uintptr(w), uintptr(h), 1, 1, uintptr(unsafe.Pointer(&maskBits[0])))
	if mask == 0 {
		return 0, fmt.Errorf("CreateBitmap icon mask: %v", maskErr)
	}
	defer procDeleteObject.Call(mask)
	info := iconInfo{Icon: 1, Mask: windows.Handle(mask), Color: windows.Handle(color)}
	icon, _, iconErr := procCreateIconIndirect.Call(uintptr(unsafe.Pointer(&info)))
	if icon == 0 {
		return 0, fmt.Errorf("CreateIconIndirect: %v", iconErr)
	}
	return windows.Handle(icon), nil
}
