// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package tftypes

import (
	"errors"
	"fmt"
	"math/big"
)

// ValueDiff expresses a subset of a Value that is different between two
// Values. The Path property indicates where the subset is located within the
// Value, and Value1 and Value2 indicate what the subset is in each of the
// Values. If the Value does not contain a subset at that AttributePath, its
// Value will be nil. This is distinct from a Value with a nil in it (a "null"
// value), which is present in the Value.
type ValueDiff struct {
	// The Path these different subsets are located at in the original
	// Values.
	Path *AttributePath

	// The subset of the first Value passed to Diff found at the
	// AttributePath indicated by Path.
	Value1 *Value

	// The subset of the second Value passed to Diff found at the
	// AttributePath indicated by Path.
	Value2 *Value
}

func (v ValueDiff) String() string {
	val1 := "{no value set}"
	if v.Value1 != nil {
		val1 = v.Value1.String()
	}
	val2 := "{no value set}"
	if v.Value2 != nil {
		val2 = v.Value2.String()
	}
	return fmt.Sprintf("%s: value1: %s, value2: %s",
		v.Path.String(), val1, val2)
}

// Equal returns whether two ValueDiffs should be considered equal or not.
// ValueDiffs are consisdered equal when their Path, Value1, and Value2
// properties are considered equal.
func (v ValueDiff) Equal(o ValueDiff) bool {
	if !v.Path.Equal(o.Path) {
		return false
	}
	if v.Value1 == nil && o.Value1 != nil {
		return false
	}
	if v.Value1 != nil && o.Value1 == nil {
		return false
	}
	if v.Value1 != nil && o.Value1 != nil && !v.Value1.Equal(*o.Value1) {
		return false
	}
	if v.Value2 == nil && o.Value2 != nil {
		return false
	}
	if v.Value2 != nil && o.Value2 == nil {
		return false
	}
	if v.Value2 != nil && o.Value2 != nil && !v.Value2.Equal(*o.Value2) {
		return false
	}
	return true
}

