//go:build windows

package main

import (
	"crypto/sha256"
	"encoding/hex"
	"fmt"
	"os"
	"path/filepath"
	"runtime"
	"strings"
	"sync"
	"syscall"
	"unsafe"
)

type point struct{ X, Y int32 }
type msg struct {
	Hwnd           uintptr
	Message        uint32
	WParam, LParam uintptr
	Time           uint32
	Pt             point
	LPrivate       uint32
}
type wndClassEx struct {
	CbSize                                   uint32
	Style                                    uint32
	LpfnWndProc                              uintptr
	CbClsExtra, CbWndExtra                   int32
	HInstance, HIcon, HCursor, HbrBackground uintptr
	LpszMenuName, LpszClassName              *uint16
	HIconSm                                  uintptr
}
type guid struct {
	Data1        uint32
	Data2, Data3 uint16
	Data4        [8]byte
}

type comInterface struct{ lpVtbl *[64]uintptr }

type installJob struct {
	selected, dataRoot string
	first              bool
	plan               *InstallPlan
	proceed            chan bool
	targetMutex        uintptr
}

const (
	WS_OVERLAPPEDWINDOW      = 0x00CF0000
	WS_VISIBLE               = 0x10000000
	WS_CHILD                 = 0x40000000
	WS_TABSTOP               = 0x00010000
	WS_BORDER                = 0x00800000
	ES_AUTOHSCROLL           = 0x0080
	BS_PUSHBUTTON            = 0
	BS_AUTOCHECKBOX          = 3
	CBS_DROPDOWNLIST         = 3
	WS_VSCROLL               = 0x00200000
	SW_SHOW                  = 5
	CW_USEDEFAULT            = 0x80000000
	WM_DESTROY               = 0x0002
	WM_COMMAND               = 0x0111
	WM_SETFONT               = 0x0030
	WM_CLOSE                 = 0x0010
	BM_GETCHECK              = 0x00F0
	BM_SETCHECK              = 0x00F1
	BST_CHECKED              = 1
	CB_ADDSTRING             = 0x0143
	CB_SETCURSEL             = 0x014E
	CB_GETCURSEL             = 0x0147
	CB_RESETCONTENT          = 0x014B
	COLOR_BTNFACE            = 15
	DEFAULT_GUI_FONT         = 17
	IDC_ARROW                = 32512
	MB_OK                    = 0
	MB_ICONERROR             = 0x10
	MB_ICONINFORMATION       = 0x40
	MB_ICONWARNING           = 0x30
	MB_YESNO                 = 0x04
	IDYES                    = 6
	WM_APP                   = 0x8000
	WM_APP_INSPECT           = WM_APP + 1
	WM_APP_BACKUPS           = WM_APP + 2
	WM_APP_INSTALL_READY     = WM_APP + 3
	WM_APP_INSTALL_DONE      = WM_APP + 4
	WM_APP_RESTORE_DONE      = WM_APP + 5
	WAIT_OBJECT_0            = 0
	ERROR_ALREADY_EXISTS     = 183
	FOS_PICKFOLDERS          = 0x20
	FOS_FORCEFILESYSTEM      = 0x40
	FOS_PATHMUSTEXIST        = 0x800
	SIGDN_FILESYSPATH        = 0x80058000
	COINIT_APARTMENTTHREADED = 0x2
	CLSCTX_INPROC_SERVER     = 0x1
	ID_TARGET                = 100
	ID_BROWSE                = 101
	ID_FIRST                 = 102
	ID_INSPECT               = 103
	ID_INSTALL               = 104
	ID_COLOR                 = 105
	ID_THICK                 = 106
	ID_OPACITY               = 107
	ID_STYLE                 = 112
	ID_STATUS                = 108
	ID_BACKUPS               = 109
	ID_REFRESH               = 110
	ID_RESTORE               = 111
)

