// Copyright 2015 Google Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package proptools

import (
	"fmt"
	"reflect"
	"slices"
	"strings"
)

// AppendProperties appends the values of properties in the property struct src to the property
// struct dst. dst and src must be the same type, and both must be pointers to structs. Properties
// tagged `blueprint:"mutated"` are skipped.
//
// The filter function can prevent individual properties from being appended by returning false, or
// abort AppendProperties with an error by returning an error.  Passing nil for filter will append
// all properties.
//
// An error returned by AppendProperties that applies to a specific property will be an
// *ExtendPropertyError, and can have the property name and error extracted from it.
//
// The append operation is defined as appending strings and slices of strings normally, OR-ing bool
// values, replacing non-nil pointers to booleans or strings, and recursing into
// embedded structs, pointers to structs, and interfaces containing
// pointers to structs.  Appending the zero value of a property will always be a no-op.
func AppendProperties(dst interface{}, src interface{}, filter ExtendPropertyFilterFunc) error {
	return extendProperties(dst, src, filter, OrderAppend)
}

// PrependProperties prepends the values of properties in the property struct src to the property
// struct dst. dst and src must be the same type, and both must be pointers to structs. Properties
// tagged `blueprint:"mutated"` are skipped.
//
// The filter function can prevent individual properties from being prepended by returning false, or
// abort PrependProperties with an error by returning an error.  Passing nil for filter will prepend
// all properties.
//
// An error returned by PrependProperties that applies to a specific property will be an
// *ExtendPropertyError, and can have the property name and error extracted from it.
//
// The prepend operation is defined as prepending strings, and slices of strings normally, OR-ing
// bool values, replacing non-nil pointers to booleans or strings, and recursing into
// embedded structs, pointers to structs, and interfaces containing
// pointers to structs.  Prepending the zero value of a property will always be a no-op.
func PrependProperties(dst interface{}, src interface{}, filter ExtendPropertyFilterFunc) error {
	return extendProperties(dst, src, filter, OrderPrepend)
}

// AppendMatchingProperties appends the values of properties in the property struct src to the
// property structs in dst.  dst and src do not have to be the same type, but every property in src
// must be found in at least one property in dst.  dst must be a slice of pointers to structs, and
// src must be a pointer to a struct.  Properties tagged `blueprint:"mutated"` are skipped.
//
// The filter function can prevent individual properties from being appended by returning false, or
// abort AppendProperties with an error by returning an error.  Passing nil for filter will append
// all properties.
//
// An error returned by AppendMatchingProperties that applies to a specific property will be an
// *ExtendPropertyError, and can have the property name and error extracted from it.
//
// The append operation is defined as appending strings, and slices of strings normally, OR-ing bool
// values, replacing pointers to booleans or strings whether they are nil or not, and recursing into
// embedded structs, pointers to structs, and interfaces containing
// pointers to structs.  Appending the zero value of a property will always be a no-op.
func AppendMatchingProperties(dst []interface{}, src interface{},
	filter ExtendPropertyFilterFunc) error {
	return extendMatchingProperties(dst, src, filter, OrderAppend)
}

// PrependMatchingProperties prepends the values of properties in the property struct src to the
// property structs in dst.  dst and src do not have to be the same type, but every property in src
// must be found in at least one property in dst.  dst must be a slice of pointers to structs, and
// src must be a pointer to a struct.  Properties tagged `blueprint:"mutated"` are skipped.
//
// The filter function can prevent individual properties from being prepended by returning false, or
// abort PrependProperties with an error by returning an error.  Passing nil for filter will prepend
// all properties.
//
// An error returned by PrependProperties that applies to a specific property will be an
// *ExtendPropertyError, and can have the property name and error extracted from it.
//
// The prepend operation is defined as prepending strings, and slices of strings normally, OR-ing
// bool values, replacing nil pointers to booleans or strings, and recursing into
// embedded structs, pointers to structs, and interfaces containing
// pointers to structs.  Prepending the zero value of a property will always be a no-op.
func PrependMatchingProperties(dst []interface{}, src interface{},
	filter ExtendPropertyFilterFunc) error {
	return extendMatchingProperties(dst, src, filter, OrderPrepend)
}

