//go:build windows
// +build windows

package wmiext

import (
	"errors"
	"fmt"
	"reflect"
	"strconv"
	"strings"
	"syscall"
	"time"
	"unsafe"

	"github.com/go-ole/go-ole"
	"github.com/sirupsen/logrus"
)

const (
	WmiPathKey = "__PATH"
)

var (
	WindowsEpoch = time.Date(1601, 1, 1, 0, 0, 0, 0, time.UTC)
)

type Instance struct {
	object  *ole.IUnknown
	vTable  *IWbemClassObjectVtbl
	service *Service
}

type IWbemClassObjectVtbl struct {
	QueryInterface          uintptr
	AddRef                  uintptr
	Release                 uintptr
	GetQualifierSet         uintptr
	Get                     uintptr
	Put                     uintptr
	Delete                  uintptr
	GetNames                uintptr
	BeginEnumeration        uintptr
	Next                    uintptr
	EndEnumeration          uintptr
	GetPropertyQualifierSet uintptr
	Clone                   uintptr
	GetObjectText           uintptr
	SpawnDerivedClass       uintptr
	SpawnInstance           uintptr
	CompareTo               uintptr
	GetPropertyOrigin       uintptr
	InheritsFrom            uintptr
	GetMethod               uintptr
	PutMethod               uintptr
	DeleteMethod            uintptr
	BeginMethodEnumeration  uintptr
	NextMethod              uintptr
	EndMethodEnumeration    uintptr
	GetMethodQualifierSet   uintptr
	GetMethodOrigin         uintptr
}

type CIMTYPE_ENUMERATION uint32

const (
	CIM_ILLEGAL    CIMTYPE_ENUMERATION = 0xFFF
	CIM_EMPTY      CIMTYPE_ENUMERATION = 0
	CIM_SINT8      CIMTYPE_ENUMERATION = 16
	CIM_UINT8      CIMTYPE_ENUMERATION = 17
	CIM_SINT16     CIMTYPE_ENUMERATION = 2
	CIM_UINT16     CIMTYPE_ENUMERATION = 18
	CIM_SINT32     CIMTYPE_ENUMERATION = 3
	CIM_UINT32     CIMTYPE_ENUMERATION = 19
	CIM_SINT64     CIMTYPE_ENUMERATION = 20
	CIM_UINT64     CIMTYPE_ENUMERATION = 21
	CIM_REAL32     CIMTYPE_ENUMERATION = 4
	CIM_REAL64     CIMTYPE_ENUMERATION = 5
	CIM_BOOLEAN    CIMTYPE_ENUMERATION = 11
	CIM_STRING     CIMTYPE_ENUMERATION = 8
	CIM_DATETIME   CIMTYPE_ENUMERATION = 101
	CIM_REFERENCE  CIMTYPE_ENUMERATION = 102
	CIM_CHAR16     CIMTYPE_ENUMERATION = 103
	CIM_OBJECT     CIMTYPE_ENUMERATION = 13
	CIM_FLAG_ARRAY CIMTYPE_ENUMERATION = 0x2000
)

type WBEM_FLAVOR_TYPE uint32

