//go:build windows

package main

import (
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"regexp"
	"sort"
	"strconv"
	"strings"
	"syscall"
	"time"
	"unsafe"

	"vrc_osc_go/internal/app"
)

const (
	wsOverlappedWindow = 0x00CF0000
	wsVisible          = 0x10000000
	wsChild            = 0x40000000
	wsBorder           = 0x00800000
	esMultiline        = 0x0004
	esReadonly         = 0x0800
	esAutovscroll      = 0x0040
	esAutohscroll      = 0x0080
	wmCreateGUI        = 0x0001
	wmDestroyGUI       = 0x0002
	wmTimerGUI         = 0x0113
	wmCommandGUI       = 0x0111
	idStatus           = 2001
	idLog              = 2002
	timerRefresh       = 1
)

var joinLeaveLinePattern = regexp.MustCompile(`^\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\]\s+\[(join|leave)\]\s+(.+?)\s+\((\d+)\)$`)
var runtimeTimePattern = regexp.MustCompile(`^\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\]`)

type guiApp struct {
	hwnd      uintptr
	leftHwnd  uintptr
	rightHwnd uintptr
	tabJoin   uintptr
	tabDiscord uintptr
	tabTranslate uintptr
	activeTab string
	lastInstanceCount int
}

type userState struct {
	Name     string
	Present  bool
	LastJoin time.Time
	LastLeave time.Time
}

func runtimeDir() string {
	exe, err := os.Executable()
	if err != nil {
		return "runtime"
	}
	return filepath.Join(filepath.Dir(exe), "runtime")
}

func guiLog(text string) {
	_ = app.AppendRuntimeLog("GUI", text)
}