// ExtendProperties appends or prepends the values of properties in the property struct src to the
// property struct dst. dst and src must be the same type, and both must be pointers to structs.
// Properties tagged `blueprint:"mutated"` are skipped.
//
// The filter function can prevent individual properties from being appended or prepended by
// returning false, or abort ExtendProperties with an error by returning an error.  Passing nil for
// filter will append or prepend all properties.
//
// The order function is called on each non-filtered property to determine if it should be appended
// or prepended.
//
// An error returned by ExtendProperties that applies to a specific property will be an
// *ExtendPropertyError, and can have the property name and error extracted from it.
//
// The append operation is defined as appending strings and slices of strings normally, OR-ing bool
// values, replacing non-nil pointers to booleans or strings, and recursing into
// embedded structs, pointers to structs, and interfaces containing
// pointers to structs.  Appending or prepending the zero value of a property will always be a
// no-op.
func ExtendProperties(dst interface{}, src interface{}, filter ExtendPropertyFilterFunc,
	order ExtendPropertyOrderFunc) error {
	return extendProperties(dst, src, filter, order)
}

// ExtendMatchingProperties appends or prepends the values of properties in the property struct src
// to the property structs in dst.  dst and src do not have to be the same type, but every property
// in src must be found in at least one property in dst.  dst must be a slice of pointers to
// structs, and src must be a pointer to a struct.  Properties tagged `blueprint:"mutated"` are
// skipped.
//
// The filter function can prevent individual properties from being appended or prepended by
// returning false, or abort ExtendMatchingProperties with an error by returning an error.  Passing
// nil for filter will append or prepend all properties.
//
// The order function is called on each non-filtered property to determine if it should be appended
// or prepended.
//
// An error returned by ExtendMatchingProperties that applies to a specific property will be an
// *ExtendPropertyError, and can have the property name and error extracted from it.
//
// The append operation is defined as appending strings, and slices of strings normally, OR-ing bool
// values, replacing non-nil pointers to booleans or strings, and recursing into
// embedded structs, pointers to structs, and interfaces containing
// pointers to structs.  Appending or prepending the zero value of a property will always be a
// no-op.
func ExtendMatchingProperties(dst []interface{}, src interface{},
	filter ExtendPropertyFilterFunc, order ExtendPropertyOrderFunc) error {
	return extendMatchingProperties(dst, src, filter, order)
}

type Order int

const (
	// When merging properties, strings and lists will be concatenated, and booleans will be OR'd together
	Append Order = iota
	// Same as append, but acts as if the arguments to the extend* functions were swapped. The src value will be
	// prepended to the dst value instead of appended.
	Prepend
	// Instead of concatenating/ORing properties, the dst value will be completely replaced by the src value.
	// Replace currently only works for slices, maps, and configurable properties. Due to legacy behavior,
	// pointer properties will always act as if they're using replace ordering.
	Replace
	// Same as replace, but acts as if the arguments to the extend* functions were swapped. The src value will be
	// used only if the dst value was unset.
	Prepend_replace
)

type ExtendPropertyFilterFunc func(dstField, srcField reflect.StructField) (bool, error)

type ExtendPropertyOrderFunc func(dstField, srcField reflect.StructField) (Order, error)

func OrderAppend(dstField, srcField reflect.StructField) (Order, error) {
	return Append, nil
}

func OrderPrepend(dstField, srcField reflect.StructField) (Order, error) {
	return Prepend, nil
}

func OrderReplace(dstField, srcField reflect.StructField) (Order, error) {
	return Replace, nil
}

type ExtendPropertyError struct {
	Err      error
	Property string
}