var (
	user32               = syscall.NewLazyDLL("user32.dll")
	kernel32             = syscall.NewLazyDLL("kernel32.dll")
	gdi32                = syscall.NewLazyDLL("gdi32.dll")
	ole32                = syscall.NewLazyDLL("ole32.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")
	pSendMessage         = user32.NewProc("SendMessageW")
	pPostMessage         = user32.NewProc("PostMessageW")
	pSetWindowText       = user32.NewProc("SetWindowTextW")
	pGetWindowText       = user32.NewProc("GetWindowTextW")
	pGetWindowTextLength = user32.NewProc("GetWindowTextLengthW")
	pEnableWindow        = user32.NewProc("EnableWindow")
	pMessageBox          = user32.NewProc("MessageBoxW")
	pLoadCursor          = user32.NewProc("LoadCursorW")
	pGetModuleHandle     = kernel32.NewProc("GetModuleHandleW")
	pGetStockObject      = gdi32.NewProc("GetStockObject")
	pCreateMutex         = kernel32.NewProc("CreateMutexW")
	pWaitForSingleObject = kernel32.NewProc("WaitForSingleObject")
	pReleaseMutex        = kernel32.NewProc("ReleaseMutex")
	pCloseHandle         = kernel32.NewProc("CloseHandle")
	pCoInitializeEx      = ole32.NewProc("CoInitializeEx")
	pCoUninitialize      = ole32.NewProc("CoUninitialize")
	pCoCreateInstance    = ole32.NewProc("CoCreateInstance")
	pCoTaskMemFree       = ole32.NewProc("CoTaskMemFree")
)

var hwndMain uintptr
var ctrls = map[int]uintptr{}
var stateMu sync.Mutex
var sourceSession *SourceSession
var sourceOK bool
var lastStatus string
var backupList []BackupInfo
var pendingJob *installJob
var installResult struct {
	backup string
	err    error
	log    string
}
var restoreResult struct {
	before string
	err    error
}
var exeDir string

func w(s string) *uint16   { p, _ := syscall.UTF16PtrFromString(s); return p }
func loword(v uintptr) int { return int(v & 0xffff) }
func callVtbl(obj *comInterface, index int, args ...uintptr) (uintptr, uintptr, syscall.Errno) {
	if obj == nil || obj.lpVtbl == nil {
		return 0, 0, syscall.EINVAL
	}
	fn := obj.lpVtbl[index]
	a := append([]uintptr{uintptr(unsafe.Pointer(obj))}, args...)
	return syscall.SyscallN(fn, a...)
}
func releaseCom(obj *comInterface) {
	if obj != nil {
		callVtbl(obj, 2)
	}
}
func succeeded(hr uintptr) bool { return int32(hr) >= 0 }

func folderDialog(owner uintptr) (string, error) {
	clsid := guid{0xDC1C5A9C, 0xE88A, 0x4DDE, [8]byte{0xA5, 0xA1, 0x60, 0xF8, 0x2A, 0x20, 0xAE, 0xF7}}
	iid := guid{0xD57C7288, 0xD4AD, 0x4768, [8]byte{0xBE, 0x02, 0x9D, 0x96, 0x95, 0x32, 0xD9, 0x60}}
	var dlg *comInterface
	hr, _, _ := pCoCreateInstance.Call(uintptr(unsafe.Pointer(&clsid)), 0, CLSCTX_INPROC_SERVER, uintptr(unsafe.Pointer(&iid)), uintptr(unsafe.Pointer(&dlg)))
	if !succeeded(hr) {
		return "", fmt.Errorf("IFileOpenDialog create failed: 0x%x", uint32(hr))
	}
	defer releaseCom(dlg)
	callVtbl(dlg, 9, FOS_PICKFOLDERS|FOS_FORCEFILESYSTEM|FOS_PATHMUSTEXIST)
	hr, _, _ = callVtbl(dlg, 3, owner)
	if !succeeded(hr) {
		return "", fmt.Errorf("folder selection canceled")
	}
	var item *comInterface
	hr, _, _ = callVtbl(dlg, 20, uintptr(unsafe.Pointer(&item)))
	if !succeeded(hr) || item == nil {
		return "", fmt.Errorf("folder result failed")
	}
	defer releaseCom(item)
	var psz *uint16
	hr, _, _ = callVtbl(item, 5, SIGDN_FILESYSPATH, uintptr(unsafe.Pointer(&psz)))
	if !succeeded(hr) || psz == nil {
		return "", fmt.Errorf("folder path failed")
	}
	defer pCoTaskMemFree.Call(uintptr(unsafe.Pointer(psz)))
	return syscall.UTF16ToString(unsafe.Slice(psz, 32768)), nil
}

