// 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"

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

// Error represents various error codes that may be encountered when coding
// against Windows APIs, including HRESULTs, windows.NTStatus, and windows.Errno.
type Error HRESULT

// Errors are HRESULTs under the hood because the HRESULT encoding allows for
// all the other common types of Windows errors to be encoded within them.

const (
	hrS_OK                 = HRESULT(0)
	hrE_ABORT              = HRESULT(-((0x80004004 ^ 0xFFFFFFFF) + 1))
	hrE_FAIL               = HRESULT(-((0x80004005 ^ 0xFFFFFFFF) + 1))
	hrE_NOINTERFACE        = HRESULT(-((0x80004002 ^ 0xFFFFFFFF) + 1))
	hrE_NOTIMPL            = HRESULT(-((0x80004001 ^ 0xFFFFFFFF) + 1))
	hrE_POINTER            = HRESULT(-((0x80004003 ^ 0xFFFFFFFF) + 1))
	hrE_UNEXPECTED         = HRESULT(-((0x8000FFFF ^ 0xFFFFFFFF) + 1))
	hrTYPE_E_WRONGTYPEKIND = HRESULT(-((0x8002802A ^ 0xFFFFFFFF) + 1))
)

// S_FALSE is a peculiar HRESULT value which means that the call executed
// successfully, but returned false as its result.
const S_FALSE = HRESULT(1)

var (
	// genericError encodes an Error whose message string is very generic.
	genericError = Error(hresultFromFacilityAndCode(hrFail, facilityWin32, hrCode(windows.ERROR_UNIDENTIFIED_ERROR)))
)

// Common HRESULT codes that don't use Win32 facilities, but have meanings that
// we can manually translate to Win32 error codes.
var commonHRESULTToErrno = map[HRESULT]windows.Errno{
	hrE_ABORT:       windows.ERROR_REQUEST_ABORTED,
	hrE_FAIL:        windows.ERROR_UNIDENTIFIED_ERROR,
	hrE_NOINTERFACE: windows.ERROR_NOINTERFACE,
	hrE_NOTIMPL:     windows.ERROR_CALL_NOT_IMPLEMENTED,
	hrE_UNEXPECTED:  windows.ERROR_INTERNAL_ERROR,
}

type hrCode uint16
type hrFacility uint16
type failBit bool

const (
	hrFlagBitsMask  = 0xF8000000
	hrFacilityMax   = 0x00001FFF
	hrFacilityMask  = hrFacilityMax << 16
	hrCodeMax       = 0x0000FFFF
	hrCodeMask      = hrCodeMax
	hrFailBit       = 0x80000000
	hrCustomerBit   = 0x20000000 // Also defined as syscall.APPLICATION_ERROR
	hrFacilityNTBit = 0x10000000
)

const (
	facilityWin32 = hrFacility(7)
)

// Succeeded returns true when hr is successful, but its actual error code
// may include additional status information.
func (hr HRESULT) Succeeded() bool {
	return hr >= 0
}

// Failed returns true when hr contains a failure code.
func (hr HRESULT) Failed() bool {
	return hr < 0
}

func (hr HRESULT) String() string {
	return fmt.Sprintf("0x%08X", uint32(hr))
}

func (hr HRESULT) isNT() bool {
	return (hr & (hrCustomerBit | hrFacilityNTBit)) == hrFacilityNTBit
}

func (hr HRESULT) isCustomer() bool {
	return (hr & hrCustomerBit) != 0
}

// isNormal returns true when the customer and NT bits are cleared, ie hr's
// encoding contains valid facility and code fields.
func (hr HRESULT) isNormal() bool {
	return (hr & (hrCustomerBit | hrFacilityNTBit)) == 0
}

// facility returns the facility bits of hr. Only valid when isNormal is true.
func (hr HRESULT) facility() hrFacility {
	return hrFacility((uint32(hr) >> 16) & hrFacilityMax)
}

// facility returns the code bits of hr. Only valid when isNormal is true.
func (hr HRESULT) code() hrCode {
	return hrCode(uint32(hr) & hrCodeMask)
}

const (
	hrFail    = failBit(true)
	hrSuccess = failBit(false)
)

func hresultFromFacilityAndCode(isFail failBit, f hrFacility, c hrCode) HRESULT {
	var r uint32
	if isFail {
		r |= hrFailBit
	}
	r |= (uint32(f) << 16) & hrFacilityMask
	r |= uint32(c) & hrCodeMask
	return HRESULT(r)
}

// ErrorFromErrno creates an Error from e.
func ErrorFromErrno(e windows.Errno) Error {
	if e == windows.ERROR_SUCCESS {
		return Error(hrS_OK)
	}
	if ue := uint32(e); (ue & hrFlagBitsMask) == hrCustomerBit {
		// syscall.APPLICATION_ERROR == hrCustomerBit, so the only other thing
		// we need to do to transform this into an HRESULT is add the fail flag
		return Error(HRESULT(ue | hrFailBit))
	}
	if uint32(e) > hrCodeMax {
		// Can't be encoded in HRESULT, return generic error instead
		return genericError
	}
	return Error(hresultFromFacilityAndCode(hrFail, facilityWin32, hrCode(e)))
}

// ErrorFromNTStatus creates an Error from s.
func ErrorFromNTStatus(s windows.NTStatus) Error {
	if s == windows.STATUS_SUCCESS {
		return Error(hrS_OK)
	}
	return Error(HRESULT(s) | hrFacilityNTBit)
}

// ErrorFromHRESULT creates an Error from hr.
func ErrorFromHRESULT(hr HRESULT) Error {
	return Error(hr)
}