const (
	WBEM_FLAVOR_DONT_PROPAGATE                  WBEM_FLAVOR_TYPE = 0
	WBEM_FLAVOR_FLAG_PROPAGATE_TO_INSTANCE      WBEM_FLAVOR_TYPE = 0x1
	WBEM_FLAVOR_FLAG_PROPAGATE_TO_DERIVED_CLASS WBEM_FLAVOR_TYPE = 0x2
	WBEM_FLAVOR_MASK_PROPAGATION                WBEM_FLAVOR_TYPE = 0xf
	WBEM_FLAVOR_OVERRIDABLE                     WBEM_FLAVOR_TYPE = 0
	WBEM_FLAVOR_NOT_OVERRIDABLE                 WBEM_FLAVOR_TYPE = 0x10
	WBEM_FLAVOR_MASK_PERMISSIONS                WBEM_FLAVOR_TYPE = 0x10
	WBEM_FLAVOR_ORIGIN_LOCAL                    WBEM_FLAVOR_TYPE = 0
	WBEM_FLAVOR_ORIGIN_PROPAGATED               WBEM_FLAVOR_TYPE = 0x20
	WBEM_FLAVOR_ORIGIN_SYSTEM                   WBEM_FLAVOR_TYPE = 0x40
	WBEM_FLAVOR_MASK_ORIGIN                     WBEM_FLAVOR_TYPE = 0x60
	WBEM_FLAVOR_NOT_AMENDED                     WBEM_FLAVOR_TYPE = 0
	WBEM_FLAVOR_AMENDED                         WBEM_FLAVOR_TYPE = 0x80
	WBEM_FLAVOR_MASK_AMENDED                    WBEM_FLAVOR_TYPE = 0x80
)

func newInstance(object *ole.IUnknown, service *Service) *Instance {
	instance := &Instance{
		object:  object,
		vTable:  (*IWbemClassObjectVtbl)(unsafe.Pointer(object.RawVTable)),
		service: service,
	}

	return instance
}

// Close cleans up all memory associated with this instance.
func (i *Instance) Close() {
	if i != nil && i.object != nil {
		i.object.Release()
	}
}

// GetClassName Gets the WMI class name for this WMI object instance
func (i *Instance) GetClassName() (className string, err error) {
	return i.GetAsString(`__CLASS`)
}

// Path gets the WMI object path of this instance
func (i *Instance) Path() (string, error) {
	ref, _, _, err := i.GetAsAny(WmiPathKey)
	return ref.(string), err
}

// IsReferenceProperty returns whether the property is of type CIM_REFERENCE, a string which points to
// an object path of another instance.
func (i *Instance) IsReferenceProperty(name string) (bool, error) {
	_, cimType, _, err := i.GetAsAny(name)
	return cimType == CIM_REFERENCE, err
}

// SpawnInstance create a new WMI object instance that is zero-initialized. The returned instance
// will not respect expected default values, which must be populated by other means.
func (i *Instance) SpawnInstance() (instance *Instance, err error) {
	var res uintptr
	var newUnknown *ole.IUnknown

	res, _, _ = syscall.SyscallN(
		i.vTable.SpawnInstance,               // IWbemClassObject::SpawnInstance(
		uintptr(unsafe.Pointer(i.object)),    // IWbemClassObject ptr
		uintptr(0),                           // [in]  long             lFlags,
		uintptr(unsafe.Pointer(&newUnknown))) // [out] IWbemClassObject **ppNewInstance)
	if res != 0 {
		return nil, NewWmiError(res)
	}

	return newInstance(newUnknown, i.service), nil
}

// CloneInstance create a new cloned copy of this WMI instance.
func (i *Instance) CloneInstance() (*Instance, error) {
	classObj := i.object
	vTable := (*IWbemClassObjectVtbl)(unsafe.Pointer(classObj.RawVTable))
	var cloned *ole.IUnknown

	ret, _, _ := syscall.SyscallN(
		vTable.Clone,                      // IWbemClassObject::Clone(
		uintptr(unsafe.Pointer(classObj)), // IWbemClassObject ptr
		uintptr(unsafe.Pointer(&cloned)))  // [out] IWbemClassObject **ppCopy)
	if ret != 0 {
		return nil, NewWmiError(ret)
	}

	return newInstance(cloned, i.service), nil
}

// PutAll sets all fields of this instance to the passed src parameter's fields, converting accordingly.
// The src parameter must be a pointer to a struct, otherwise an error will be returned.
func (i *Instance) PutAll(src interface{}) error {
	val := reflect.ValueOf(src)
	if val.Kind() == reflect.Pointer {
		val = val.Elem()
	}

	if val.Kind() != reflect.Struct {
		return errors.New("not a struct or pointer to struct")
	}

	props, err := i.GetAllProperties()
	if err != nil {
		return err
	}

	return i.instancePutAllTraverse(val, props)
}

