// Copyright 2019, The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package cmp

import (
	"fmt"
	"reflect"

	"github.com/google/go-cmp/cmp/internal/value"
)

// numContextRecords is the number of surrounding equal records to print.
const numContextRecords = 2

type diffMode byte

const (
	diffUnknown   diffMode = 0
	diffIdentical diffMode = ' '
	diffRemoved   diffMode = '-'
	diffInserted  diffMode = '+'
)

type typeMode int

const (
	// emitType always prints the type.
	emitType typeMode = iota
	// elideType never prints the type.
	elideType
	// autoType prints the type only for composite kinds
	// (i.e., structs, slices, arrays, and maps).
	autoType
)

type formatOptions struct {
	// DiffMode controls the output mode of FormatDiff.
	//
	// If diffUnknown,   then produce a diff of the x and y values.
	// If diffIdentical, then emit values as if they were equal.
	// If diffRemoved,   then only emit x values (ignoring y values).
	// If diffInserted,  then only emit y values (ignoring x values).
	DiffMode diffMode

	// TypeMode controls whether to print the type for the current node.
	//
	// As a general rule of thumb, we always print the type of the next node
	// after an interface, and always elide the type of the next node after
	// a slice or map node.
	TypeMode typeMode

	// formatValueOptions are options specific to printing reflect.Values.
	formatValueOptions
}

func (opts formatOptions) WithDiffMode(d diffMode) formatOptions {
	opts.DiffMode = d
	return opts
}
func (opts formatOptions) WithTypeMode(t typeMode) formatOptions {
	opts.TypeMode = t
	return opts
}
func (opts formatOptions) WithVerbosity(level int) formatOptions {
	opts.VerbosityLevel = level
	opts.LimitVerbosity = true
	return opts
}
func (opts formatOptions) verbosity() uint {
	switch {
	case opts.VerbosityLevel < 0:
		return 0
	case opts.VerbosityLevel > 16:
		return 16 // some reasonable maximum to avoid shift overflow
	default:
		return uint(opts.VerbosityLevel)
	}
}

const maxVerbosityPreset = 3

// verbosityPreset modifies the verbosity settings given an index
// between 0 and maxVerbosityPreset, inclusive.
func verbosityPreset(opts formatOptions, i int) formatOptions {
	opts.VerbosityLevel = int(opts.verbosity()) + 2*i
	if i > 0 {
		opts.AvoidStringer = true
	}
	if i >= maxVerbosityPreset {
		opts.PrintAddresses = true
		opts.QualifiedNames = true
	}
	return opts
}

