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

// 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.
//
// 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, false)
}

// 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.
//
// 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, true)
}

// 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.
//
// 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 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 AppendMatchingProperties(dst []interface{}, src interface{},
	filter ExtendPropertyFilterFunc) error {
	return extendMatchingProperties(dst, src, filter, false)
}

// 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.
//
// 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 PrependMatchingProperties(dst []interface{}, src interface{},
	filter ExtendPropertyFilterFunc) error {
	return extendMatchingProperties(dst, src, filter, true)
}

type ExtendPropertyFilterFunc func(property string,
	dstField, srcField reflect.StructField,
	dstValue, srcValue interface{}) (bool, error)

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,
	prepend bool) error {

	dstValue, err := getStruct(dst)
	if err != nil {
		return err
	}
	srcValue, err := getStruct(src)
	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, "", filter, true, prepend)
}

func extendMatchingProperties(dst []interface{}, src interface{}, filter ExtendPropertyFilterFunc,
	prepend bool) error {

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

	srcValue, err := getStruct(src)
	if err != nil {
		return err
	}

	return extendPropertiesRecursive(dstValues, srcValue, "", filter, false, prepend)
}

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

	srcType := srcValue.Type()
	for i := 0; i < srcValue.NumField(); i++ {
		srcField := srcType.Field(i)
		if srcField.PkgPath != "" {
			// The field is not exported so just skip it.
			continue
		}
		if HasTag(srcField, "blueprint", "mutated") {
			continue
		}

		propertyName := prefix + PropertyNameForField(srcField.Name)
		srcFieldValue := srcValue.Field(i)

		found := false
		for _, dstValue := range dstValues {
			dstType := dstValue.Type()
			var dstField reflect.StructField

			if dstType == srcType {
				dstField = dstType.Field(i)
			} else {
				var ok bool
				dstField, ok = dstType.FieldByName(srcField.Name)
				if !ok {
					continue
				}
			}

			found = true

			dstFieldValue := dstValue.FieldByIndex(dstField.Index)

			if srcFieldValue.Kind() != dstFieldValue.Kind() {
				return extendPropertyErrorf(propertyName, "mismatched types %s and %s",
					dstFieldValue.Type(), srcFieldValue.Type())
			}

			switch srcFieldValue.Kind() {
			case reflect.Interface:
				if dstFieldValue.IsNil() != srcFieldValue.IsNil() {
					return extendPropertyErrorf(propertyName, "nilitude mismatch")
				}
				if dstFieldValue.IsNil() {
					continue
				}

				dstFieldValue = dstFieldValue.Elem()
				srcFieldValue = srcFieldValue.Elem()

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

				fallthrough
			case reflect.Ptr:
				ptrKind := srcFieldValue.Type().Elem().Kind()
				if ptrKind == reflect.Bool || ptrKind == reflect.String {
					if srcFieldValue.Type() != dstFieldValue.Type() {
						return extendPropertyErrorf(propertyName, "mismatched pointer types %s and %s",
							dstFieldValue.Type(), srcFieldValue.Type())
					}
					break
				} else if ptrKind != reflect.Struct {
					return extendPropertyErrorf(propertyName, "pointer is a %s", ptrKind)
				}

				// Pointer to a struct
				if dstFieldValue.IsNil() != srcFieldValue.IsNil() {
					return extendPropertyErrorf(propertyName, "nilitude mismatch")
				}
				if dstFieldValue.IsNil() {
					continue
				}

				dstFieldValue = dstFieldValue.Elem()
				srcFieldValue = srcFieldValue.Elem()

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

				// Recursively extend the struct's fields.
				err := extendPropertiesRecursive([]reflect.Value{dstFieldValue}, srcFieldValue,
					propertyName+".", filter, sameTypes, prepend)
				if err != nil {
					return err
				}
				continue
			case reflect.Bool, reflect.String, reflect.Slice:
				if srcFieldValue.Type() != dstFieldValue.Type() {
					return extendPropertyErrorf(propertyName, "mismatched types %s and %s",
						dstFieldValue.Type(), srcFieldValue.Type())
				}
			default:
				return extendPropertyErrorf(propertyName, "unsupported kind %s",
					srcFieldValue.Kind())
			}

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

			switch srcFieldValue.Kind() {
			case reflect.Bool:
				// Boolean OR
				dstFieldValue.Set(reflect.ValueOf(srcFieldValue.Bool() || dstFieldValue.Bool()))
			case reflect.String:
				// Append the extension 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 {
					newSlice = reflect.AppendSlice(newSlice, dstFieldValue)
					newSlice = reflect.AppendSlice(newSlice, srcFieldValue)
				}
				dstFieldValue.Set(newSlice)
			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.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())))
					}
				default:
					panic(fmt.Errorf("unexpected pointer kind %s", ptrKind))
				}
			}
		}
		if !found {
			return extendPropertyErrorf(propertyName, "failed to find property to extend")
		}
	}

	return nil
}

func getStruct(in interface{}) (reflect.Value, error) {
	value := reflect.ValueOf(in)
	if value.Kind() != reflect.Ptr {
		return reflect.Value{}, fmt.Errorf("expected pointer to struct, got %T", in)
	}
	value = value.Elem()
	if value.Kind() != reflect.Struct {
		return reflect.Value{}, fmt.Errorf("expected pointer to struct, got %T", in)
	}
	return value, nil
}