func (e *ExtendPropertyError) Error() string {
	return fmt.Sprintf("can't extend property %q: %s", e.Property, e.Err)
}

func extendPropertyErrorf(property string, format string, a ...interface{}) *ExtendPropertyError {
	return &ExtendPropertyError{
		Err:      fmt.Errorf(format, a...),
		Property: property,
	}
}

func extendProperties(dst interface{}, src interface{}, filter ExtendPropertyFilterFunc,
	order ExtendPropertyOrderFunc) error {

	srcValue, err := getStruct(src)
	if err != nil {
		if _, ok := err.(getStructEmptyError); ok {
			return nil
		}
		return err
	}

	dstValue, err := getOrCreateStruct(dst)
	if err != nil {
		return err
	}

	if dstValue.Type() != srcValue.Type() {
		return fmt.Errorf("expected matching types for dst and src, got %T and %T", dst, src)
	}

	dstValues := []reflect.Value{dstValue}

	return extendPropertiesRecursive(dstValues, srcValue, make([]string, 0, 8), filter, true, order)
}

func extendMatchingProperties(dst []interface{}, src interface{}, filter ExtendPropertyFilterFunc,
	order ExtendPropertyOrderFunc) error {

	srcValue, err := getStruct(src)
	if err != nil {
		if _, ok := err.(getStructEmptyError); ok {
			return nil
		}
		return err
	}

	dstValues := make([]reflect.Value, len(dst))
	for i := range dst {
		var err error
		dstValues[i], err = getOrCreateStruct(dst[i])
		if err != nil {
			return err
		}
	}

	return extendPropertiesRecursive(dstValues, srcValue, make([]string, 0, 8), filter, false, order)
}

