// Package diff implements "Diff" that compares two JSON objects and
// generates Deltas that describes differences between them. The package also
// provides "Patch" that apply Deltas to a JSON object.
//
// Updated copy from https://github.com/yudai/gojsondiff.
package diff

import (
	"container/list"
	"encoding/json"
	"reflect"
	"sort"

	"github.com/bool64/shared"
	dmp "github.com/sergi/go-diff/diffmatchpatch"
	lcs "github.com/yudai/golcs"
)

// A Diff holds deltas generated by a Differ.
type Diff interface {
	// Deltas returns Deltas that describe differences between two JSON objects
	Deltas() []Delta
	// Modified returns true if Diff has at least one Delta.
	Modified() bool
}

type diff struct {
	deltas []Delta
}

func (diff *diff) Deltas() []Delta {
	return diff.deltas
}

func (diff *diff) Modified() bool {
	return len(diff.deltas) > 0
}

// A Differ compares JSON objects and apply patches.
type Differ struct {
	textDiffMinimumLength int
}

// New returns new Differ with default configuration.
func New() *Differ {
	return &Differ{
		textDiffMinimumLength: 30,
	}
}

// Compare compares two JSON strings as []bytes and return a Diff object.
func (differ *Differ) Compare(
	left []byte,
	right []byte,
) (Diff, error) {
	var leftMap, rightMap map[string]interface{}

	err := json.Unmarshal(left, &leftMap)
	if err != nil {
		return nil, err
	}

	err = json.Unmarshal(right, &rightMap)
	if err != nil {
		return nil, err
	}

	return differ.CompareObjects(leftMap, rightMap), nil
}

// CompareObjects compares two JSON object as map[string]interface{}
// and return a Diff object.
func (differ *Differ) CompareObjects(
	left map[string]interface{},
	right map[string]interface{},
) Diff {
	deltas := differ.compareMaps(left, right)

	return &diff{deltas: deltas}
}

// CompareArrays compares two JSON arrays as []interface{}
// and return a Diff object.
func (differ *Differ) CompareArrays(
	left []interface{},
	right []interface{},
) Diff {
	deltas := differ.compareArrays(left, right)

	return &diff{deltas: deltas}
}

func (differ *Differ) compareMaps(
	left map[string]interface{},
	right map[string]interface{},
) (deltas []Delta) {
	deltas = make([]Delta, 0)

	names := sortedKeys(left) // stabilize delta order
	for _, name := range names {
		if rightValue, ok := right[name]; ok {
			same, delta := differ.compareValues(Name(name), left[name], rightValue)
			if !same {
				deltas = append(deltas, delta)
			}
		} else {
			deltas = append(deltas, NewDeleted(Name(name), left[name]))
		}
	}

	names = sortedKeys(right) // stabilize delta order
	for _, name := range names {
		if _, ok := left[name]; !ok {
			deltas = append(deltas, NewAdded(Name(name), right[name]))
		}
	}

	return deltas
}

// ApplyPatch applies a Diff to an JSON object. This method is destructive.
func (differ *Differ) ApplyPatch(json map[string]interface{}, patch Diff) {
	applyDeltas(patch.Deltas(), json)
}

type maybe struct {
	index    int
	lcsIndex int
	item     interface{}
}