func runNativeGUI() error {
	user32 := syscall.NewLazyDLL("user32.dll")
	kernel32 := syscall.NewLazyDLL("kernel32.dll")
	registerClass := user32.NewProc("RegisterClassW")
	createWindowEx := user32.NewProc("CreateWindowExW")
	defWindowProc := user32.NewProc("DefWindowProcW")
	showWindow := user32.NewProc("ShowWindow")
	updateWindow := user32.NewProc("UpdateWindow")
	getMessage := user32.NewProc("GetMessageW")
	translateMessage := user32.NewProc("TranslateMessage")
	dispatchMessage := user32.NewProc("DispatchMessageW")
	postQuitMessage := user32.NewProc("PostQuitMessage")
	setTimer := user32.NewProc("SetTimer")
	loadCursor := user32.NewProc("LoadCursorW")
	setWindowText := user32.NewProc("SetWindowTextW")
	getModuleHandle := kernel32.NewProc("GetModuleHandleW")
	loadIcon := user32.NewProc("LoadIconW")

	type wndClass struct {
		style         uint32
		lpfnWndProc   uintptr
		cbClsExtra    int32
		cbWndExtra    int32
		hInstance     uintptr
		hIcon         uintptr
		hCursor       uintptr
		hbrBackground uintptr
		lpszMenuName  *uint16
		lpszClassName *uint16
	}
	type msg struct {
		hwnd    uintptr
		message uint32
		wParam  uintptr
		lParam  uintptr
		time    uint32
		pt      struct{ X, Y int32 }
	}

	hInstance, _, _ := getModuleHandle.Call(0)
	className, _ := syscall.UTF16PtrFromString("VRC_OSC_NATIVE_GUI")
	title, _ := syscall.UTF16PtrFromString("VRC OSC")
	cursor, _, _ := loadCursor.Call(0, 32512)
	icon, _, _ := loadIcon.Call(0, 32512)
	guiLog("stage=prepare window")

	var app guiApp
	wndProc := syscall.NewCallback(func(hwnd uintptr, message uint32, wParam, lParam uintptr) uintptr {
		switch message {
		case wmCreateGUI:
			buttonClass, _ := syscall.UTF16PtrFromString("BUTTON")
			leftClass, _ := syscall.UTF16PtrFromString("EDIT")
			rightClass, _ := syscall.UTF16PtrFromString("EDIT")
			leftTitle, _ := syscall.UTF16PtrFromString("")
			rightTitle, _ := syscall.UTF16PtrFromString("")
			joinTitle, _ := syscall.UTF16PtrFromString("Join Log")
			discordTitle, _ := syscall.UTF16PtrFromString("Discord")
			translateTitle, _ := syscall.UTF16PtrFromString("Translate")
			app.tabJoin, _, _ = createWindowEx.Call(0, uintptr(unsafe.Pointer(buttonClass)), uintptr(unsafe.Pointer(joinTitle)), wsVisible|wsChild, 370, 20, 90, 28, hwnd, 3001, hInstance, 0)
			app.tabDiscord, _, _ = createWindowEx.Call(0, uintptr(unsafe.Pointer(buttonClass)), uintptr(unsafe.Pointer(discordTitle)), wsVisible|wsChild, 470, 20, 90, 28, hwnd, 3002, hInstance, 0)
			app.tabTranslate, _, _ = createWindowEx.Call(0, uintptr(unsafe.Pointer(buttonClass)), uintptr(unsafe.Pointer(translateTitle)), wsVisible|wsChild, 570, 20, 90, 28, hwnd, 3003, hInstance, 0)
			app.leftHwnd, _, _ = createWindowEx.Call(0, uintptr(unsafe.Pointer(leftClass)), uintptr(unsafe.Pointer(leftTitle)), wsVisible|wsChild|wsBorder|esMultiline|esReadonly|esAutovscroll|esAutohscroll, 20, 20, 330, 300, hwnd, idStatus, hInstance, 0)
			app.rightHwnd, _, _ = createWindowEx.Call(0, uintptr(unsafe.Pointer(rightClass)), uintptr(unsafe.Pointer(rightTitle)), wsVisible|wsChild|wsBorder|esMultiline|esReadonly|esAutovscroll|esAutohscroll, 370, 60, 330, 260, hwnd, idLog, hInstance, 0)
			app.activeTab = "join"
			setTimer.Call(hwnd, timerRefresh, 2000, 0)
			refreshGUI(setWindowText, &app)
			return 0
		case wmCommandGUI:
			switch uint16(wParam & 0xffff) {
			case 3001:
				app.activeTab = "join"
			case 3002:
				app.activeTab = "discord"
			case 3003:
				app.activeTab = "translate"
			}
			refreshGUI(setWindowText, &app)
			return 0
		case wmTimerGUI:
			refreshGUI(setWindowText, &app)
			return 0
		case wmDestroyGUI:
			postQuitMessage.Call(0)
			return 0
		}
		ret, _, _ := defWindowProc.Call(hwnd, uintptr(message), wParam, lParam)
		return ret
	})

	guiLog("stage=register class")
	atom, _, regErr := registerClass.Call(uintptr(unsafe.Pointer(&wndClass{
		style:         0,
		lpfnWndProc:   wndProc,
		hInstance:     hInstance,
		hIcon:         icon,
		hCursor:       cursor,
		hbrBackground: 6,
		lpszClassName: className,
	})))
	if atom == 0 {
		guiLog(fmt.Sprintf("stage=register class failed err=%v", regErr))
		return fmt.Errorf("register class: %v", regErr)
	}

	guiLog("stage=create window")
	app.hwnd, _, _ = createWindowEx.Call(0, uintptr(unsafe.Pointer(className)), uintptr(unsafe.Pointer(title)), wsOverlappedWindow, 200, 120, 760, 380, 0, 0, hInstance, 0)
	if app.hwnd == 0 {
		guiLog("stage=create window failed")
		return fmt.Errorf("create window failed")
	}

	guiLog("stage=show window")
	showWindow.Call(app.hwnd, 5)
	updateWindow.Call(app.hwnd)
	guiLog("stage=message loop")

	var m msg
	for {
		r, _, _ := getMessage.Call(uintptr(unsafe.Pointer(&m)), 0, 0, 0)
		if int32(r) <= 0 {
			break
		}
		translateMessage.Call(uintptr(unsafe.Pointer(&m)))
		dispatchMessage.Call(uintptr(unsafe.Pointer(&m)))
	}
	return nil
}

func refreshGUI(setWindowText *syscall.LazyProc, app *guiApp) {
	current, instanceCount := currentUsersFromJoinLeave()
	if instanceCount == 0 && app.lastInstanceCount > 0 {
		instanceCount = app.lastInstanceCount
	}
	if instanceCount > 0 {
		app.lastInstanceCount = instanceCount
	}
	if app.leftHwnd != 0 {
		t, _ := syscall.UTF16PtrFromString(formatGuestPane("Join Log", current))
		setWindowText.Call(app.leftHwnd, uintptr(unsafe.Pointer(t)))
	}
	if app.rightHwnd != 0 {
		t, _ := syscall.UTF16PtrFromString(formatRightPane(app.activeTab, readRuntimeSnapshot(), currentWorldLabel(), instanceCount))
		setWindowText.Call(app.rightHwnd, uintptr(unsafe.Pointer(t)))
	}
}

