package dashdiffs

import (
	"bytes"
	"errors"
	"fmt"
	"html/template"
	"sort"

	diff "github.com/yudai/gojsondiff"
)

type ChangeType int

const (
	ChangeNil ChangeType = iota
	ChangeAdded
	ChangeDeleted
	ChangeOld
	ChangeNew
	ChangeUnchanged
)

var (
	// changeTypeToSymbol is used for populating the terminating character in
	// the diff
	changeTypeToSymbol = map[ChangeType]string{
		ChangeNil:     "",
		ChangeAdded:   "+",
		ChangeDeleted: "-",
		ChangeOld:     "-",
		ChangeNew:     "+",
	}

	// changeTypeToName is used for populating class names in the diff
	changeTypeToName = map[ChangeType]string{
		ChangeNil:     "same",
		ChangeAdded:   "added",
		ChangeDeleted: "deleted",
		ChangeOld:     "old",
		ChangeNew:     "new",
	}
)

var (
	// tplJSONDiffWrapper is the template that wraps a diff
	tplJSONDiffWrapper = `{{ define "JSONDiffWrapper" -}}
	{{ range $index, $element := . }}
		{{ template "JSONDiffLine" $element }}
	{{ end }}
{{ end }}`

	// tplJSONDiffLine is the template that prints each line in a diff
	tplJSONDiffLine = `{{ define "JSONDiffLine" -}}
<p id="l{{ .LineNum }}" class="diff-line diff-json-{{ cton .Change }}">
	<span class="diff-line-number">
		{{if .LeftLine }}{{ .LeftLine }}{{ end }}
	</span>
	<span class="diff-line-number">
		{{if .RightLine }}{{ .RightLine }}{{ end }}
	</span>
	<span class="diff-value diff-indent-{{ .Indent }}" title="{{ .Text }}" ng-non-bindable>
		{{ .Text }}
	</span>
	<span class="diff-line-icon">{{ ctos .Change }}</span>
</p>
{{ end }}`
)

var diffTplFuncs = template.FuncMap{
	"ctos": func(c ChangeType) string {
		if symbol, ok := changeTypeToSymbol[c]; ok {
			return symbol
		}
		return ""
	},
	"cton": func(c ChangeType) string {
		if name, ok := changeTypeToName[c]; ok {
			return name
		}
		return ""
	},
}

// JSONLine contains the data required to render each line of the JSON diff
// and contains the data required to produce the tokens output in the basic
// diff.
type JSONLine struct {
	LineNum   int         `json:"line"`
	LeftLine  int         `json:"leftLine"`
	RightLine int         `json:"rightLine"`
	Indent    int         `json:"indent"`
	Text      string      `json:"text"`
	Change    ChangeType  `json:"changeType"`
	Key       string      `json:"key"`
	Val       interface{} `json:"value"`
}

func NewJSONFormatter(left interface{}) *JSONFormatter {
	tpl := template.Must(template.New("JSONDiffWrapper").Funcs(diffTplFuncs).Parse(tplJSONDiffWrapper))
	tpl = template.Must(tpl.New("JSONDiffLine").Funcs(diffTplFuncs).Parse(tplJSONDiffLine))

	return &JSONFormatter{
		left:      left,
		Lines:     []*JSONLine{},
		tpl:       tpl,
		path:      []string{},
		size:      []int{},
		lineCount: 0,
		inArray:   []bool{},
	}
}

type JSONFormatter struct {
	left      interface{}
	path      []string
	size      []int
	inArray   []bool
	lineCount int
	leftLine  int
	rightLine int
	line      *AsciiLine
	Lines     []*JSONLine
	tpl       *template.Template
}

type AsciiLine struct {
	// the type of change
	change ChangeType

	// the actual changes - no formatting
	key string
	val interface{}

	// level of indentation for the current line
	indent int

	// buffer containing the fully formatted line
	buffer *bytes.Buffer
}

func (f *JSONFormatter) Format(diff diff.Diff) (result string, err error) {
	if v, ok := f.left.(map[string]interface{}); ok {
		if err := f.formatObject(v, diff); err != nil {
			return "", err
		}
	} else if v, ok := f.left.([]interface{}); ok {
		if err := f.formatArray(v, diff); err != nil {
			return "", err
		}
	} else {
		return "", fmt.Errorf("expected map[string]interface{} or []interface{}, got %T",
			f.left)
	}

	b := &bytes.Buffer{}
	err = f.tpl.ExecuteTemplate(b, "JSONDiffWrapper", f.Lines)
	if err != nil {
		fmt.Printf("%v\n", err)
		return "", err
	}

	return b.String(), nil
}