func extendPropertiesRecursive(dstValues []reflect.Value, srcValue reflect.Value,
	prefix []string, filter ExtendPropertyFilterFunc, sameTypes bool,
	orderFunc ExtendPropertyOrderFunc) error {

	dstValuesCopied := false

	propertyName := func(field reflect.StructField) string {
		names := make([]string, 0, len(prefix)+1)
		for _, s := range prefix {
			names = append(names, PropertyNameForField(s))
		}
		names = append(names, PropertyNameForField(field.Name))
		return strings.Join(names, ".")
	}

	srcType := srcValue.Type()
	for i, srcField := range typeFields(srcType) {
		if ShouldSkipProperty(srcField) {
			continue
		}

		srcFieldValue := srcValue.Field(i)

		// Step into source interfaces
		if srcFieldValue.Kind() == reflect.Interface {
			if srcFieldValue.IsNil() {
				continue
			}

			srcFieldValue = srcFieldValue.Elem()

			if srcFieldValue.Kind() != reflect.Ptr {
				return extendPropertyErrorf(propertyName(srcField), "interface not a pointer")
			}
		}

		// Step into source pointers to structs
		if isStructPtr(srcFieldValue.Type()) {
			if srcFieldValue.IsNil() {
				continue
			}

			srcFieldValue = srcFieldValue.Elem()
		}

		found := false
		var recurse []reflect.Value
		// Use an iteration loop so elements can be added to the end of dstValues inside the loop.
		for j := 0; j < len(dstValues); j++ {
			dstValue := dstValues[j]
			dstType := dstValue.Type()
			var dstField reflect.StructField

			dstFields := typeFields(dstType)
			if dstType == srcType {
				dstField = dstFields[i]
			} else {
				var ok bool
				for _, field := range dstFields {
					if field.Name == srcField.Name {
						dstField = field
						ok = true
					} else if IsEmbedded(field) {
						embeddedDstValue := dstValue.FieldByIndex(field.Index)
						if isStructPtr(embeddedDstValue.Type()) {
							if embeddedDstValue.IsNil() {
								newEmbeddedDstValue := reflect.New(embeddedDstValue.Type().Elem())
								embeddedDstValue.Set(newEmbeddedDstValue)
							}
							embeddedDstValue = embeddedDstValue.Elem()
						}
						if !isStruct(embeddedDstValue.Type()) {
							return extendPropertyErrorf(propertyName(srcField), "%s is not a struct (%s)",
								propertyName(field), embeddedDstValue.Type())
						}
						// The destination struct contains an embedded struct, add it to the list
						// of destinations to consider.  Make a copy of dstValues if necessary
						// to avoid modifying the backing array of an input parameter.
						if !dstValuesCopied {
							dstValues = slices.Clone(dstValues)
							dstValuesCopied = true
						}
						dstValues = append(dstValues, embeddedDstValue)
					}
				}
				if !ok {
					continue
				}
			}

			found = true

			dstFieldValue := dstValue.FieldByIndex(dstField.Index)
			origDstFieldValue := dstFieldValue

			// Step into destination interfaces
			if dstFieldValue.Kind() == reflect.Interface {
				if dstFieldValue.IsNil() {
					return extendPropertyErrorf(propertyName(srcField), "nilitude mismatch")
				}

				dstFieldValue = dstFieldValue.Elem()

				if dstFieldValue.Kind() != reflect.Ptr {
					return extendPropertyErrorf(propertyName(srcField), "interface not a pointer")
				}
			}

			// Step into destination pointers to structs
			if isStructPtr(dstFieldValue.Type()) {
				if dstFieldValue.IsNil() {
					dstFieldValue = reflect.New(dstFieldValue.Type().Elem())
					origDstFieldValue.Set(dstFieldValue)
				}

				dstFieldValue = dstFieldValue.Elem()
			}

			switch srcFieldValue.Kind() {
			case reflect.Struct:
				if isConfigurable(srcField.Type) {
					if srcFieldValue.Type() != dstFieldValue.Type() {
						return extendPropertyErrorf(propertyName(srcField), "mismatched types %s and %s",
							dstFieldValue.Type(), srcFieldValue.Type())
					}
				} else {
					if sameTypes && dstFieldValue.Type() != srcFieldValue.Type() {
						return extendPropertyErrorf(propertyName(srcField), "mismatched types %s and %s",
							dstFieldValue.Type(), srcFieldValue.Type())
					}

					// Recursively extend the struct's fields.
					recurse = append(recurse, dstFieldValue)
					continue
				}
			case reflect.Bool, reflect.String, reflect.Slice, reflect.Map:
				// If the types don't match or srcFieldValue cannot be converted to a Configurable type, it's an error
				ct, err := configurableType(srcFieldValue.Type())
				if srcFieldValue.Type() != dstFieldValue.Type() && (err != nil || dstFieldValue.Type() != ct) {
					return extendPropertyErrorf(propertyName(srcField), "mismatched types %s and %s",
						dstFieldValue.Type(), srcFieldValue.Type())
				}
			case reflect.Ptr:
				// If the types don't match or srcFieldValue cannot be converted to a Configurable type, it's an error
				ct, err := configurableType(srcFieldValue.Type().Elem())
				if srcFieldValue.Type() != dstFieldValue.Type() && (err != nil || dstFieldValue.Type() != ct) {
					return extendPropertyErrorf(propertyName(srcField), "mismatched types %s and %s",
						dstFieldValue.Type(), srcFieldValue.Type())
				}
				switch ptrKind := srcFieldValue.Type().Elem().Kind(); ptrKind {
				case reflect.Bool, reflect.Int64, reflect.String, reflect.Struct:
				// Nothing
				default:
					return extendPropertyErrorf(propertyName(srcField), "pointer is a %s", ptrKind)
				}
			default:
				return extendPropertyErrorf(propertyName(srcField), "unsupported kind %s",
					srcFieldValue.Kind())
			}

			if filter != nil {
				b, err := filter(dstField, srcField)
				if err != nil {
					return &ExtendPropertyError{
						Property: propertyName(srcField),
						Err:      err,
					}
				}
				if !b {
					continue
				}
			}

			order := Append
			if orderFunc != nil {
				var err error
				order, err = orderFunc(dstField, srcField)
				if err != nil {
					return &ExtendPropertyError{
						Property: propertyName(srcField),
						Err:      err,
					}
				}
			}

			if HasTag(dstField, "android", "replace_instead_of_append") {
				if order == Append {
					order = Replace
				} else if order == Prepend {
					order = Prepend_replace
				}
			}

			ExtendBasicType(dstFieldValue, srcFieldValue, order)
		}

		if len(recurse) > 0 {
			err := extendPropertiesRecursive(recurse, srcFieldValue,
				append(prefix, srcField.Name), filter, sameTypes, orderFunc)
			if err != nil {
				return err
			}
		} else if !found {
			return extendPropertyErrorf(propertyName(srcField), "failed to find property to extend")
		}
	}

	return nil
}