func makeCtrl(class, text string, style uint32, x, y, wid, hei int, id int) uintptr {
	h, _, _ := pCreateWindowEx.Call(0, uintptr(unsafe.Pointer(w(class))), uintptr(unsafe.Pointer(w(text))), uintptr(style|WS_CHILD|WS_VISIBLE), uintptr(x), uintptr(y), uintptr(wid), uintptr(hei), hwndMain, uintptr(id), 0, 0)
	font, _, _ := pGetStockObject.Call(DEFAULT_GUI_FONT)
	pSendMessage.Call(h, WM_SETFONT, font, 1)
	ctrls[id] = h
	return h
}
func setText(id int, s string) { pSetWindowText.Call(ctrls[id], uintptr(unsafe.Pointer(w(s)))) }
func getText(id int) string {
	n, _, _ := pGetWindowTextLength.Call(ctrls[id])
	buf := make([]uint16, n+1)
	pGetWindowText.Call(ctrls[id], uintptr(unsafe.Pointer(&buf[0])), n+1)
	return syscall.UTF16ToString(buf)
}
func enabled(id int, on bool) {
	v := uintptr(0)
	if on {
		v = 1
	}
	pEnableWindow.Call(ctrls[id], v)
}
func checked(id int) bool {
	r, _, _ := pSendMessage.Call(ctrls[id], BM_GETCHECK, 0, 0)
	return r == BST_CHECKED
}
func comboSel(id int) int { r, _, _ := pSendMessage.Call(ctrls[id], CB_GETCURSEL, 0, 0); return int(r) }
func comboAdd(id int, s string) {
	pSendMessage.Call(ctrls[id], CB_ADDSTRING, 0, uintptr(unsafe.Pointer(w(s))))
}
func comboSet(id, idx int) { pSendMessage.Call(ctrls[id], CB_SETCURSEL, uintptr(idx), 0) }
func msgbox(text, title string, flags uintptr) int {
	r, _, _ := pMessageBox.Call(hwndMain, uintptr(unsafe.Pointer(w(text))), uintptr(unsafe.Pointer(w(title))), flags)
	return int(r)
}

func setBusy(on bool) {
	for _, id := range []int{ID_TARGET, ID_BROWSE, ID_FIRST, ID_INSPECT, ID_STYLE, ID_COLOR, ID_THICK, ID_OPACITY, ID_REFRESH, ID_RESTORE} {
		enabled(id, !on)
	}
	stateMu.Lock()
	ok := sourceOK
	stateMu.Unlock()
	enabled(ID_INSTALL, !on && ok)
}
func post(m uint32) { pPostMessage.Call(hwndMain, uintptr(m), 0, 0) }

func inspectSource() {
	setBusy(true)
	setText(ID_STATUS, "설치 원본 검사 중...")
	go func() {
		s, e := ValidateSource(exeDir)
		stateMu.Lock()
		if e == nil {
			sourceSession = s
			sourceOK = true
			lastStatus = fmt.Sprintf("검사 성공: 계약 source %d개 확인", len(s.Fingerprints))
		} else {
			sourceSession = nil
			sourceOK = false
			lastStatus = "검사 실패: " + e.Error()
		}
		stateMu.Unlock()
		post(WM_APP_INSPECT)
	}()
}
func refreshBackups() {
	enabled(ID_REFRESH, false)
	setText(ID_STATUS, "백업 목록 검사 중...")
	go func() {
		b, e := ListBackups(exeDir)
		stateMu.Lock()
		backupList = b
		if e != nil {
			lastStatus = "백업 검사 실패: " + e.Error()
		} else {
			lastStatus = fmt.Sprintf("백업 %d개 확인", len(b))
		}
		stateMu.Unlock()
		post(WM_APP_BACKUPS)
	}()
}

