// Copyright © 2016 Abcum Ltd
//
// 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 diff

import (
	"fmt"
	"reflect"
	"sort"
	"strings"

	"github.com/fatih/structs"
	"github.com/sergi/go-diff/diffmatchpatch"
)

type Operation struct {
	Op     string      `cork:"op,omietmpty" json:"op,omietmpty" structs:"op,omitempty"`
	From   string      `cork:"from,omitempty" json:"from,omitempty" structs:"from,omitempty"`
	Path   string      `cork:"path,omitempty" json:"path,omitempty" structs:"path,omitempty"`
	Value  interface{} `cork:"value,omitempty" json:"value,omitempty" structs:"value,omitempty"`
	Before interface{} `cork:"-" json:"-" structs:"-"`
}

type Operations struct {
	Ops []*Operation
}

func Diff(old, now map[string]interface{}) (ops *Operations) {

	ops = &Operations{}

	ops.diff(old, now, "")

	return

}

func (o *Operations) Patch(old map[string]interface{}) (now map[string]interface{}, err error) {
	return nil, nil
}

func (o *Operations) Rebase(other *Operations) (ops *Operations, err error) {
	return nil, nil
}

func (o *Operations) Out() (ops []map[string]interface{}) {

	for _, v := range o.Ops {
		ops = append(ops, structs.Map(v))
	}

	return

}

func route(path string, part interface{}) string {
	if path == "" {
		return fmt.Sprintf("/%v", part)
	} else {
		if strings.HasSuffix(path, "/") {
			return path + fmt.Sprintf("%v", part)
		} else {
			return path + fmt.Sprintf("/%v", part)
		}
	}
}

func (o *Operations) op(op, from, path string, before, after interface{}) {

	o.Ops = append(o.Ops, &Operation{
		Op:     op,
		From:   from,
		Path:   path,
		Value:  after,
		Before: before,
	})

}

func (o *Operations) diff(old, now map[string]interface{}, path string) {

	for key, after := range now {

		p := route(path, key)

		// Check if the value existed
		before, ok := old[key]

		// Value did not previously exist
		if !ok {
			o.op("add", "", p, nil, after)
			continue
		}

		// Data type is now completely different
		if reflect.TypeOf(after) != reflect.TypeOf(before) {
			o.op("replace", "", p, before, after)
			continue
		}

		// Check whether the values have changed
		o.vals(before, after, p)

	}

	for key, before := range old {

		p := route(path, key)

		// Check if the value exists
		after, ok := now[key]

		// Value now no longer exists
		if !ok {
			o.op("remove", "", p, before, after)
			continue
		}

	}

	var used []int

	for i := len(o.Ops) - 1; i >= 0; i-- {
		if iv := o.Ops[i]; !isIn(i, used) && iv.Op == "add" {
			for j := len(o.Ops) - 1; j >= 0; j-- {
				if jv := o.Ops[j]; !isIn(j, used) && jv.Op == "remove" {
					if reflect.DeepEqual(iv.Value, jv.Before) {
						used = append(used, []int{i, j}...)
						o.op("move", jv.Path, iv.Path, nil, nil)
					}
				}
			}
		}
	}

	sort.Sort(sort.Reverse(sort.IntSlice(used)))

	for _, i := range used {
		o.Ops = append(o.Ops[:i], o.Ops[i+1:]...)
	}

}

func isIn(a int, list []int) bool {
	for _, b := range list {
		if b == a {
			return true
		}
	}
	return false
}

func (o *Operations) text(old, now string, path string) {

	dmp := diffmatchpatch.New()

	dif := dmp.DiffMain(old, now, false)

	txt := dmp.DiffToDelta(dif)

	o.op("change", "", path, old, txt)

}

func (o *Operations) vals(old, now interface{}, path string) {

	if reflect.TypeOf(old) != reflect.TypeOf(now) {
		o.op("replace", "", path, old, now)
		return
	}

	switch ov := old.(type) {
	default:
		if !reflect.DeepEqual(old, now) {
			o.op("replace", "", path, old, now)
		}
	case bool:
		if ov != now.(bool) {
			o.op("replace", "", path, old, now)
		}
	case int64:
		if ov != now.(int64) {
			o.op("replace", "", path, old, now)
		}
	case float64:
		if ov != now.(float64) {
			o.op("replace", "", path, old, now)
		}
	case string:
		if ov != now.(string) {
			o.text(ov, now.(string), path)
		}
	case nil:
		switch now.(type) {
		case nil:
		default:
			o.op("replace", "", path, old, now)
		}
	case map[string]interface{}:
		o.diff(ov, now.(map[string]interface{}), path)
	case []interface{}:
		o.arrs(ov, now.([]interface{}), path)
	}

}

func (o *Operations) arrs(old, now []interface{}, path string) {

	var i int

	for i = 0; i < len(old) && i < len(now); i++ {
		o.vals(old[i], now[i], route(path, i))
	}

	for j := i; j < len(now); j++ {
		if j >= len(old) || !reflect.DeepEqual(now[j], old[j]) {
			o.op("add", "", route(path, j), nil, now[j])
		}
	}

	for j := i; j < len(old); j++ {
		if j >= len(now) || !reflect.DeepEqual(old[j], now[j]) {
			o.op("remove", "", route(path, j), old[j], nil)
		}
	}

}