func (f *JSONFormatter) formatObject(left map[string]interface{}, df diff.Diff) error {
	f.addLineWith(ChangeNil, "{")
	f.push("ROOT", len(left), false)
	if err := f.processObject(left, df.Deltas()); err != nil {
		f.pop()
		return err
	}

	f.pop()
	f.addLineWith(ChangeNil, "}")
	return nil
}

func (f *JSONFormatter) formatArray(left []interface{}, df diff.Diff) error {
	f.addLineWith(ChangeNil, "[")
	f.push("ROOT", len(left), true)
	if err := f.processArray(left, df.Deltas()); err != nil {
		f.pop()
		return err
	}

	f.pop()
	f.addLineWith(ChangeNil, "]")
	return nil
}

func (f *JSONFormatter) processArray(array []interface{}, deltas []diff.Delta) error {
	patchedIndex := 0
	for index, value := range array {
		if err := f.processItem(value, deltas, diff.Index(index)); err != nil {
			return err
		}

		patchedIndex++
	}

	// additional Added
	for _, delta := range deltas {
		d, ok := delta.(*diff.Added)
		if ok {
			// skip items already processed
			if int(d.Position.(diff.Index)) < len(array) {
				continue
			}
			f.printRecursive(d.Position.String(), d.Value, ChangeAdded)
		}
	}

	return nil
}

func (f *JSONFormatter) processObject(object map[string]interface{}, deltas []diff.Delta) error {
	names := sortKeys(object)
	for _, name := range names {
		value := object[name]
		if err := f.processItem(value, deltas, diff.Name(name)); err != nil {
			return err
		}
	}

	// Added
	for _, delta := range deltas {
		d, ok := delta.(*diff.Added)
		if ok {
			f.printRecursive(d.Position.String(), d.Value, ChangeAdded)
		}
	}

	return nil
}

func (f *JSONFormatter) processItem(value interface{}, deltas []diff.Delta, position diff.Position) error {
	matchedDeltas := f.searchDeltas(deltas, position)
	positionStr := position.String()
	if len(matchedDeltas) > 0 {
		for _, matchedDelta := range matchedDeltas {
			switch matchedDelta := matchedDelta.(type) {
			case *diff.Object:
				switch value.(type) {
				case map[string]interface{}:
					// ok
				default:
					return errors.New("type mismatch")
				}
				o := value.(map[string]interface{})

				f.newLine(ChangeNil)
				f.printKey(positionStr)
				f.print("{")
				f.closeLine()
				f.push(positionStr, len(o), false)
				if err := f.processObject(o, matchedDelta.Deltas); err != nil {
					f.pop()
					return err
				}

				f.pop()
				f.newLine(ChangeNil)
				f.print("}")
				f.printComma()
				f.closeLine()

			case *diff.Array:
				switch value.(type) {
				case []interface{}:
					// ok
				default:
					return errors.New("type mismatch")
				}
				a := value.([]interface{})

				f.newLine(ChangeNil)
				f.printKey(positionStr)
				f.print("[")
				f.closeLine()
				f.push(positionStr, len(a), true)
				if err := f.processArray(a, matchedDelta.Deltas); err != nil {
					f.pop()
					return err
				}

				f.pop()
				f.newLine(ChangeNil)
				f.print("]")
				f.printComma()
				f.closeLine()

			case *diff.Added:
				f.printRecursive(positionStr, matchedDelta.Value, ChangeAdded)
				f.size[len(f.size)-1]++

			case *diff.Modified:
				savedSize := f.size[len(f.size)-1]
				f.printRecursive(positionStr, matchedDelta.OldValue, ChangeOld)
				f.size[len(f.size)-1] = savedSize
				f.printRecursive(positionStr, matchedDelta.NewValue, ChangeNew)

			case *diff.TextDiff:
				savedSize := f.size[len(f.size)-1]
				f.printRecursive(positionStr, matchedDelta.OldValue, ChangeOld)
				f.size[len(f.size)-1] = savedSize
				f.printRecursive(positionStr, matchedDelta.NewValue, ChangeNew)

			case *diff.Deleted:
				f.printRecursive(positionStr, matchedDelta.Value, ChangeDeleted)

			default:
				return errors.New("unknown Delta type detected")
			}
		}
	} else {
		f.printRecursive(positionStr, value, ChangeUnchanged)
	}

	return nil
}