func selectedOptions() BeaconOptions {
	st := comboSel(ID_STYLE)
	c := comboSel(ID_COLOR)
	s := comboSel(ID_THICK)
	o := comboSel(ID_OPACITY)
	styles := []string{"outer", "rings"}
	ops := []string{"o25", "o50", "o75", "o100"}
	if st < 0 || st >= len(styles) {
		st = 0
	}
	if c < 0 {
		c = 17
	}
	if s < 0 {
		s = 0
	}
	if o < 0 {
		o = 2
	}
	return BeaconOptions{styles[st], fmt.Sprintf("c%02d", c), fmt.Sprintf("s%02d", s), ops[o]}
}

func targetMutexName(dataRoot string) string {
	a, _ := filepath.Abs(dataRoot)
	h := sha256.Sum256([]byte(strings.ToLower(filepath.Clean(a))))
	return "Local\\D2R_Data_Target_" + hex.EncodeToString(h[:16])
}
func acquireMutex(name string) (uintptr, error) {
	h, _, e := pCreateMutex.Call(0, 0, uintptr(unsafe.Pointer(w(name))))
	if h == 0 {
		return 0, e
	}
	r, _, _ := pWaitForSingleObject.Call(h, 0)
	if r != WAIT_OBJECT_0 {
		pCloseHandle.Call(h)
		return 0, fmt.Errorf("another installer is using the same target")
	}
	return h, nil
}
func releaseMutex(h uintptr) {
	if h != 0 {
		pReleaseMutex.Call(h)
		pCloseHandle.Call(h)
	}
}

func beginInstall() {
	selected := getText(ID_TARGET)
	first := checked(ID_FIRST)
	opts := selectedOptions()
	stateMu.Lock()
	sess := sourceSession
	ok := sourceOK
	stateMu.Unlock()
	if !ok || sess == nil {
		msgbox("먼저 [검사]를 완료하세요.", "설치", MB_OK|MB_ICONWARNING)
		return
	}
	setBusy(true)
	setText(ID_STATUS, "대상 사전 검사 및 병합 계획 생성 중...")
	go func() {
		dr, _, e := ResolveTarget(selected, first)
		if e != nil {
			stateMu.Lock()
			installResult.err = e
			installResult.log = ""
			stateMu.Unlock()
			post(WM_APP_INSTALL_DONE)
			return
		}
		mh, e := acquireMutex(targetMutexName(dr))
		if e != nil {
			stateMu.Lock()
			installResult.err = e
			stateMu.Unlock()
			post(WM_APP_INSTALL_DONE)
			return
		}
		plan, e := BuildInstallPlan(sess, dr, opts)
		if e != nil {
			releaseMutex(mh)
			stateMu.Lock()
			installResult.err = e
			if strings.Contains(e.Error(), "source changed after inspection") {
				sourceOK = false
				sourceSession = nil
			}
			stateMu.Unlock()
			post(WM_APP_INSTALL_DONE)
			return
		}
		job := &installJob{selected: selected, dataRoot: dr, first: first, plan: plan, proceed: make(chan bool), targetMutex: mh}
		stateMu.Lock()
		pendingJob = job
		stateMu.Unlock()
		post(WM_APP_INSTALL_READY)
		if !<-job.proceed {
			releaseMutex(mh)
			stateMu.Lock()
			pendingJob = nil
			installResult.err = nil
			installResult.backup = ""
			stateMu.Unlock()
			post(WM_APP_INSTALL_DONE)
			return
		}
		backup, e := ExecuteInstall(exeDir, plan)
		log := ""
		if e != nil {
			log = WriteFailureLog(exeDir, "install", sess.Root, selected, dr, first, e, backup, "see error")
		}
		releaseMutex(mh)
		stateMu.Lock()
		pendingJob = nil
		installResult.backup = backup
		installResult.err = e
		installResult.log = log
		stateMu.Unlock()
		post(WM_APP_INSTALL_DONE)
	}()
}