// FormatDiff converts a valueNode tree into a textNode tree, where the later
// is a textual representation of the differences detected in the former.
func (opts formatOptions) FormatDiff(v *valueNode, ptrs *pointerReferences) (out textNode) {
	if opts.DiffMode == diffIdentical {
		opts = opts.WithVerbosity(1)
	} else {
		opts = opts.WithVerbosity(3)
	}

	// Check whether we have specialized formatting for this node.
	// This is not necessary, but helpful for producing more readable outputs.
	if opts.CanFormatDiffSlice(v) {
		return opts.FormatDiffSlice(v)
	}

	var parentKind reflect.Kind
	if v.parent != nil && v.parent.TransformerName == "" {
		parentKind = v.parent.Type.Kind()
	}

	// For leaf nodes, format the value based on the reflect.Values alone.
	if v.MaxDepth == 0 {
		switch opts.DiffMode {
		case diffUnknown, diffIdentical:
			// Format Equal.
			if v.NumDiff == 0 {
				outx := opts.FormatValue(v.ValueX, parentKind, ptrs)
				outy := opts.FormatValue(v.ValueY, parentKind, ptrs)
				if v.NumIgnored > 0 && v.NumSame == 0 {
					return textEllipsis
				} else if outx.Len() < outy.Len() {
					return outx
				} else {
					return outy
				}
			}

			// Format unequal.
			assert(opts.DiffMode == diffUnknown)
			var list textList
			outx := opts.WithTypeMode(elideType).FormatValue(v.ValueX, parentKind, ptrs)
			outy := opts.WithTypeMode(elideType).FormatValue(v.ValueY, parentKind, ptrs)
			for i := 0; i <= maxVerbosityPreset && outx != nil && outy != nil && outx.Equal(outy); i++ {
				opts2 := verbosityPreset(opts, i).WithTypeMode(elideType)
				outx = opts2.FormatValue(v.ValueX, parentKind, ptrs)
				outy = opts2.FormatValue(v.ValueY, parentKind, ptrs)
			}
			if outx != nil {
				list = append(list, textRecord{Diff: '-', Value: outx})
			}
			if outy != nil {
				list = append(list, textRecord{Diff: '+', Value: outy})
			}
			return opts.WithTypeMode(emitType).FormatType(v.Type, list)
		case diffRemoved:
			return opts.FormatValue(v.ValueX, parentKind, ptrs)
		case diffInserted:
			return opts.FormatValue(v.ValueY, parentKind, ptrs)
		default:
			panic("invalid diff mode")
		}
	}

	// Register slice element to support cycle detection.
	if parentKind == reflect.Slice {
		ptrRefs := ptrs.PushPair(v.ValueX, v.ValueY, opts.DiffMode, true)
		defer ptrs.Pop()
		defer func() { out = wrapTrunkReferences(ptrRefs, out) }()
	}

	// Descend into the child value node.
	if v.TransformerName != "" {
		out := opts.WithTypeMode(emitType).FormatDiff(v.Value, ptrs)
		out = &textWrap{Prefix: "Inverse(" + v.TransformerName + ", ", Value: out, Suffix: ")"}
		return opts.FormatType(v.Type, out)
	} else {
		switch k := v.Type.Kind(); k {
		case reflect.Struct, reflect.Array, reflect.Slice:
			out = opts.formatDiffList(v.Records, k, ptrs)
			out = opts.FormatType(v.Type, out)
		case reflect.Map:
			// Register map to support cycle detection.
			ptrRefs := ptrs.PushPair(v.ValueX, v.ValueY, opts.DiffMode, false)
			defer ptrs.Pop()

			out = opts.formatDiffList(v.Records, k, ptrs)
			out = wrapTrunkReferences(ptrRefs, out)
			out = opts.FormatType(v.Type, out)
		case reflect.Ptr:
			// Register pointer to support cycle detection.
			ptrRefs := ptrs.PushPair(v.ValueX, v.ValueY, opts.DiffMode, false)
			defer ptrs.Pop()

			out = opts.FormatDiff(v.Value, ptrs)
			out = wrapTrunkReferences(ptrRefs, out)
			out = &textWrap{Prefix: "&", Value: out}
		case reflect.Interface:
			out = opts.WithTypeMode(emitType).FormatDiff(v.Value, ptrs)
		default:
			panic(fmt.Sprintf("%v cannot have children", k))
		}
		return out
	}
}