func (i *Instance) instancePutAllTraverse(val reflect.Value, propMap map[string]interface{}) error {
	for j := 0; j < val.NumField(); j++ {
		fieldVal := val.Field(j)
		fieldType := val.Type().Field(j)

		if fieldType.Type.Kind() == reflect.Struct && fieldType.Anonymous {
			if err := i.instancePutAllTraverse(fieldVal, propMap); err != nil {
				return err
			}
			continue
		}
		if strings.HasPrefix(fieldType.Name, "S__") {
			continue
		}

		if !fieldType.IsExported() {
			continue
		}

		if _, exists := propMap[fieldType.Name]; !exists {
			continue
		}

		if fieldVal.Kind() == reflect.String && fieldVal.Len() == 0 {
			continue
		}

		if err := i.Put(fieldType.Name, fieldVal.Interface()); err != nil {
			return err
		}
	}

	return nil
}

// Put sets the specified property to the passed Golang value, converting appropriately.
func (i *Instance) Put(name string, value interface{}) (err error) {
	var variant ole.VARIANT

	switch cast := value.(type) {
	case ole.VARIANT:
		variant = cast
	case *ole.VARIANT:
		variant = *cast
	default:
		variant, err = NewAutomationVariant(value)
		if err != nil {
			return err
		}
	}

	var wszName *uint16
	if wszName, err = syscall.UTF16PtrFromString(name); err != nil {
		return
	}

	classObj := i.object
	vTable := (*IWbemClassObjectVtbl)(unsafe.Pointer(classObj.RawVTable))
	res, _, _ := syscall.SyscallN(
		vTable.Put,                        // IWbemClassObject::Put(
		uintptr(unsafe.Pointer(classObj)), // IWbemClassObject ptr
		uintptr(unsafe.Pointer(wszName)),  // [in] LPCWSTR wszName,
		uintptr(0),                        // [in] long    lFlags,
		uintptr(unsafe.Pointer(&variant)), // [in] VARIANT *pVal,
		uintptr(0))                        // [in] CIMTYPE Type)
	if res != 0 {
		return NewWmiError(res)
	}

	_ = variant.Clear()
	return
}

// GetCimText returns the CIM XML representation of this instance. Some WMI methods use a string
// parameter to represent a full complex object, and this method is used to generate
// the expected format.
func (i *Instance) GetCimText() string {
	type wmiWbemTxtSrcVtable struct {
		QueryInterface uintptr
		AddRef         uintptr
		Release        uintptr
		GetTxt         uintptr
	}
	const CIM_XML_FORMAT = 1

	classObj := i.object

	vTable := (*wmiWbemTxtSrcVtable)(unsafe.Pointer(wmiWbemTxtLocator.RawVTable))
	var retString *uint16
	res, _, _ := syscall.SyscallN(
		vTable.GetTxt,                           // IWbemObjectTextSrc::GetText()
		uintptr(unsafe.Pointer(wmiWbemLocator)), // IWbemObjectTextSrc ptr
		uintptr(0),                              // [in]  long             lFlags
		uintptr(unsafe.Pointer(classObj)),       // [in]  IWbemClassObject *pObj
		uintptr(CIM_XML_FORMAT),                 // [in]  ULONG            uObjTextFormat,
		uintptr(0),                              // [in]  IWbemContext     *pCtx,
		uintptr(unsafe.Pointer(&retString)))     // [out] BSTR             *strText)
	if res != 0 {
		return ""
	}
	itemStr := ole.BstrToString(retString)
	return itemStr
}