func (differ *Differ) compareArrays(
	left []interface{},
	right []interface{},
) (deltas []Delta) {
	deltas = make([]Delta, 0)
	// LCS index pairs
	lcsPairs := lcs.New(left, right).IndexPairs()

	// list up items not in LCS, they are maybe deleted
	maybeDeleted := list.New() // but maybe moved or modified

	lcsI := 0
	for i, leftValue := range left {
		if lcsI < len(lcsPairs) && lcsPairs[lcsI].Left == i {
			lcsI++
		} else {
			maybeDeleted.PushBack(maybe{index: i, lcsIndex: lcsI, item: leftValue})
		}
	}

	// list up items not in LCS, they are maybe Added
	maybeAdded := list.New() // but maybe moved or modified

	lcsI = 0
	for i, rightValue := range right {
		if lcsI < len(lcsPairs) && lcsPairs[lcsI].Right == i {
			lcsI++
		} else {
			maybeAdded.PushBack(maybe{index: i, lcsIndex: lcsI, item: rightValue})
		}
	}

	// find moved items
	var delNext *list.Element // for prefetch to remove item in iteration

	for delCandidate := maybeDeleted.Front(); delCandidate != nil; delCandidate = delNext {
		delCan := delCandidate.Value.(maybe)
		delNext = delCandidate.Next()

		for addCandidate := maybeAdded.Front(); addCandidate != nil; addCandidate = addCandidate.Next() {
			addCan := addCandidate.Value.(maybe)
			if reflect.DeepEqual(delCan.item, addCan.item) {
				deltas = append(deltas, NewMoved(Index(delCan.index), Index(addCan.index), delCan.item, nil))

				maybeAdded.Remove(addCandidate)
				maybeDeleted.Remove(delCandidate)

				break
			}
		}
	}

	// find modified or add+del
	prevIndexDel := 0
	prevIndexAdd := 0
	delElement := maybeDeleted.Front()
	addElement := maybeAdded.Front()

	for i := 0; i <= len(lcsPairs); i++ { // not "< len(lcsPairs)"
		var lcsPair lcs.IndexPair

		var delSize, addSize int

		if i < len(lcsPairs) {
			lcsPair = lcsPairs[i]
			delSize = lcsPair.Left - prevIndexDel - 1
			addSize = lcsPair.Right - prevIndexAdd - 1
			prevIndexDel = lcsPair.Left
			prevIndexAdd = lcsPair.Right
		}

		var delSlice []maybe
		if delSize > 0 {
			delSlice = make([]maybe, 0, delSize)
		} else {
			delSlice = make([]maybe, 0, maybeDeleted.Len())
		}

		for ; delElement != nil; delElement = delElement.Next() {
			d := delElement.Value.(maybe)
			if d.lcsIndex != i {
				break
			}

			delSlice = append(delSlice, d)
		}

		var addSlice []maybe
		if addSize > 0 {
			addSlice = make([]maybe, 0, addSize)
		} else {
			addSlice = make([]maybe, 0, maybeAdded.Len())
		}

		for ; addElement != nil; addElement = addElement.Next() {
			a := addElement.Value.(maybe)
			if a.lcsIndex != i {
				break
			}

			addSlice = append(addSlice, a)
		}

		if len(delSlice) > 0 && len(addSlice) > 0 {
			var bestDeltas []Delta
			bestDeltas, delSlice, addSlice = differ.maximizeSimilarities(delSlice, addSlice)

			deltas = append(deltas, bestDeltas...)
		}

		for _, del := range delSlice {
			deltas = append(deltas, NewDeleted(Index(del.index), del.item))
		}

		for _, add := range addSlice {
			deltas = append(deltas, NewAdded(Index(add.index), add.item))
		}
	}

	return deltas
}

func (differ *Differ) compareValues(
	position Position,
	left interface{},
	right interface{},
) (same bool, delta Delta) {
	if reflect.TypeOf(left) != reflect.TypeOf(right) {
		return false, NewModified(position, left, right)
	}

	switch l := left.(type) {
	case map[string]interface{}:
		childDeltas := differ.compareMaps(l, right.(map[string]interface{}))
		if len(childDeltas) > 0 {
			return false, NewObject(position, childDeltas)
		}

	case []interface{}:
		childDeltas := differ.compareArrays(l, right.([]interface{}))

		if len(childDeltas) > 0 {
			return false, NewArray(position, childDeltas)
		}

	default:
		if !reflect.DeepEqual(left, right) {
			if ln, ok := left.(json.Number); ok {
				left = shared.DecodeJSONNumber(ln)
			}

			if rn, ok := right.(json.Number); ok {
				right = shared.DecodeJSONNumber(rn)
			}

			if reflect.ValueOf(left).Kind() == reflect.String &&
				reflect.ValueOf(right).Kind() == reflect.String &&
				differ.textDiffMinimumLength <= len(reflect.ValueOf(left).String()) {
				textDiff := dmp.New()
				patches := textDiff.PatchMake(reflect.ValueOf(left).String(), reflect.ValueOf(right).String())

				return false, NewTextDiff(position, patches, left, right)
			}

			return false, NewModified(position, left, right)
		}
	}

	return true, nil
}