func ExtendBasicType(dstFieldValue, srcFieldValue reflect.Value, order Order) {
	prepend := order == Prepend || order == Prepend_replace

	if !srcFieldValue.IsValid() {
		return
	}

	// If dst is a Configurable and src isn't, promote src to a Configurable.
	// This isn't necessary if all property structs are using Configurable values,
	// but it's helpful to avoid having to change as many places in the code when
	// converting properties to Configurable properties. For example, load hooks
	// make their own mini-property structs and append them onto the main property
	// structs when they want to change the default values of properties.
	srcFieldType := srcFieldValue.Type()
	if isConfigurable(dstFieldValue.Type()) && !isConfigurable(srcFieldType) {
		srcFieldValue = promoteValueToConfigurable(srcFieldValue)
	}

	switch srcFieldValue.Kind() {
	case reflect.Struct:
		if !isConfigurable(srcFieldValue.Type()) {
			panic("Should be unreachable")
		}
		replace := order == Prepend_replace || order == Replace
		unpackedDst := dstFieldValue.Interface().(configurableReflection)
		if unpackedDst.isEmpty() {
			// Properties that were never initialized via unpacking from a bp file value
			// will have a nil inner value, making them unable to be modified without a pointer
			// like we don't have here. So instead replace the whole configurable object.
			dstFieldValue.Set(reflect.ValueOf(srcFieldValue.Interface().(configurableReflection).clone()))
		} else {
			unpackedDst.setAppend(srcFieldValue.Interface(), replace, prepend)
		}
	case reflect.Bool:
		// Boolean OR
		dstFieldValue.Set(reflect.ValueOf(srcFieldValue.Bool() || dstFieldValue.Bool()))
	case reflect.String:
		if prepend {
			dstFieldValue.SetString(srcFieldValue.String() +
				dstFieldValue.String())
		} else {
			dstFieldValue.SetString(dstFieldValue.String() +
				srcFieldValue.String())
		}
	case reflect.Slice:
		if srcFieldValue.IsNil() {
			break
		}

		newSlice := reflect.MakeSlice(srcFieldValue.Type(), 0,
			dstFieldValue.Len()+srcFieldValue.Len())
		if prepend {
			newSlice = reflect.AppendSlice(newSlice, srcFieldValue)
			newSlice = reflect.AppendSlice(newSlice, dstFieldValue)
		} else if order == Append {
			newSlice = reflect.AppendSlice(newSlice, dstFieldValue)
			newSlice = reflect.AppendSlice(newSlice, srcFieldValue)
		} else {
			// replace
			newSlice = reflect.AppendSlice(newSlice, srcFieldValue)
		}
		dstFieldValue.Set(newSlice)
	case reflect.Map:
		if srcFieldValue.IsNil() {
			break
		}
		var mapValue reflect.Value
		// for append/prepend, maintain keys from original value
		// for replace, replace entire map
		if order == Replace || dstFieldValue.IsNil() {
			mapValue = srcFieldValue
		} else {
			mapValue = dstFieldValue

			iter := srcFieldValue.MapRange()
			for iter.Next() {
				dstValue := dstFieldValue.MapIndex(iter.Key())
				if prepend {
					// if the key exists in the map, keep the original value.
					if !dstValue.IsValid() {
						// otherwise, add the new value
						mapValue.SetMapIndex(iter.Key(), iter.Value())
					}
				} else {
					// For append, replace the original value.
					mapValue.SetMapIndex(iter.Key(), iter.Value())
				}
			}
		}
		dstFieldValue.Set(mapValue)
	case reflect.Ptr:
		if srcFieldValue.IsNil() {
			break
		}

		switch ptrKind := srcFieldValue.Type().Elem().Kind(); ptrKind {
		case reflect.Bool:
			if prepend {
				if dstFieldValue.IsNil() {
					dstFieldValue.Set(reflect.ValueOf(BoolPtr(srcFieldValue.Elem().Bool())))
				}
			} else {
				// For append, replace the original value.
				dstFieldValue.Set(reflect.ValueOf(BoolPtr(srcFieldValue.Elem().Bool())))
			}
		case reflect.Int64:
			if prepend {
				if dstFieldValue.IsNil() {
					// Int() returns Int64
					dstFieldValue.Set(reflect.ValueOf(Int64Ptr(srcFieldValue.Elem().Int())))
				}
			} else {
				// For append, replace the original value.
				// Int() returns Int64
				dstFieldValue.Set(reflect.ValueOf(Int64Ptr(srcFieldValue.Elem().Int())))
			}
		case reflect.String:
			if prepend {
				if dstFieldValue.IsNil() {
					dstFieldValue.Set(reflect.ValueOf(StringPtr(srcFieldValue.Elem().String())))
				}
			} else {
				// For append, replace the original value.
				dstFieldValue.Set(reflect.ValueOf(StringPtr(srcFieldValue.Elem().String())))
			}
		case reflect.Struct:
			srcFieldValue := srcFieldValue.Elem()
			if !isConfigurable(srcFieldValue.Type()) {
				panic("Should be unreachable")
			}
			panic("Don't use pointers to Configurable properties. All Configurable properties can be unset, " +
				"and the 'replacing' behavior can be accomplished with the `blueprint:\"replace_instead_of_append\" " +
				"struct field tag. There's no reason to have a pointer configurable property.")
		default:
			panic(fmt.Errorf("unexpected pointer kind %s", ptrKind))
		}
	}
}

