//go:build windows
// +build windows

// Display color on Windows
//
// refer:
//
//	golang.org/x/sys/windows
//	golang.org/x/crypto/ssh/terminal
//	https://docs.microsoft.com/en-us/windows/console
package color

import (
	"os"
	"syscall"
	"unsafe"

	"github.com/xo/terminfo"
	"golang.org/x/sys/windows"
)

// related docs
// https://docs.microsoft.com/zh-cn/windows/console/console-virtual-terminal-sequences
// https://docs.microsoft.com/zh-cn/windows/console/console-virtual-terminal-sequences#samples
var (
	// isMSys bool
	kernel32 *syscall.LazyDLL

	procGetConsoleMode *syscall.LazyProc
	procSetConsoleMode *syscall.LazyProc
)

func init() {
	if !SupportColor() {
		isLikeInCmd = true
		return
	}

	// if disabled.
	if !Enable {
		return
	}

	// if at Windows's ConEmu, Cmder, putty ... terminals not need VTP

	// -------- try force enable colors on windows terminal -------
	tryEnableVTP(needVTP)

	// fetch console screen buffer info
	// err := getConsoleScreenBufferInfo(uintptr(syscall.Stdout), &defScreenInfo)
}

// try force enable colors on Windows terminal
func tryEnableVTP(enable bool) bool {
	if !enable {
		return false
	}

	debugf("True-Color by enable VirtualTerminalProcessing on windows")

	initKernel32Proc()

	// enable colors on Windows terminal
	if tryEnableOnCONOUT() {
		return true
	}

	return tryEnableOnStdout()
}

func initKernel32Proc() {
	if kernel32 != nil {
		return
	}

	// load related Windows dll
	// https://docs.microsoft.com/en-us/windows/console/setconsolemode
	kernel32 = syscall.NewLazyDLL("kernel32.dll")

	procGetConsoleMode = kernel32.NewProc("GetConsoleMode")
	procSetConsoleMode = kernel32.NewProc("SetConsoleMode")
}

func tryEnableOnCONOUT() bool {
	outHandle, err := syscall.Open("CONOUT$", syscall.O_RDWR, 0)
	if err != nil {
		saveInternalError(err)
		return false
	}

	err = EnableVirtualTerminalProcessing(outHandle, true)
	if err != nil {
		saveInternalError(err)
		return false
	}

	return true
}

func tryEnableOnStdout() bool {
	// try direct open syscall.Stdout
	err := EnableVirtualTerminalProcessing(syscall.Stdout, true)
	if err != nil {
		saveInternalError(err)
		return false
	}

	return true
}

// Get the Windows Version and Build Number
var (
	winVersion, _, buildNumber = windows.RtlGetNtVersionNumbers()
)

// refer
//
//	https://github.com/Delta456/box-cli-maker/blob/7b5a1ad8a016ce181e7d8b05e24b54ff60b4b38a/detect_windows.go#L30-L57
//	https://github.com/gookit/color/issues/25#issuecomment-738727917
//
// detects the color level supported on Windows: cmd, powerShell
func detectSpecialTermColor(termVal string) (tl Level, needVTP bool) {
	if os.Getenv("ConEmuANSI") == "ON" {
		debugf("support True Color by ConEmuANSI=ON")
		// ConEmuANSI is "ON" for generic ANSI support
		// but True Color option is enabled by default
		// I am just assuming that people wouldn't have disabled it
		// Even if it is not enabled then ConEmu will auto round off
		// accordingly
		return terminfo.ColorLevelMillions, false
	}

	// Before Windows 10 Build Number 10586, console never supported ANSI Colors
	if buildNumber < 10586 || winVersion < 10 {
		// Detect if using ANSICON on older systems
		if os.Getenv("ANSICON") != "" {
			conVersion := os.Getenv("ANSICON_VER")
			// 8-bit Colors were only supported after v1.81 release
			if conVersion >= "181" {
				return terminfo.ColorLevelHundreds, false
			}
			return terminfo.ColorLevelBasic, false
		}

		return terminfo.ColorLevelNone, false
	}

	// True Color is not available before build 14931 so fallback to 8-bit color.
	if buildNumber < 14931 {
		return terminfo.ColorLevelHundreds, true
	}

	// Windows 10 build 14931 is the first release that supports 16m/TrueColor
	debugf("support True Color on windows version is >= build 14931")
	return terminfo.ColorLevelMillions, true
}

/*************************************************************
 * render full color code on Windows(8,16,24bit color)
 *************************************************************/

// docs https://docs.microsoft.com/zh-cn/windows/console/getconsolemode#parameters
const (
	// equals to docs page's ENABLE_VIRTUAL_TERMINAL_PROCESSING 0x0004
	EnableVirtualTerminalProcessingMode uint32 = 0x4
)

// EnableVirtualTerminalProcessing Enable virtual terminal processing
//
// ref from github.com/konsorten/go-windows-terminal-sequences
// doc https://docs.microsoft.com/zh-cn/windows/console/console-virtual-terminal-sequences#samples
//
// Usage:
//
//	err := EnableVirtualTerminalProcessing(syscall.Stdout, true)
//	// support print color text
//	err = EnableVirtualTerminalProcessing(syscall.Stdout, false)
func EnableVirtualTerminalProcessing(stream syscall.Handle, enable bool) error {
	var mode uint32
	// Check if it is currently in the terminal
	// err := syscall.GetConsoleMode(syscall.Stdout, &mode)
	err := syscall.GetConsoleMode(stream, &mode)
	if err != nil {
		// fmt.Println("EnableVirtualTerminalProcessing", err)
		return err
	}

	if enable {
		mode |= EnableVirtualTerminalProcessingMode
	} else {
		mode &^= EnableVirtualTerminalProcessingMode
	}

	ret, _, err := procSetConsoleMode.Call(uintptr(stream), uintptr(mode))
	if ret == 0 {
		return err
	}

	return nil
}

// renderColorCodeOnCmd enable cmd color render.
// func renderColorCodeOnCmd(fn func()) {
// 	err := EnableVirtualTerminalProcessing(syscall.Stdout, true)
// 	// if is not in terminal, will clear color tag.
// 	if err != nil {
// 		// panic(err)
// 		fn()
// 		return
// 	}
//
// 	// force open color render
// 	old := ForceOpenColor()
// 	fn()
// 	// revert color setting
// 	supportColor = old
//
// 	err = EnableVirtualTerminalProcessing(syscall.Stdout, false)
// 	if err != nil {
// 		panic(err)
// 	}
// }

/*************************************************************
 * render simple color code on Windows
 *************************************************************/

// IsTty returns true if the given file descriptor is a terminal.
func IsTty(fd uintptr) bool {
	initKernel32Proc()

	var st uint32
	r, _, e := syscall.Syscall(procGetConsoleMode.Addr(), 2, fd, uintptr(unsafe.Pointer(&st)), 0)
	return r != 0 && e == 0
}

// IsTerminal returns true if the given file descriptor is a terminal.
//
// Usage:
//
//	fd := os.Stdout.Fd()
//	fd := uintptr(syscall.Stdout) // for Windows
//	IsTerminal(fd)
func IsTerminal(fd uintptr) bool {
	initKernel32Proc()

	var st uint32
	r, _, e := syscall.Syscall(procGetConsoleMode.Addr(), 2, fd, uintptr(unsafe.Pointer(&st)), 0)
	return r != 0 && e == 0
}