func applyDeltas(deltas []Delta, object interface{}) interface{} {
	preDeltas := make(preDeltas, 0)

	for _, delta := range deltas {
		if dt, ok := delta.(PreDelta); ok {
			preDeltas = append(preDeltas, dt)
		}
	}

	sort.Sort(preDeltas)

	for _, delta := range preDeltas {
		object = delta.PreApply(object)
	}

	postDeltas := make(postDeltas, 0, len(deltas)-len(preDeltas))

	for _, delta := range deltas {
		if dt, ok := delta.(PostDelta); ok {
			postDeltas = append(postDeltas, dt)
		}
	}

	sort.Sort(postDeltas)

	for _, delta := range postDeltas {
		object = delta.PostApply(object)
	}

	return object
}

func (differ *Differ) maximizeSimilarities(left []maybe, right []maybe) (resultDeltas []Delta, freeLeft, freeRight []maybe) {
	deltaTable := make([][]Delta, len(left))

	for i := 0; i < len(left); i++ {
		deltaTable[i] = make([]Delta, len(right))
	}

	for i, leftValue := range left {
		for j, rightValue := range right {
			_, delta := differ.compareValues(Index(rightValue.index), leftValue.item, rightValue.item)
			deltaTable[i][j] = delta
		}
	}

	sizeX := len(left) + 1 // margins for both sides
	sizeY := len(right) + 1

	// fill out with similarities
	dpTable := make([][]float64, sizeX)

	for i := 0; i < sizeX; i++ {
		dpTable[i] = make([]float64, sizeY)
	}

	for x := sizeX - 2; x >= 0; x-- {
		for y := sizeY - 2; y >= 0; y-- {
			prevX := dpTable[x+1][y]
			prevY := dpTable[x][y+1]
			score := deltaTable[x][y].Similarity() + dpTable[x+1][y+1]

			dpTable[x][y] = maxFloat(prevX, prevY, score)
		}
	}

	minLength := len(left)

	if minLength > len(right) {
		minLength = len(right)
	}

	maxInvalidLength := minLength - 1

	freeLeft = make([]maybe, 0, len(left)-minLength)
	freeRight = make([]maybe, 0, len(right)-minLength)

	resultDeltas = make([]Delta, 0, minLength)

	var x, y int

	for x, y = 0, 0; x <= sizeX-2 && y <= sizeY-2; {
		current := dpTable[x][y]
		nextX := dpTable[x+1][y]
		nextY := dpTable[x][y+1]

		xValidLength := len(left) - maxInvalidLength + y
		yValidLength := len(right) - maxInvalidLength + x

		switch {
		case x+1 < xValidLength && current == nextX:
			freeLeft = append(freeLeft, left[x])
			x++
		case y+1 < yValidLength && current == nextY:
			freeRight = append(freeRight, right[y])
			y++
		default:
			resultDeltas = append(resultDeltas, deltaTable[x][y])
			x++
			y++
		}
	}

	for ; x < sizeX-1; x++ {
		freeLeft = append(freeLeft, left[x-1])
	}

	for ; y < sizeY-1; y++ {
		freeRight = append(freeRight, right[y-1])
	}

	return resultDeltas, freeLeft, freeRight
}

func deltasSimilarity(deltas []Delta) float64 {
	similarity := float64(0)

	for _, delta := range deltas {
		similarity += delta.Similarity()
	}

	similarity /= float64(len(deltas))

	return similarity
}

func stringSimilarity(left, right string) (similarity float64) {
	matchingLength := float64(
		lcs.New(
			stringToInterfaceSlice(left),
			stringToInterfaceSlice(right),
		).Length(),
	)
	similarity = (matchingLength / float64(len(left))) * (matchingLength / float64(len(right)))

	return similarity
}

func stringToInterfaceSlice(str string) []interface{} {
	s := make([]interface{}, len(str))
	for i, v := range str {
		s[i] = v
	}

	return s
}

func sortedKeys(m map[string]interface{}) (keys []string) {
	keys = make([]string, 0, len(m))
	for key := range m {
		keys = append(keys, key)
	}

	sort.Strings(keys)

	return keys
}

func maxFloat(first float64, rest ...float64) float64 {
	m := first
	for _, value := range rest {
		if m < value {
			m = value
		}
	}

	return m
}