func currentUsersFromJoinLeave() ([]userState, int) {
	since := currentWorldStartTime()
	users := map[string]*userState{}
	lastLoggedCount := 0
	for _, s := range readGuestSnapshot() {
		if !s.Present {
			continue
		}
		cp := s
		users[s.Name] = &userState{
			Name:     s.Name,
			Present:  true,
			LastJoin: cp.LastJoin,
		}
	}

	p := filepath.Join(runtimeDir(), "join_leave.log")
	b, err := os.ReadFile(p)
	if err != nil {
		out := make([]userState, 0, len(users))
		for _, u := range users {
			out = append(out, *u)
		}
		sort.Slice(out, func(i, j int) bool {
			if out[i].Present != out[j].Present {
				return out[i].Present
			}
			return strings.ToLower(out[i].Name) < strings.ToLower(out[j].Name)
		})
		return out, countPresent(out)
	}

	for _, raw := range strings.Split(string(b), "\n") {
		line := strings.TrimSpace(raw)
		if line == "" {
			continue
		}
		if c := parseJoinLeaveCount(line); c > 0 {
			lastLoggedCount = c
		}
		at, kind, name := parseJoinLeaveLine(line)
		if name == "" {
			continue
		}
		if !since.IsZero() && at.Before(since) {
			continue
		}
		u := users[name]
		if u == nil {
			u = &userState{Name: name}
			users[name] = u
		}
		switch kind {
		case "join":
			u.Present = true
			if at.After(u.LastJoin) {
				u.LastJoin = at
			}
		case "leave":
			u.Present = false
			if at.After(u.LastLeave) {
				u.LastLeave = at
			}
		}
	}

	// Keep any snapshot-marked present users that the join/leave delta can't rebuild.
	for _, snap := range readGuestSnapshot() {
		if !snap.Present {
			continue
		}
		u := users[snap.Name]
		if u == nil {
			users[snap.Name] = &userState{
				Name:     snap.Name,
				Present:  true,
				LastJoin: snap.LastJoin,
			}
			continue
		}
		if !u.Present {
			u.Present = true
			if u.LastJoin.IsZero() {
				u.LastJoin = snap.LastJoin
			}
		}
	}

	out := make([]userState, 0, len(users))
	for _, u := range users {
		out = append(out, *u)
	}
	sort.Slice(out, func(i, j int) bool {
		if out[i].Present != out[j].Present {
			return out[i].Present
		}
		return strings.ToLower(out[i].Name) < strings.ToLower(out[j].Name)
	})
	present := countPresent(out)
	if lastLoggedCount > present {
		present = lastLoggedCount
	}
	return out, present
}

func readGuestSnapshot() []app.GuestStatus {
	p := filepath.Join(runtimeDir(), "guest_snapshot.json")
	b, err := os.ReadFile(p)
	if err != nil {
		return nil
	}
	var out []app.GuestStatus
	if err := json.Unmarshal(b, &out); err != nil {
		return nil
	}
	return out
}

func formatGuestPane(title string, items []userState) string {
	var b strings.Builder
	b.WriteString(title)
	b.WriteString("\r\n")
	b.WriteString(strings.Repeat("=", len(title)))
	b.WriteString("\r\n")
	if len(items) == 0 {
		b.WriteString("Current in room: 0")
		b.WriteString("\r\n")
		b.WriteString("(none)")
		return b.String()
	}
	b.WriteString("Current in room: ")
	b.WriteString(strconv.Itoa(countPresent(items)))
	b.WriteString("\r\n\r\n")
	for _, item := range items {
		b.WriteString("・ ")
		b.WriteString(item.Name)
		if item.Present {
			b.WriteString(" [present]")
		} else {
			b.WriteString(" [gray]")
			if !item.LastLeave.IsZero() {
				b.WriteString(" ")
				b.WriteString(timeAgo(item.LastLeave))
			}
		}
		b.WriteString("\r\n")
	}
	return strings.TrimRight(b.String(), "\r\n")
}