// ShouldSkipProperty indicates whether a property should be skipped in processing.
func ShouldSkipProperty(structField reflect.StructField) bool {
	return structField.PkgPath != "" || // The field is not exported so just skip it.
		HasTag(structField, "blueprint", "mutated") // The field is not settable in a blueprint file
}

// IsEmbedded indicates whether a property is embedded. This is useful for determining nesting name
// as the name of the embedded field is _not_ used in blueprint files.
func IsEmbedded(structField reflect.StructField) bool {
	return structField.Name == "BlueprintEmbed" || structField.Anonymous
}

type getStructEmptyError struct{}

func (getStructEmptyError) Error() string { return "interface containing nil pointer" }

func getOrCreateStruct(in interface{}) (reflect.Value, error) {
	value, err := getStruct(in)
	if _, ok := err.(getStructEmptyError); ok {
		value := reflect.ValueOf(in)
		newValue := reflect.New(value.Type().Elem())
		value.Set(newValue)
	}

	return value, err
}

func getStruct(in interface{}) (reflect.Value, error) {
	value := reflect.ValueOf(in)
	if !isStructPtr(value.Type()) {
		return reflect.Value{}, fmt.Errorf("expected pointer to struct, got %s", value.Type())
	}
	if value.IsNil() {
		return reflect.Value{}, getStructEmptyError{}
	}
	value = value.Elem()
	return value, nil
}