// GetAll gets all fields that map to a target struct and populates all struct fields according to
// the expected type information. The target parameter should be a pointer to a struct, and
// will return an error otherwise.
func (i *Instance) GetAll(target interface{}) error {
	elem := reflect.ValueOf(target)
	if elem.Kind() != reflect.Ptr || elem.IsNil() {
		return errors.New("invalid destination type for mapping a WMI instance to an object")
	}

	// deref pointer
	elem = elem.Elem()
	var err error

	if err = i.BeginEnumeration(); err != nil {
		return err
	}

	properties := make(map[string]*ole.VARIANT)

	for {
		var name string
		var value *ole.VARIANT
		var done bool

		if done, name, value, _, _, err = i.NextAsVariant(); err != nil {
			return err
		}

		if done {
			break
		}

		if value != nil {
			properties[name] = value
		}
	}

	defer func() {
		for _, v := range properties {
			_ = v.Clear()
		}
	}()

	_ = i.EndEnumeration()

	return i.instanceGetAllPopulate(elem, elem.Type(), properties)
}

// GetAsAny gets a property and converts it to a Golang type that matches the internal
// variant automation type passed back from WMI. For usage with predictable static
// type mapping, use GetAsString(), GetAsUint(), or GetAll() instead of this method.
func (i *Instance) GetAsAny(name string) (interface{}, CIMTYPE_ENUMERATION, WBEM_FLAVOR_TYPE, error) {
	variant, cimType, flavor, err := i.GetAsVariant(name)
	if err != nil {
		return nil, cimType, flavor, err
	}

	defer func() {
		if err := variant.Clear(); err != nil {
			logrus.Error(err)
		}
	}()

	// Since there is no type information only perform the stock conversion
	result := convertToGenericValue(variant)

	return result, cimType, flavor, err
}

// GetAsString gets a property value as a string value, converting if necessary
func (i *Instance) GetAsString(name string) (value string, err error) {
	variant, _, _, err := i.GetAsVariant(name)
	if err != nil || variant == nil {
		return "", err
	}
	defer func() {
		if err := variant.Clear(); err != nil {
			logrus.Error(err)
		}
	}()

	// TODO: replace with something better
	return fmt.Sprintf("%v", convertToGenericValue(variant)), nil
}

// GetAsUint gets a property value as a uint value, if conversion is possible. Otherwise,
// returns an error.
func (i *Instance) GetAsUint(name string) (uint, error) {
	val, _, _, err := i.GetAsAny(name)
	if err != nil {
		return 0, err
	}

	switch ret := val.(type) {
	case int:
		return uint(ret), nil
	case int8:
		return uint(ret), nil
	case int16:
		return uint(ret), nil
	case int32:
		return uint(ret), nil
	case int64:
		return uint(ret), nil
	case uint:
		return ret, nil
	case uint8:
		return uint(ret), nil
	case uint16:
		return uint(ret), nil
	case uint32:
		return uint(ret), nil
	case uint64:
		return uint(ret), nil
	case string:
		parse, err := strconv.ParseUint(ret, 10, 64)
		return uint(parse), err
	default:
		return 0, fmt.Errorf("type conversion from %T on param %s not supported", val, name)
	}
}

// GetAsVariant obtains a specified property value, if it exists.
func (i *Instance) GetAsVariant(name string) (*ole.VARIANT, CIMTYPE_ENUMERATION, WBEM_FLAVOR_TYPE, error) {
	var variant ole.VARIANT
	var err error
	var wszName *uint16
	var cimType CIMTYPE_ENUMERATION
	var flavor WBEM_FLAVOR_TYPE

	if wszName, err = syscall.UTF16PtrFromString(name); err != nil {
		return nil, 0, 0, err
	}

	classObj := i.object
	vTable := (*IWbemClassObjectVtbl)(unsafe.Pointer(classObj.RawVTable))

	res, _, _ := syscall.SyscallN(
		vTable.Get,                        // IWbemClassObject::Get(
		uintptr(unsafe.Pointer(classObj)), // IWbemClassObject ptr
		uintptr(unsafe.Pointer(wszName)),  // [in]            LPCWSTR wszName,
		uintptr(0),                        // [in]            long    lFlags,
		uintptr(unsafe.Pointer(&variant)), // [out]           VARIANT *pVal,
		uintptr(unsafe.Pointer(&cimType)), // [out, optional] CIMTYPE *pType,
		uintptr(unsafe.Pointer(&flavor)))  // [out, optional] long    *plFlavor)
	if res != 0 {
		return nil, 0, 0, NewWmiError(res)
	}

	return &variant, cimType, flavor, nil
}