func (opts formatOptions) formatDiffList(recs []reportRecord, k reflect.Kind, ptrs *pointerReferences) textNode {
	// Derive record name based on the data structure kind.
	var name string
	var formatKey func(reflect.Value) string
	switch k {
	case reflect.Struct:
		name = "field"
		opts = opts.WithTypeMode(autoType)
		formatKey = func(v reflect.Value) string { return v.String() }
	case reflect.Slice, reflect.Array:
		name = "element"
		opts = opts.WithTypeMode(elideType)
		formatKey = func(reflect.Value) string { return "" }
	case reflect.Map:
		name = "entry"
		opts = opts.WithTypeMode(elideType)
		formatKey = func(v reflect.Value) string { return formatMapKey(v, false, ptrs) }
	}

	maxLen := -1
	if opts.LimitVerbosity {
		if opts.DiffMode == diffIdentical {
			maxLen = ((1 << opts.verbosity()) >> 1) << 2 // 0, 4, 8, 16, 32, etc...
		} else {
			maxLen = (1 << opts.verbosity()) << 1 // 2, 4, 8, 16, 32, 64, etc...
		}
		opts.VerbosityLevel--
	}

	// Handle unification.
	switch opts.DiffMode {
	case diffIdentical, diffRemoved, diffInserted:
		var list textList
		var deferredEllipsis bool // Add final "..." to indicate records were dropped
		for _, r := range recs {
			if len(list) == maxLen {
				deferredEllipsis = true
				break
			}

			// Elide struct fields that are zero value.
			if k == reflect.Struct {
				var isZero bool
				switch opts.DiffMode {
				case diffIdentical:
					isZero = value.IsZero(r.Value.ValueX) || value.IsZero(r.Value.ValueY)
				case diffRemoved:
					isZero = value.IsZero(r.Value.ValueX)
				case diffInserted:
					isZero = value.IsZero(r.Value.ValueY)
				}
				if isZero {
					continue
				}
			}
			// Elide ignored nodes.
			if r.Value.NumIgnored > 0 && r.Value.NumSame+r.Value.NumDiff == 0 {
				deferredEllipsis = !(k == reflect.Slice || k == reflect.Array)
				if !deferredEllipsis {
					list.AppendEllipsis(diffStats{})
				}
				continue
			}
			if out := opts.FormatDiff(r.Value, ptrs); out != nil {
				list = append(list, textRecord{Key: formatKey(r.Key), Value: out})
			}
		}
		if deferredEllipsis {
			list.AppendEllipsis(diffStats{})
		}
		return &textWrap{Prefix: "{", Value: list, Suffix: "}"}
	case diffUnknown:
	default:
		panic("invalid diff mode")
	}

	// Handle differencing.
	var numDiffs int
	var list textList
	var keys []reflect.Value // invariant: len(list) == len(keys)
	groups := coalesceAdjacentRecords(name, recs)
	maxGroup := diffStats{Name: name}
	for i, ds := range groups {
		if maxLen >= 0 && numDiffs >= maxLen {
			maxGroup = maxGroup.Append(ds)
			continue
		}

		// Handle equal records.
		if ds.NumDiff() == 0 {
			// Compute the number of leading and trailing records to print.
			var numLo, numHi int
			numEqual := ds.NumIgnored + ds.NumIdentical
			for numLo < numContextRecords && numLo+numHi < numEqual && i != 0 {
				if r := recs[numLo].Value; r.NumIgnored > 0 && r.NumSame+r.NumDiff == 0 {
					break
				}
				numLo++
			}
			for numHi < numContextRecords && numLo+numHi < numEqual && i != len(groups)-1 {
				if r := recs[numEqual-numHi-1].Value; r.NumIgnored > 0 && r.NumSame+r.NumDiff == 0 {
					break
				}
				numHi++
			}
			if numEqual-(numLo+numHi) == 1 && ds.NumIgnored == 0 {
				numHi++ // Avoid pointless coalescing of a single equal record
			}

			// Format the equal values.
			for _, r := range recs[:numLo] {
				out := opts.WithDiffMode(diffIdentical).FormatDiff(r.Value, ptrs)
				list = append(list, textRecord{Key: formatKey(r.Key), Value: out})
				keys = append(keys, r.Key)
			}
			if numEqual > numLo+numHi {
				ds.NumIdentical -= numLo + numHi
				list.AppendEllipsis(ds)
				for len(keys) < len(list) {
					keys = append(keys, reflect.Value{})
				}
			}
			for _, r := range recs[numEqual-numHi : numEqual] {
				out := opts.WithDiffMode(diffIdentical).FormatDiff(r.Value, ptrs)
				list = append(list, textRecord{Key: formatKey(r.Key), Value: out})
				keys = append(keys, r.Key)
			}
			recs = recs[numEqual:]
			continue
		}

		// Handle unequal records.
		for _, r := range recs[:ds.NumDiff()] {
			switch {
			case opts.CanFormatDiffSlice(r.Value):
				out := opts.FormatDiffSlice(r.Value)
				list = append(list, textRecord{Key: formatKey(r.Key), Value: out})
				keys = append(keys, r.Key)
			case r.Value.NumChildren == r.Value.MaxDepth:
				outx := opts.WithDiffMode(diffRemoved).FormatDiff(r.Value, ptrs)
				outy := opts.WithDiffMode(diffInserted).FormatDiff(r.Value, ptrs)
				for i := 0; i <= maxVerbosityPreset && outx != nil && outy != nil && outx.Equal(outy); i++ {
					opts2 := verbosityPreset(opts, i)
					outx = opts2.WithDiffMode(diffRemoved).FormatDiff(r.Value, ptrs)
					outy = opts2.WithDiffMode(diffInserted).FormatDiff(r.Value, ptrs)
				}
				if outx != nil {
					list = append(list, textRecord{Diff: diffRemoved, Key: formatKey(r.Key), Value: outx})
					keys = append(keys, r.Key)
				}
				if outy != nil {
					list = append(list, textRecord{Diff: diffInserted, Key: formatKey(r.Key), Value: outy})
					keys = append(keys, r.Key)
				}
			default:
				out := opts.FormatDiff(r.Value, ptrs)
				list = append(list, textRecord{Key: formatKey(r.Key), Value: out})
				keys = append(keys, r.Key)
			}
		}
		recs = recs[ds.NumDiff():]
		numDiffs += ds.NumDiff()
	}
	if maxGroup.IsZero() {
		assert(len(recs) == 0)
	} else {
		list.AppendEllipsis(maxGroup)
		for len(keys) < len(list) {
			keys = append(keys, reflect.Value{})
		}
	}
	assert(len(list) == len(keys))

	// For maps, the default formatting logic uses fmt.Stringer which may
	// produce ambiguous output. Avoid calling String to disambiguate.
	if k == reflect.Map {
		var ambiguous bool
		seenKeys := map[string]reflect.Value{}
		for i, currKey := range keys {
			if currKey.IsValid() {
				strKey := list[i].Key
				prevKey, seen := seenKeys[strKey]
				if seen && prevKey.CanInterface() && currKey.CanInterface() {
					ambiguous = prevKey.Interface() != currKey.Interface()
					if ambiguous {
						break
					}
				}
				seenKeys[strKey] = currKey
			}
		}
		if ambiguous {
			for i, k := range keys {
				if k.IsValid() {
					list[i].Key = formatMapKey(k, true, ptrs)
				}
			}
		}
	}

	return &textWrap{Prefix: "{", Value: list, Suffix: "}"}
}

// coalesceAdjacentRecords coalesces the list of records into groups of
// adjacent equal, or unequal counts.
func coalesceAdjacentRecords(name string, recs []reportRecord) (groups []diffStats) {
	var prevCase int // Arbitrary index into which case last occurred
	lastStats := func(i int) *diffStats {
		if prevCase != i {
			groups = append(groups, diffStats{Name: name})
			prevCase = i
		}
		return &groups[len(groups)-1]
	}
	for _, r := range recs {
		switch rv := r.Value; {
		case rv.NumIgnored > 0 && rv.NumSame+rv.NumDiff == 0:
			lastStats(1).NumIgnored++
		case rv.NumDiff == 0:
			lastStats(1).NumIdentical++
		case rv.NumDiff > 0 && !rv.ValueY.IsValid():
			lastStats(2).NumRemoved++
		case rv.NumDiff > 0 && !rv.ValueX.IsValid():
			lastStats(2).NumInserted++
		default:
			lastStats(2).NumModified++
		}
	}
	return groups
}