func (f *JSONFormatter) searchDeltas(deltas []diff.Delta, position diff.Position) (results []diff.Delta) {
	results = make([]diff.Delta, 0)
	for _, delta := range deltas {
		switch typedDelta := delta.(type) {
		case diff.PostDelta:
			if typedDelta.PostPosition() == position {
				results = append(results, delta)
			}
		case diff.PreDelta:
			if typedDelta.PrePosition() == position {
				results = append(results, delta)
			}
		default:
			panic("heh")
		}
	}
	return
}

func (f *JSONFormatter) push(name string, size int, array bool) {
	f.path = append(f.path, name)
	f.size = append(f.size, size)
	f.inArray = append(f.inArray, array)
}

func (f *JSONFormatter) pop() {
	f.path = f.path[0 : len(f.path)-1]
	f.size = f.size[0 : len(f.size)-1]
	f.inArray = f.inArray[0 : len(f.inArray)-1]
}

func (f *JSONFormatter) addLineWith(change ChangeType, value string) {
	f.line = &AsciiLine{
		change: change,
		indent: len(f.path),
		buffer: bytes.NewBufferString(value),
	}
	f.closeLine()
}

func (f *JSONFormatter) newLine(change ChangeType) {
	f.line = &AsciiLine{
		change: change,
		indent: len(f.path),
		buffer: bytes.NewBuffer([]byte{}),
	}
}

func (f *JSONFormatter) closeLine() {
	leftLine := 0
	rightLine := 0
	f.lineCount++

	switch f.line.change {
	case ChangeAdded, ChangeNew:
		f.rightLine++
		rightLine = f.rightLine

	case ChangeDeleted, ChangeOld:
		f.leftLine++
		leftLine = f.leftLine

	case ChangeNil, ChangeUnchanged:
		f.rightLine++
		f.leftLine++
		rightLine = f.rightLine
		leftLine = f.leftLine
	}

	s := f.line.buffer.String()
	f.Lines = append(f.Lines, &JSONLine{
		LineNum:   f.lineCount,
		RightLine: rightLine,
		LeftLine:  leftLine,
		Indent:    f.line.indent,
		Text:      s,
		Change:    f.line.change,
		Key:       f.line.key,
		Val:       f.line.val,
	})
}

func (f *JSONFormatter) printKey(name string) {
	if !f.inArray[len(f.inArray)-1] {
		f.line.key = name
		fmt.Fprintf(f.line.buffer, `"%s": `, name)
	}
}

func (f *JSONFormatter) printComma() {
	f.size[len(f.size)-1]--
	if f.size[len(f.size)-1] > 0 {
		f.line.buffer.WriteRune(',')
	}
}

func (f *JSONFormatter) printValue(value interface{}) {
	switch value.(type) {
	case string:
		f.line.val = value
		fmt.Fprintf(f.line.buffer, `"%s"`, value)
	case nil:
		f.line.val = "null"
		f.line.buffer.WriteString("null")
	default:
		f.line.val = value
		fmt.Fprintf(f.line.buffer, `%#v`, value)
	}
}

func (f *JSONFormatter) print(a string) {
	f.line.buffer.WriteString(a)
}

func (f *JSONFormatter) printRecursive(name string, value interface{}, change ChangeType) {
	switch value := value.(type) {
	case map[string]interface{}:
		f.newLine(change)
		f.printKey(name)
		f.print("{")
		f.closeLine()

		size := len(value)
		f.push(name, size, false)

		keys := sortKeys(value)
		for _, key := range keys {
			f.printRecursive(key, value[key], change)
		}
		f.pop()

		f.newLine(change)
		f.print("}")
		f.printComma()
		f.closeLine()

	case []interface{}:
		f.newLine(change)
		f.printKey(name)
		f.print("[")
		f.closeLine()

		size := len(value)
		f.push("", size, true)
		for _, item := range value {
			f.printRecursive("", item, change)
		}
		f.pop()

		f.newLine(change)
		f.print("]")
		f.printComma()
		f.closeLine()

	default:
		f.newLine(change)
		f.printKey(name)
		f.printValue(value)
		f.printComma()
		f.closeLine()
	}
}

func sortKeys(m map[string]interface{}) (keys []string) {
	keys = make([]string, 0, len(m))
	for key := range m {
		keys = append(keys, key)
	}
	sort.Strings(keys)
	return
}