// Next retrieves the next property as a Golang type when iterating the properties using an enumerator
// created by BeginEnumeration(). The returned value's type represents the internal automation type
// used by WMI. It is usually preferred to use GetAsXXX(), GetAll(), or GetAll Properties() over this
// method.
func (i *Instance) Next() (done bool, name string, value interface{}, cimType CIMTYPE_ENUMERATION, flavor WBEM_FLAVOR_TYPE, err error) {
	var variant *ole.VARIANT
	done, name, variant, cimType, flavor, err = i.NextAsVariant()

	if err == nil && !done {
		defer func() {
			if err := variant.Clear(); err != nil {
				logrus.Error(err)
			}
		}()
		value = convertToGenericValue(variant)
	}

	return
}

// NextAsVariant retrieves the next property as a VARIANT type when iterating the properties using an enumerator
// created by BeginEnumeration(). The returned value's type represents the internal automation type
// used by WMI. It is usually preferred to use GetAsXXX(), GetAll(), or GetAllProperties() over this
// method. Callers are responsible for clearing the VARIANT, otherwise associated memory will leak.
func (i *Instance) NextAsVariant() (bool, string, *ole.VARIANT, CIMTYPE_ENUMERATION, WBEM_FLAVOR_TYPE, error) {
	var res uintptr
	var strName *uint16
	var variant ole.VARIANT
	var cimType CIMTYPE_ENUMERATION
	var flavor WBEM_FLAVOR_TYPE

	res, _, _ = syscall.SyscallN(
		i.vTable.Next,                     // IWbemClassObject::Next(
		uintptr(unsafe.Pointer(i.object)), // IWbemClassObject ptr
		uintptr(0),                        // [in]            long    lFlags,
		uintptr(unsafe.Pointer(&strName)), // [out]           BSTR    *strName,
		uintptr(unsafe.Pointer(&variant)), // [out]           VARIANT *pVal,
		uintptr(unsafe.Pointer(&cimType)), // [out, optional] CIMTYPE *pType,
		uintptr(unsafe.Pointer(&flavor)))  // [out, optional] long    *plFlavor
	if int(res) < 0 {
		return false, "", nil, cimType, flavor, NewWmiError(res)
	}

	if res == WBEM_S_NO_MORE_DATA {
		return true, "", nil, cimType, flavor, nil
	}

	defer ole.SysFreeString((*int16)(unsafe.Pointer(strName))) //nolint:errcheck
	name := ole.BstrToString(strName)

	return false, name, &variant, cimType, flavor, nil
}

// GetAllProperties gets all properties on this instance. The returned map is keyed by the field name and the value
// is a Golang type which matches the WMI internal implementation. For static type conversions,
// it's recommended to use either GetAll(), which uses struct fields for type information, or
// the GetAsXXX() methods.
func (i *Instance) GetAllProperties() (map[string]interface{}, error) {
	var err error
	properties := make(map[string]interface{})

	if err = i.BeginEnumeration(); err != nil {
		return nil, err
	}

	defer func() {
		if err := i.EndEnumeration(); err != nil {
			logrus.Error(err)
		}
	}()

	for {
		var name string
		var value interface{}
		var done bool

		if done, name, value, _, _, err = i.Next(); err != nil || done {
			return properties, err
		}

		properties[name] = value
	}
}

