package formatter

import (
	"errors"
	"fmt"
	"sort"

	diff "github.com/yudai/gojsondiff"
)

func NewAsciiFormatter(left map[string]interface{}) *AsciiFormatter {
	return &AsciiFormatter{
		left:           left,
		ShowArrayIndex: false,
	}
}

type AsciiFormatter struct {
	left           map[string]interface{}
	ShowArrayIndex bool
	buffer         string
	path           []string
	size           []int
	inArray        []bool
}

func (f *AsciiFormatter) Format(diff diff.Diff) (result string, err error) {
	f.buffer = ""
	f.path = []string{}
	f.size = []int{}
	f.inArray = []bool{}

	f.printIndent(AsciiSame)
	f.println("{")
	f.push("ROOT", len(f.left), false)
	f.processObject(f.left, diff.Deltas())
	f.pop()
	f.printIndent(AsciiSame)
	f.println("}")

	return f.buffer, nil
}

func (f *AsciiFormatter) processArray(array []interface{}, deltas []diff.Delta) error {
	patchedIndex := 0
	for index, value := range array {
		f.processItem(value, deltas, diff.Index(index))
		patchedIndex++
	}

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

	return nil
}

func (f *AsciiFormatter) processObject(object map[string]interface{}, deltas []diff.Delta) error {
	names := sortedKeys(object)
	for _, name := range names {
		value := object[name]
		f.processItem(value, deltas, diff.Name(name))
	}

	// Added
	for _, delta := range deltas {
		switch delta.(type) {
		case *diff.Added:
			d := delta.(*diff.Added)
			f.printRecursive(d.Position.String(), d.Value, AsciiAdded)
		}
	}

	return nil
}

func (f *AsciiFormatter) 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.(type) {
			case *diff.Object:
				d := matchedDelta.(*diff.Object)
				switch value.(type) {
				case map[string]interface{}:
					//ok
				default:
					return errors.New("Type mismatch")
				}
				o := value.(map[string]interface{})

				f.printKeyWithIndent(positionStr, AsciiSame)
				f.println("{")
				f.push(positionStr, len(o), false)
				f.processObject(o, d.Deltas)
				f.pop()
				f.printIndent(AsciiSame)
				f.print("}")
				f.printComma()

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

				f.printKeyWithIndent(positionStr, AsciiSame)
				f.println("[")
				f.push(positionStr, len(a), true)
				f.processArray(a, d.Deltas)
				f.pop()
				f.printIndent(AsciiSame)
				f.print("]")
				f.printComma()

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

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

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

			case *diff.Deleted:
				d := matchedDelta.(*diff.Deleted)
				f.printRecursive(positionStr, d.Value, AsciiDeleted)

			default:
				return errors.New("Unknown Delta type detected")
			}

		}
	} else {
		f.printRecursive(positionStr, value, AsciiSame)
	}

	return nil
}

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

const (
	AsciiSame    = " "
	AsciiAdded   = "+"
	AsciiDeleted = "-"
)

func (f *AsciiFormatter) 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 *AsciiFormatter) 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 *AsciiFormatter) printIndent(marker string) {
	f.print(marker)
	for n := 0; n < len(f.path); n++ {
		f.print("  ")
	}
}

func (f *AsciiFormatter) printKeyWithIndent(name string, marker string) {
	f.printIndent(marker)
	if !f.inArray[len(f.inArray)-1] {
		f.printf(`"%s": `, name)
	} else if f.ShowArrayIndex {
		f.printf(`%s: `, name)
	}
}

func (f *AsciiFormatter) printComma() {
	f.size[len(f.size)-1]--
	if f.size[len(f.size)-1] > 0 {
		f.println(",")
	} else {
		f.println()
	}
}

func (f *AsciiFormatter) printValue(value interface{}) {
	switch value.(type) {
	case string:
		f.buffer += fmt.Sprintf(`"%s"`, value)
	default:
		f.buffer += fmt.Sprintf(`%#v`, value)
	}
}

func (f *AsciiFormatter) print(a ...interface{}) {
	f.buffer += fmt.Sprint(a...)
}

func (f *AsciiFormatter) printf(format string, a ...interface{}) {
	f.buffer += fmt.Sprintf(format, a...)
}

func (f *AsciiFormatter) println(a ...interface{}) {
	f.buffer += fmt.Sprintln(a...)
}

func (f *AsciiFormatter) printRecursive(name string, value interface{}, marker string) {
	switch value.(type) {
	case map[string]interface{}:
		f.printKeyWithIndent(name, marker)
		f.println("{")

		m := value.(map[string]interface{})
		size := len(m)
		f.push(name, size, false)

		keys := sortedKeys(m)
		for _, key := range keys {
			f.printRecursive(key, m[key], marker)
		}
		f.pop()

		f.printIndent(marker)
		f.print("}")
		f.printComma()
	case []interface{}:
		f.printKeyWithIndent(name, marker)
		f.println("[")

		s := value.([]interface{})
		size := len(s)
		f.push("", size, true)
		for _, item := range s {
			f.printRecursive("", item, marker)
		}
		f.pop()

		f.printIndent(marker)
		f.print("]")
		f.printComma()
	default:
		f.printKeyWithIndent(name, marker)
		f.printValue(value)
		f.printComma()
	}
}

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
}