func formatRightPane(tab string, state map[string]any, worldLabel string, currentUsers int) string {
	var b strings.Builder
	switch tab {
	case "discord":
		b.WriteString("Discord")
	case "translate":
		b.WriteString("Translate")
	default:
		b.WriteString("Current World")
	}
	b.WriteString("\r\n")
	titleLine := "Current World"
	switch tab {
	case "discord":
		titleLine = "Discord"
	case "translate":
		titleLine = "Translate"
	}
	b.WriteString(strings.Repeat("=", len(titleLine)))
	b.WriteString("\r\n")
	discordMuted, _ := state["discord_muted"].(bool)
	ocr, _ := state["ocr"].(string)
	translate, _ := state["translate"].(string)
	switch tab {
	case "discord":
		b.WriteString("Muted: ")
		b.WriteString(fmt.Sprintf("%v", discordMuted))
		b.WriteString("\r\nWorld: ")
		b.WriteString(worldLabel)
		b.WriteString("\r\nUsers: ")
		b.WriteString(fmt.Sprintf("%d", currentUsers))
	case "translate":
		b.WriteString("Translate: ")
		b.WriteString(translate)
		b.WriteString("\r\nOCR: ")
		b.WriteString(ocr)
		b.WriteString("\r\nWorld: ")
		b.WriteString(worldLabel)
	default:
		b.WriteString("World: ")
		b.WriteString(worldLabel)
		b.WriteString("\r\nUsers: ")
		b.WriteString(fmt.Sprintf("%d", currentUsers))
		b.WriteString("\r\nDiscord muted: ")
		b.WriteString(fmt.Sprintf("%v", discordMuted))
		b.WriteString("\r\nOCR: ")
		b.WriteString(ocr)
		b.WriteString("\r\nTranslate: ")
		b.WriteString(translate)
	}
	return b.String()
}

func currentWorldLabel() string {
	p := filepath.Join(runtimeDir(), "runtime.log")
	b, err := os.ReadFile(p)
	if err != nil {
		return ""
	}
	lines := strings.Split(string(b), "\n")
	var latest string
	for i := 0; i < len(lines); i++ {
		if !strings.Contains(lines[i], "VRC WORLD") {
			continue
		}
		if i+1 < len(lines) {
			latest = strings.TrimSpace(lines[i+1])
		}
	}
	return latest
}

func readRuntimeSnapshot() map[string]any {
	p := filepath.Join(runtimeDir(), "state.json")
	b, err := os.ReadFile(p)
	if err != nil {
		return map[string]any{}
	}
	var out map[string]any
	_ = json.Unmarshal(b, &out)
	return out
}

func currentWorldStartTime() time.Time {
	p := filepath.Join(runtimeDir(), "runtime.log")
	b, err := os.ReadFile(p)
	if err != nil {
		return time.Time{}
	}
	var latest time.Time
	for _, raw := range strings.Split(string(b), "\n") {
		line := strings.TrimSpace(raw)
		if line == "" || !strings.Contains(line, "VRC WORLD") {
			continue
		}
		if at, ok := parseRuntimeTime(line); ok && at.After(latest) {
			latest = at
		}
	}
	return latest
}

func parseJoinLeaveLine(line string) (time.Time, string, string) {
	m := joinLeaveLinePattern.FindStringSubmatch(line)
	if len(m) != 4 {
		return time.Time{}, "", ""
	}
	at, err := time.Parse("2006-01-02 15:04:05", m[1])
	if err != nil {
		return time.Time{}, "", ""
	}
	return at, m[2], strings.TrimSpace(m[3])
}

func parseJoinLeaveCount(line string) int {
	m := regexp.MustCompile(`\((\d+)\)$`).FindStringSubmatch(line)
	if len(m) != 2 {
		return 0
	}
	n, err := strconv.Atoi(m[1])
	if err != nil {
		return 0
	}
	return n
}

func parseRuntimeTime(line string) (time.Time, bool) {
	m := runtimeTimePattern.FindStringSubmatch(line)
	if len(m) != 2 {
		return time.Time{}, false
	}
	at, err := time.Parse("2006-01-02 15:04:05", m[1])
	if err != nil {
		return time.Time{}, false
	}
	return at, true
}

func countPresent(items []userState) int {
	n := 0
	for _, item := range items {
		if item.Present {
			n++
		}
	}
	return n
}

func timeAgo(at time.Time) string {
	if at.IsZero() {
		return ""
	}
	d := time.Since(at)
	if d < time.Minute {
		return "0分前"
	}
	if d < time.Hour {
		return fmt.Sprintf("%d分前", int(d.Minutes()))
	}
	if d < 24*time.Hour {
		return fmt.Sprintf("%d時間前", int(d.Hours()))
	}
	return fmt.Sprintf("%d日前", int(d.Hours()/24))
}