func beginRestore() {
	idx := comboSel(ID_BACKUPS)
	stateMu.Lock()
	if idx < 0 || idx >= len(backupList) {
		stateMu.Unlock()
		msgbox("복구할 백업을 선택하세요.", "백업 복구", MB_OK|MB_ICONWARNING)
		return
	}
	bi := backupList[idx]
	stateMu.Unlock()
	if msgbox("선택한 백업 상태로 되돌립니다.\n\n이 백업 이후 같은 파일에 적용한 다른 모드의 수정도 함께 되돌아갑니다.\n계속할까요?", "백업 복구", MB_YESNO|MB_ICONWARNING) != IDYES {
		return
	}
	selected := getText(ID_TARGET)
	first := checked(ID_FIRST)
	setBusy(true)
	setText(ID_STATUS, "백업 복구 준비 중...")
	go func() {
		dr, _, e := ResolveTarget(selected, first)
		if e == nil {
			var mh uintptr
			mh, e = acquireMutex(targetMutexName(dr))
			if e == nil {
				var before string
				before, e = RestoreBackup(exeDir, dr, bi.Folder)
				releaseMutex(mh)
				stateMu.Lock()
				restoreResult.before = before
				restoreResult.err = e
				stateMu.Unlock()
				post(WM_APP_RESTORE_DONE)
				return
			}
		}
		stateMu.Lock()
		restoreResult.err = e
		stateMu.Unlock()
		post(WM_APP_RESTORE_DONE)
	}()
}

func wndProc(hwnd uintptr, m uint32, wp, lp uintptr) uintptr {
	switch m {
	case WM_COMMAND:
		switch loword(wp) {
		case ID_BROWSE:
			if p, e := folderDialog(hwnd); e == nil {
				setText(ID_TARGET, p)
			}
		case ID_INSPECT:
			inspectSource()
		case ID_INSTALL:
			beginInstall()
		case ID_REFRESH:
			refreshBackups()
		case ID_RESTORE:
			beginRestore()
		}
		return 0
	case WM_APP_INSPECT:
		stateMu.Lock()
		s := lastStatus
		ok := sourceOK
		stateMu.Unlock()
		setText(ID_STATUS, s)
		setBusy(false)
		enabled(ID_INSTALL, ok)
		if ok {
			msgbox("설치 원본 검사가 완료되었습니다.", "검사", MB_OK|MB_ICONINFORMATION)
		}
		return 0
	case WM_APP_BACKUPS:
		stateMu.Lock()
		b := append([]BackupInfo(nil), backupList...)
		s := lastStatus
		stateMu.Unlock()
		pSendMessage.Call(ctrls[ID_BACKUPS], CB_RESETCONTENT, 0, 0)
		for _, x := range b {
			comboAdd(ID_BACKUPS, x.Label)
		}
		if len(b) > 0 {
			comboSet(ID_BACKUPS, 0)
		}
		setText(ID_STATUS, s)
		enabled(ID_REFRESH, true)
		return 0
	case WM_APP_INSTALL_READY:
		stateMu.Lock()
		job := pendingJob
		stateMu.Unlock()
		if job == nil {
			return 0
		}
		text := fmt.Sprintf("실제 수정 대상:\n%s\n\n비콘: %s / %s / %s\n\n현재 target을 기준으로 계약 부분만 스마트 병합합니다.\n쓰기 전에 전체 영향 파일을 백업합니다. 백업은 자동 삭제하지 않습니다.\n설치할까요?", job.dataRoot, job.plan.Options.Color, job.plan.Options.Thickness, job.plan.Options.Opacity)
		yes := msgbox(text, "설치 확인", MB_YESNO|MB_ICONWARNING) == IDYES
		job.proceed <- yes
		return 0
	case WM_APP_INSTALL_DONE:
		stateMu.Lock()
		r := installResult
		installResult = struct {
			backup string
			err    error
			log    string
		}{}
		ok := sourceOK
		stateMu.Unlock()
		setBusy(false)
		enabled(ID_INSTALL, ok)
		if r.err != nil {
			txt := "설치 실패: " + r.err.Error()
			if r.log != "" {
				txt += "\n\n실패 로그: " + r.log
			}
			setText(ID_STATUS, txt)
			msgbox(txt, "설치 실패", MB_OK|MB_ICONERROR)
		} else if r.backup != "" {
			txt := "설치 완료\n백업: " + r.backup
			setText(ID_STATUS, txt)
			msgbox(txt, "설치 완료", MB_OK|MB_ICONINFORMATION)
			refreshBackups()
		} else {
			setText(ID_STATUS, "설치가 취소되었습니다.")
		}
		return 0
	case WM_APP_RESTORE_DONE:
		stateMu.Lock()
		r := restoreResult
		restoreResult = struct {
			before string
			err    error
		}{}
		stateMu.Unlock()
		setBusy(false)
		if r.err != nil {
			setText(ID_STATUS, "복구 실패: "+r.err.Error())
			msgbox("복구 실패: "+r.err.Error(), "백업 복구", MB_OK|MB_ICONERROR)
		} else {
			txt := "복구 완료\n복구 직전 백업: " + r.before
			setText(ID_STATUS, txt)
			msgbox(txt, "백업 복구", MB_OK|MB_ICONINFORMATION)
			refreshBackups()
		}
		return 0
	case WM_DESTROY:
		pPostQuitMessage.Call(0)
		return 0
	}
	r, _, _ := pDefWindowProc.Call(hwnd, uintptr(m), wp, lp)
	return r
}