// Diff computes the differences between `val1` and `val2` and surfaces them as
// a slice of ValueDiffs. The ValueDiffs in the struct will use `val1`'s values
// as Value1 and `val2`'s values as Value2. An empty or nil slice means the two
// Values can be considered equal. Values must be the same type when passed to
// Diff; passing in Values of two different types will result in an error. If
// both Values are empty, they are considered equal. If one Value is missing
// type, it will result in an error. val1.Type().Is(val2.Type()) is a safe way
// to check that Values can be compared with Diff.
func (val1 Value) Diff(val2 Value) ([]ValueDiff, error) {
	var diffs []ValueDiff

	if val1.Type() == nil && val2.Type() == nil && val1.value == nil && val2.value == nil {
		return diffs, nil
	}
	if (val1.Type() == nil && val2.Type() != nil) || (val1.Type() != nil && val2.Type() == nil) {
		return nil, errors.New("cannot diff value missing type")
	}
	if !val1.Type().Is(val2.Type()) {
		return nil, errors.New("Can't diff values of different types")
	}

	// make sure everything in val2 is also in val1
	err := Walk(val2, func(path *AttributePath, value2 Value) (bool, error) {
		_, _, err := WalkAttributePath(val1, path)
		if err != nil && err != ErrInvalidStep {
			return false, fmt.Errorf("Error walking %q: %w", path, err)
		} else if err == ErrInvalidStep {
			diffs = append(diffs, ValueDiff{
				Path:   path,
				Value1: nil,
				Value2: &value2,
			})
			return false, nil
		}
		return true, nil
	})
	if err != nil {
		return nil, err
	}

	// make sure everything in val1 is also in val2 and also that it all matches
	err = Walk(val1, func(path *AttributePath, value1 Value) (bool, error) {
		// pull out the Value at the same path in val2
		value2I, _, err := WalkAttributePath(val2, path)
		if err != nil && err != ErrInvalidStep {
			return false, fmt.Errorf("Error walking %q: %w", path, err)
		} else if err == ErrInvalidStep {
			diffs = append(diffs, ValueDiff{
				Path:   path,
				Value1: &value1,
				Value2: nil,
			})
			return true, nil
		}

		value2, ok := value2I.(Value)
		if !ok {
			return false, fmt.Errorf("unexpected type %T in Diff", value2I)
		}

		// if they're both unknown, no need to continue
		if !value1.IsKnown() && !value2.IsKnown() {
			return false, nil
		}

		// if val1 is unknown and val2 not, we have a diff
		// no need to continue to recurse into val1, no further to go
		if !value1.IsKnown() && value2.IsKnown() {
			diffs = append(diffs, ValueDiff{
				Path:   path,
				Value1: &value1,
				Value2: &value2,
			})
			return false, nil
		}

		// if val2 is unknown and val1 not, we have a diff
		// continue to recurse though, so we can surface the elements of val1
		// that are now "missing" as diffs
		if value1.IsKnown() && !value2.IsKnown() {
			diffs = append(diffs, ValueDiff{
				Path:   path,
				Value1: &value1,
				Value2: &value2,
			})
			return true, nil
		}

		// if they're both null, no need to continue
		if value1.IsNull() && value2.IsNull() {
			return false, nil
		}

		// if val1 is null and val2 not, we have a diff
		// no need to continue to recurse into val1, no further to go
		if value1.IsNull() && !value2.IsNull() {
			diffs = append(diffs, ValueDiff{
				Path:   path,
				Value1: &value1,
				Value2: &value2,
			})
			return false, nil
		}

		// if val2 is null and val1 not, we have a diff
		// continue to recurse though, so we can surface the elements of val1
		// that are now "missing" as diffs
		if !value1.IsNull() && value2.IsNull() {
			diffs = append(diffs, ValueDiff{
				Path:   path,
				Value1: &value1,
				Value2: &value2,
			})
			return true, nil
		}

		// we know there are known, non-null values, time to compare them
		switch {
		case value1.Type().Is(String):
			var s1, s2 string
			err := value1.As(&s1)
			if err != nil {
				return false, fmt.Errorf("Error converting %s (value1) at %q: %w", value1, path, err)
			}
			err = value2.As(&s2)
			if err != nil {
				return false, fmt.Errorf("Error converting %s (value2) at %q: %w", value2, path, err)
			}
			if s1 != s2 {
				diffs = append(diffs, ValueDiff{
					Path:   path,
					Value1: &value1,
					Value2: &value2,
				})
			}
			return false, nil
		case value1.Type().Is(Number):
			n1, n2 := big.NewFloat(0), big.NewFloat(0)
			err := value1.As(&n1)
			if err != nil {
				return false, fmt.Errorf("Error converting %q: %w", path, err)
			}
			err = value2.As(&n2)
			if err != nil {
				return false, fmt.Errorf("Error converting %q: %w", path, err)
			}
			if n1.Cmp(n2) != 0 {
				diffs = append(diffs, ValueDiff{
					Path:   path,
					Value1: &value1,
					Value2: &value2,
				})
			}
			return false, nil
		case value1.Type().Is(Bool):
			var b1, b2 bool
			err := value1.As(&b1)
			if err != nil {
				return false, fmt.Errorf("Error converting %q: %w", path, err)
			}
			err = value2.As(&b2)
			if err != nil {
				return false, fmt.Errorf("Error converting %q: %w", path, err)
			}
			if b1 != b2 {
				diffs = append(diffs, ValueDiff{
					Path:   path,
					Value1: &value1,
					Value2: &value2,
				})
			}
			return false, nil
		case value1.Type().Is(List{}), value1.Type().Is(Set{}), value1.Type().Is(Tuple{}):
			var s1, s2 []Value
			err := value1.As(&s1)
			if err != nil {
				return false, fmt.Errorf("Error converting %q: %w", path, err)
			}
			err = value2.As(&s2)
			if err != nil {
				return false, fmt.Errorf("Error converting %q: %w", path, err)
			}
			// we only care about if the lengths match for lists,
			// sets, and tuples. If any of the elements differ,
			// the recursion of the walk will find them for us.
			if len(s1) != len(s2) {
				diffs = append(diffs, ValueDiff{
					Path:   path,
					Value1: &value1,
					Value2: &value2,
				})
				return true, nil
			}
			return true, nil
		case value1.Type().Is(Map{}), value1.Type().Is(Object{}):
			m1 := map[string]Value{}
			m2 := map[string]Value{}
			err := value1.As(&m1)
			if err != nil {
				return false, fmt.Errorf("Error converting %q: %w", path, err)
			}
			err = value2.As(&m2)
			if err != nil {
				return false, fmt.Errorf("Error converting %q: %w", path, err)
			}
			// we need maps and objects to have the same exact keys
			// as each other
			if len(m1) != len(m2) {
				diffs = append(diffs, ValueDiff{
					Path:   path,
					Value1: &value1,
					Value2: &value2,
				})
				return true, nil
			}
			// if we have the same keys, we can just let recursion
			// from the walk check the sub-values match
			return true, nil
		case value1.Type().Is(DynamicPseudoType):
			// Let recursion from the walk check the sub-values match
			return true, nil
		}
		return false, fmt.Errorf("unexpected type %v in Diff at %s", value1.Type(), path)
	})
	return diffs, err
}