// NewError converts e into an Error if e's type is supported. It returns
// both the Error and a bool indicating whether the conversion was successful.
func NewError(e any) (Error, bool) {
	switch v := e.(type) {
	case Error:
		return v, true
	case windows.NTStatus:
		return ErrorFromNTStatus(v), true
	case windows.Errno:
		return ErrorFromErrno(v), true
	case HRESULT:
		return ErrorFromHRESULT(v), true
	default:
		return ErrorFromHRESULT(hrTYPE_E_WRONGTYPEKIND), false
	}
}

// IsOK returns true when the Error is unconditionally successful.
func (e Error) IsOK() bool {
	return HRESULT(e) == hrS_OK
}

// Succeeded returns true when the Error is successful, but its error code
// may include additional status information.
func (e Error) Succeeded() bool {
	return HRESULT(e).Succeeded()
}

// Failed returns true when the Error contains a failure code.
func (e Error) Failed() bool {
	return HRESULT(e).Failed()
}

// AsHRESULT converts the Error to a HRESULT.
func (e Error) AsHRESULT() HRESULT {
	return HRESULT(e)
}

type errnoFailHandler func(hr HRESULT) windows.Errno

func (e Error) toErrno(f errnoFailHandler) windows.Errno {
	hr := HRESULT(e)

	if hr == hrS_OK {
		return windows.ERROR_SUCCESS
	}

	if hr.isCustomer() {
		return windows.Errno(uint32(e) ^ hrFailBit)
	}

	if hr.isNT() {
		return e.AsNTStatus().Errno()
	}

	if hr.facility() == facilityWin32 {
		return windows.Errno(hr.code())
	}

	if errno, ok := commonHRESULTToErrno[hr]; ok {
		return errno
	}

	return f(hr)
}

// AsError converts the Error to a windows.Errno, but panics if not possible.
func (e Error) AsErrno() windows.Errno {
	handler := func(hr HRESULT) windows.Errno {
		panic(fmt.Sprintf("wingoes.Error: Called AsErrno on a non-convertable HRESULT 0x%08X", uint32(hr)))
		return windows.ERROR_UNIDENTIFIED_ERROR
	}

	return e.toErrno(handler)
}

type ntStatusFailHandler func(hr HRESULT) windows.NTStatus

func (e Error) toNTStatus(f ntStatusFailHandler) windows.NTStatus {
	hr := HRESULT(e)

	if hr == hrS_OK {
		return windows.STATUS_SUCCESS
	}

	if hr.isNT() {
		return windows.NTStatus(hr ^ hrFacilityNTBit)
	}

	return f(hr)
}

// AsNTStatus converts the Error to a windows.NTStatus, but panics if not possible.
func (e Error) AsNTStatus() windows.NTStatus {
	handler := func(hr HRESULT) windows.NTStatus {
		panic(fmt.Sprintf("windows.Error: Called AsNTStatus on a non-NTSTATUS HRESULT 0x%08X", uint32(hr)))
		return windows.STATUS_UNSUCCESSFUL
	}

	return e.toNTStatus(handler)
}

// TryAsErrno converts the Error to a windows.Errno, or returns defval if
// such a conversion is not possible.
func (e Error) TryAsErrno(defval windows.Errno) windows.Errno {
	handler := func(hr HRESULT) windows.Errno {
		return defval
	}

	return e.toErrno(handler)
}

// TryAsNTStatus converts the Error to a windows.NTStatus, or returns defval if
// such a conversion is not possible.
func (e Error) TryAsNTStatus(defval windows.NTStatus) windows.NTStatus {
	handler := func(hr HRESULT) windows.NTStatus {
		return defval
	}

	return e.toNTStatus(handler)
}

// IsAvailableAsHRESULT returns true if e may be converted to an HRESULT.
func (e Error) IsAvailableAsHRESULT() bool {
	return true
}

// IsAvailableAsErrno returns true if e may be converted to a windows.Errno.
func (e Error) IsAvailableAsErrno() bool {
	hr := HRESULT(e)
	if hr.isCustomer() || e.IsAvailableAsNTStatus() || (hr.facility() == facilityWin32) {
		return true
	}
	_, convertable := commonHRESULTToErrno[hr]
	return convertable
}

// IsAvailableAsNTStatus returns true if e may be converted to a windows.NTStatus.
func (e Error) IsAvailableAsNTStatus() bool {
	return HRESULT(e) == hrS_OK || HRESULT(e).isNT()
}

// Error produces a human-readable message describing Error e.
func (e Error) Error() string {
	if HRESULT(e).isCustomer() {
		return windows.Errno(uint32(e) ^ hrFailBit).Error()
	}

	buf := make([]uint16, 300)
	const flags = windows.FORMAT_MESSAGE_FROM_SYSTEM | windows.FORMAT_MESSAGE_IGNORE_INSERTS
	lenExclNul, err := windows.FormatMessage(flags, 0, uint32(e), 0, buf, nil)
	if err != nil {
		return fmt.Sprintf("wingoes.Error 0x%08X", uint32(e))
	}
	for ; lenExclNul > 0 && (buf[lenExclNul-1] == '\n' || buf[lenExclNul-1] == '\r'); lenExclNul-- {
	}
	return windows.UTF16ToString(buf[:lenExclNul])
}

// Unwrap permits extraction of underlying windows.NTStatus or windows.Errno
// errors that are encoded within e.
func (e Error) Unwrap() error {
	// Order is important! We need earlier checks to exclude certain things that
	// would otherwise be (in this case) false positives in later checks!
	switch {
	case e.IsOK():
		return nil
	case e.IsAvailableAsNTStatus():
		return e.AsNTStatus()
	case e.IsAvailableAsErrno():
		return e.AsErrno()
	default:
		return nil
	}
}