func createUI() error {
	hinst, _, _ := pGetModuleHandle.Call(0)
	cur, _, _ := pLoadCursor.Call(0, IDC_ARROW)
	class := w("JimSSngFakeStashInstallerWindow")
	wc := wndClassEx{CbSize: uint32(unsafe.Sizeof(wndClassEx{})), LpfnWndProc: syscall.NewCallback(wndProc), HInstance: hinst, HCursor: cur, HbrBackground: uintptr(COLOR_BTNFACE + 1), LpszClassName: class}
	if r, _, e := pRegisterClassEx.Call(uintptr(unsafe.Pointer(&wc))); r == 0 {
		return e
	}
	title := fmt.Sprintf("[%s] 자동 인스톨러 %s", ModName, AppVersion)
	h, _, e := pCreateWindowEx.Call(0, uintptr(unsafe.Pointer(class)), uintptr(unsafe.Pointer(w(title))), WS_OVERLAPPEDWINDOW, 100, 100, 720, 610, 0, 0, hinst, 0)
	if h == 0 {
		return e
	}
	hwndMain = h
	makeCtrl("STATIC", "설치 원본: EXE 옆 data / variants 자동 인식", 0, 20, 18, 650, 20, 200)
	makeCtrl("STATIC", "대상 폴더", 0, 20, 52, 80, 20, 201)
	makeCtrl("EDIT", "", WS_BORDER|ES_AUTOHSCROLL|WS_TABSTOP, 105, 48, 465, 25, ID_TARGET)
	makeCtrl("BUTTON", "찾기", BS_PUSHBUTTON|WS_TABSTOP, 580, 47, 90, 27, ID_BROWSE)
	makeCtrl("BUTTON", "모드폴더 없음", BS_AUTOCHECKBOX|WS_TABSTOP, 105, 80, 150, 24, ID_FIRST)
	makeCtrl("STATIC", "표시 유형", 0, 20, 122, 80, 20, 209)
	makeCtrl("COMBOBOX", "", CBS_DROPDOWNLIST|WS_TABSTOP, 105, 118, 190, 120, ID_STYLE)
	comboAdd(ID_STYLE, "최외곽 원 하나만")
	comboAdd(ID_STYLE, "여러 개의 동심원")
	comboSet(ID_STYLE, 0)
	makeCtrl("STATIC", "비콘 색상", 0, 20, 164, 80, 20, 202)
	makeCtrl("COMBOBOX", "", CBS_DROPDOWNLIST|WS_TABSTOP|WS_VSCROLL, 105, 160, 190, 240, ID_COLOR)
	makeCtrl("STATIC", "선두께", 0, 315, 164, 60, 20, 203)
	makeCtrl("COMBOBOX", "", CBS_DROPDOWNLIST|WS_TABSTOP|WS_VSCROLL, 375, 160, 120, 220, ID_THICK)
	makeCtrl("STATIC", "불투명도", 0, 510, 164, 70, 20, 204)
	makeCtrl("COMBOBOX", "", CBS_DROPDOWNLIST|WS_TABSTOP, 580, 160, 90, 150, ID_OPACITY)
	colors := []string{"검정", "회색", "와인", "빨강", "주황", "노랑", "초록", "청록", "남색", "보라", "흰색", "연회색", "갈색", "분홍", "황색", "크림", "연두", "연하늘", "회청", "라벤더"}
	for i, n := range colors {
		comboAdd(ID_COLOR, fmt.Sprintf("c%02d - %s", i, n))
	}
	comboSet(ID_COLOR, 17)
	for i := 0; i < 8; i++ {
		comboAdd(ID_THICK, fmt.Sprintf("s%02d - %d/8", i, i+1))
	}
	comboSet(ID_THICK, 0)
	for _, x := range []string{"o25 - 25%", "o50 - 50%", "o75 - 75%", "o100 - 100%"} {
		comboAdd(ID_OPACITY, x)
	}
	comboSet(ID_OPACITY, 2)
	makeCtrl("BUTTON", "검사", BS_PUSHBUTTON|WS_TABSTOP, 105, 212, 110, 30, ID_INSPECT)
	makeCtrl("BUTTON", "설치", BS_PUSHBUTTON|WS_TABSTOP, 225, 212, 110, 30, ID_INSTALL)
	enabled(ID_INSTALL, false)
	makeCtrl("STATIC", "※ [검사]는 설치 원본만 확인하고 대상은 [설치] 시 검사합니다.", 0, 105, 250, 520, 20, 205)
	makeCtrl("STATIC", "검사 결과", 0, 20, 284, 80, 20, 206)
	makeCtrl("STATIC", "검사 전", WS_BORDER, 105, 280, 565, 72, ID_STATUS)
	makeCtrl("STATIC", "백업 목록", 0, 20, 382, 80, 20, 207)
	makeCtrl("COMBOBOX", "", CBS_DROPDOWNLIST|WS_TABSTOP|WS_VSCROLL, 105, 378, 565, 160, ID_BACKUPS)
	makeCtrl("BUTTON", "백업 새로고침", BS_PUSHBUTTON|WS_TABSTOP, 105, 420, 130, 30, ID_REFRESH)
	makeCtrl("BUTTON", "선택 백업 복구", BS_PUSHBUTTON|WS_TABSTOP, 245, 420, 140, 30, ID_RESTORE)
	makeCtrl("STATIC", "복구 시 현재 상태를 BeforeRestore로 다시 백업합니다.", 0, 105, 462, 500, 20, 208)
	pShowWindow.Call(hwndMain, SW_SHOW)
	pUpdateWindow.Call(hwndMain)
	return nil
}

func main() {
	runtime.LockOSThread()
	defer runtime.UnlockOSThread()
	ex, err := os.Executable()
	if err != nil {
		return
	}
	exeDir = filepath.Dir(ex)
	pCoInitializeEx.Call(0, COINIT_APARTMENTTHREADED)
	defer pCoUninitialize.Call()
	im, err := acquireMutex("Local\\JimSSng_FakeStash_Installer_v1")
	if err != nil {
		pMessageBox.Call(0, uintptr(unsafe.Pointer(w("이미 같은 설치기가 실행 중입니다."))), uintptr(unsafe.Pointer(w(AppName))), MB_OK|MB_ICONWARNING)
		return
	}
	defer releaseMutex(im)
	if err = createUI(); err != nil {
		return
	}
	refreshBackups()
	var m msg
	for {
		r, _, _ := pGetMessage.Call(uintptr(unsafe.Pointer(&m)), 0, 0, 0)
		if int32(r) <= 0 {
			break
		}
		pTranslateMessage.Call(uintptr(unsafe.Pointer(&m)))
		pDispatchMessage.Call(uintptr(unsafe.Pointer(&m)))
	}
}
