// Copyright (c) 2022 Tailscale Inc & AUTHORS. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

//go:build windows

package wingoes

import (
	"fmt"
	"sync"

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

var (
	verOnce sync.Once
	verInfo osVersionInfo // must access via getVersionInfo()
)

// osVersionInfo is more compact than windows.OsVersionInfoEx, which contains
// extraneous information.
type osVersionInfo struct {
	major       uint32
	minor       uint32
	build       uint32
	servicePack uint16
	str         string
	isDC        bool
	isServer    bool
}

const (
	_VER_NT_WORKSTATION       = 1
	_VER_NT_DOMAIN_CONTROLLER = 2
	_VER_NT_SERVER            = 3
)

func getVersionInfo() *osVersionInfo {
	verOnce.Do(func() {
		osv := windows.RtlGetVersion()
		verInfo = osVersionInfo{
			major:       osv.MajorVersion,
			minor:       osv.MinorVersion,
			build:       osv.BuildNumber,
			servicePack: osv.ServicePackMajor,
			str:         fmt.Sprintf("%d.%d.%d", osv.MajorVersion, osv.MinorVersion, osv.BuildNumber),
			isDC:        osv.ProductType == _VER_NT_DOMAIN_CONTROLLER,
			// Domain Controllers are also implicitly servers.
			isServer: osv.ProductType == _VER_NT_DOMAIN_CONTROLLER || osv.ProductType == _VER_NT_SERVER,
		}
		// UBR is only available on Windows 10 and 11 (MajorVersion == 10).
		if osv.MajorVersion == 10 {
			if ubr, err := getUBR(); err == nil {
				verInfo.str = fmt.Sprintf("%s.%d", verInfo.str, ubr)
			}
		}
	})
	return &verInfo
}

// getUBR returns the "update build revision," ie. the fourth component of the
// version string found on Windows 10 and Windows 11 systems.
func getUBR() (uint32, error) {
	key, err := registry.OpenKey(registry.LOCAL_MACHINE,
		`SOFTWARE\Microsoft\Windows NT\CurrentVersion`, registry.QUERY_VALUE|registry.WOW64_64KEY)
	if err != nil {
		return 0, err
	}
	defer key.Close()

	val, valType, err := key.GetIntegerValue("UBR")
	if err != nil {
		return 0, err
	}
	if valType != registry.DWORD {
		return 0, registry.ErrUnexpectedType
	}

	return uint32(val), nil
}

// GetOSVersionString returns the Windows version of the current machine in
// dotted-decimal form. The version string contains 3 components on Windows 7
// and 8.x, and 4 components on Windows 10 and 11.
func GetOSVersionString() string {
	return getVersionInfo().String()
}

// IsWinServer returns true if and only if this computer's version of Windows is
// a server edition.
func IsWinServer() bool {
	return getVersionInfo().isServer
}

// IsWinDomainController returs true if this computer's version of Windows is
// configured to act as a domain controller.
func IsWinDomainController() bool {
	return getVersionInfo().isDC
}

// IsWin7SP1OrGreater returns true when running on Windows 7 SP1 or newer.
func IsWin7SP1OrGreater() bool {
	if IsWin8OrGreater() {
		return true
	}

	vi := getVersionInfo()
	return vi.major == 6 && vi.minor == 1 && vi.servicePack > 0
}

// IsWin8OrGreater returns true when running on Windows 8.0 or newer.
func IsWin8OrGreater() bool {
	return getVersionInfo().isVersionOrGreater(6, 2, 0)
}

// IsWin8Point1OrGreater returns true when running on Windows 8.1 or newer.
func IsWin8Point1OrGreater() bool {
	return getVersionInfo().isVersionOrGreater(6, 3, 0)
}

// IsWin10OrGreater returns true when running on any build of Windows 10 or newer.
func IsWin10OrGreater() bool {
	return getVersionInfo().major >= 10
}

// Win10BuildConstant encodes build numbers for the various editions of Windows 10,
// for use with IsWin10BuildOrGreater.
type Win10BuildConstant uint32

const (
	Win10BuildRTM          = Win10BuildConstant(10240)
	Win10Build1511         = Win10BuildConstant(10586)
	Win10Build1607         = Win10BuildConstant(14393)
	Win10BuildAnniversary  = Win10Build1607
	Win10Build1703         = Win10BuildConstant(15063)
	Win10BuildCreators     = Win10Build1703
	Win10Build1709         = Win10BuildConstant(16299)
	Win10BuildFallCreators = Win10Build1709
	Win10Build1803         = Win10BuildConstant(17134)
	Win10Build1809         = Win10BuildConstant(17763)
	Win10Build1903         = Win10BuildConstant(18362)
	Win10Build1909         = Win10BuildConstant(18363)
	Win10Build2004         = Win10BuildConstant(19041)
	Win10Build20H2         = Win10BuildConstant(19042)
	Win10Build21H1         = Win10BuildConstant(19043)
	Win10Build21H2         = Win10BuildConstant(19044)
	Win10Build22H2         = Win10BuildConstant(19045)
)

// IsWin10BuildOrGreater returns true when running on the specified Windows 10
// build, or newer.
func IsWin10BuildOrGreater(build Win10BuildConstant) bool {
	return getVersionInfo().isWin10BuildOrGreater(uint32(build))
}

// Win11BuildConstant encodes build numbers for the various editions of Windows 11,
// for use with IsWin11BuildOrGreater.
type Win11BuildConstant uint32

const (
	Win11BuildRTM  = Win11BuildConstant(22000)
	Win11Build22H2 = Win11BuildConstant(22621)
	Win11Build23H2 = Win11BuildConstant(22631)
)

// IsWin11OrGreater returns true when running on any release of Windows 11,
// or newer.
func IsWin11OrGreater() bool {
	return IsWin11BuildOrGreater(Win11BuildRTM)
}

// IsWin11BuildOrGreater returns true when running on the specified Windows 11
// build, or newer.
func IsWin11BuildOrGreater(build Win11BuildConstant) bool {
	// Under the hood, Windows 11 is just Windows 10 with a sufficiently advanced
	// build number.
	return getVersionInfo().isWin10BuildOrGreater(uint32(build))
}

func (osv *osVersionInfo) String() string {
	return osv.str
}

func (osv *osVersionInfo) isWin10BuildOrGreater(build uint32) bool {
	return osv.isVersionOrGreater(10, 0, build)
}

func (osv *osVersionInfo) isVersionOrGreater(major, minor, build uint32) bool {
	return isVerGE(osv.major, major, osv.minor, minor, osv.build, build)
}

func isVerGE(lmajor, rmajor, lminor, rminor, lbuild, rbuild uint32) bool {
	return lmajor > rmajor ||
		lmajor == rmajor &&
			(lminor > rminor ||
				lminor == rminor && lbuild >= rbuild)
}