// GetMethodParameters returns a WMI class object which represents the [in] method parameters for a method invocation.
// This is an advanced method, used for dynamic introspection or manual method invocation. In most
// cases it is recommended to use BeginInvoke() instead, which constructs the parameter payload
// automatically.
func (i *Instance) GetMethodParameters(method string) (*Instance, error) {
	var err error
	var res uintptr
	var inSignature *ole.IUnknown

	var wszName *uint16
	if wszName, err = syscall.UTF16PtrFromString(method); err != nil {
		return nil, err
	}

	res, _, _ = syscall.SyscallN(
		i.vTable.GetMethod,                    // IWbemClassObject::GetMethod(
		uintptr(unsafe.Pointer(i.object)),     // IWbemClassObject ptr
		uintptr(unsafe.Pointer(wszName)),      // [in]  LPCWSTR          wszName
		uintptr(0),                            // [in]  long             lFlags,
		uintptr(unsafe.Pointer(&inSignature)), // [out] IWbemClassObject **ppInSignature,
		uintptr(0))                            // [out] IWbemClassObject **ppOutSignature)
	if res != 0 {
		return nil, NewWmiError(res)
	}

	return newInstance(inSignature, i.service), nil
}

func (i *Instance) instanceGetAllPopulate(elem reflect.Value, elemType reflect.Type, properties map[string]*ole.VARIANT) error {
	var err error

	for j := 0; j < elemType.NumField(); j++ {
		fieldType := elemType.Field(j)
		fieldVal := elem.Field(j)

		if !fieldType.IsExported() {
			continue
		}

		if fieldType.Type.Kind() == reflect.Struct && fieldType.Anonymous {
			if err := i.instanceGetAllPopulate(fieldVal, fieldType.Type, properties); err != nil {
				return err
			}
			continue
		}

		fieldName := fieldType.Name

		if strings.HasPrefix(fieldName, "S__") {
			fieldName = fieldName[1:]
		}
		if variant, ok := properties[fieldName]; ok {
			var val interface{}
			if val, err = convertToGoType(variant, fieldVal, fieldType.Type); err != nil {
				return err
			}

			if val != nil {
				fieldVal.Set(reflect.ValueOf(val))
			}
		}
	}

	return nil
}

// BeginEnumeration begins iterating the property list on this instance. This is an advanced method.
// In most cases, the GetAsXXX() methods, GetAll(), and GetAllProperties() methods should be
// preferred.
func (i *Instance) BeginEnumeration() error {
	classObj := i.object
	vTable := (*IWbemClassObjectVtbl)(unsafe.Pointer(classObj.RawVTable))

	result, _, _ := syscall.SyscallN(
		vTable.BeginEnumeration,           // IWbemClassObject::BeginEnumeration(
		uintptr(unsafe.Pointer(classObj)), // IWbemClassObject ptr,
		uintptr(0))                        // [in] long lEnumFlags) // 0 = defaults
	if result != 0 {
		return NewWmiError(result)
	}

	return nil
}

// EndEnumeration completes iterating a property list on this instance. This is an advanced method.
// In most cases, the GetAsXXX() methods, GetAll(), and GetAllProperties() methods
// should be preferred.
func (i *Instance) EndEnumeration() error {
	res, _, _ := syscall.SyscallN(
		i.vTable.EndEnumeration,           // IWbemClassObject::EndEnumeration(
		uintptr(unsafe.Pointer(i.object))) // IWbemClassObject ptr)
	if res != 0 {
		return NewWmiError(res)
	}

	return nil
}

// BeginInvoke invokes a method on this Instance. Returns a MethodExecutor builder object
// that is used to construct the input parameters (via calls to In()), perform the
// invocation (using calls to Execute()), retrieve output parameters (via calls to
// Out()), and finally the method return value (using a call to End())
func (i *Instance) BeginInvoke(method string) *MethodExecutor {
	objPath, err := i.Path()
	if err != nil {
		return &MethodExecutor{err: err}
	}

	var class, inParam *Instance
	if class, err = i.service.GetClassInstance(i); err == nil {
		inParam, err = class.GetMethodParameters(method)
		class.Close()
	}

	return &MethodExecutor{method: method, path: objPath, service: i.service, inParam: inParam, err: err}
}
