package main

import (
	"fmt"
	"regexp"
)

type searchResult struct {
	path   string
	index  int
	ranges []*foundRange
}

type rangeKind int

const (
	keyRange rangeKind = 1 + iota
	valueRange
	delimRange
	openBracketRange
	closeBracketRange
	commaRange
)

type foundRange struct {
	parent *searchResult
	// Range needs separate path, as for one searchResult's path
	// there can be multiple ranges for different paths (within parent path).
	path       string
	start, end int
	kind       rangeKind
}

type rangeGroup struct {
	key          []*foundRange
	value        []*foundRange
	delim        []*foundRange
	openBracket  *foundRange
	closeBracket *foundRange
	comma        *foundRange
}

func (m *model) clearSearchResults() {
	m.searchRegexCompileError = ""
	m.searchResults = nil
	m.highlightIndex = nil
}

func (m *model) doSearch(s string) {
	m.clearSearchResults()
	re, err := regexp.Compile("(?i)" + s)
	if err != nil {
		m.searchRegexCompileError = err.Error()
		m.searchInput.Blur()
		return
	}
	indexes := re.FindAllStringIndex(stringify(m.json), -1)
	m.remapSearchResult(m.json, "", 0, indexes, 0, nil)
	m.indexSearchResults()
	m.searchInput.Blur()
	m.showSearchResults = true
	m.jumpToSearchResult(0)
}

func (m *model) remapSearchResult(object interface{}, path string, pos int, indexes [][]int, id int, current *searchResult) (int, int, *searchResult) {
	switch object.(type) {
	case nil:
		s := "null"
		id, current = m.findRanges(valueRange, s, path, pos, indexes, id, current)
		return pos + len(s), id, current

	case bool:
		var s string
		if object.(bool) {
			s = "true"
		} else {
			s = "false"
		}
		id, current = m.findRanges(valueRange, s, path, pos, indexes, id, current)
		return pos + len(s), id, current

	case number:
		s := object.(number).String()
		id, current = m.findRanges(valueRange, s, path, pos, indexes, id, current)
		return pos + len(s), id, current

	case string:
		s := fmt.Sprintf("%q", object)
		id, current = m.findRanges(valueRange, s, path, pos, indexes, id, current)
		return pos + len(s), id, current

	case *dict:
		id, current = m.findRanges(openBracketRange, "{", path, pos, indexes, id, current)
		pos++ // {
		for i, k := range object.(*dict).keys {
			subpath := path + "." + k

			key := fmt.Sprintf("%q", k)
			id, current = m.findRanges(keyRange, key, subpath, pos, indexes, id, current)
			pos += len(key)

			delim := ": "
			id, current = m.findRanges(delimRange, delim, subpath, pos, indexes, id, current)
			pos += len(delim)

			pos, id, current = m.remapSearchResult(object.(*dict).values[k], subpath, pos, indexes, id, current)
			if i < len(object.(*dict).keys)-1 {
				comma := ","
				id, current = m.findRanges(commaRange, comma, subpath, pos, indexes, id, current)
				pos += len(comma)
			}
		}
		id, current = m.findRanges(closeBracketRange, "}", path, pos, indexes, id, current)
		pos++ // }
		return pos, id, current

	case array:
		id, current = m.findRanges(openBracketRange, "[", path, pos, indexes, id, current)
		pos++ // [
		for i, v := range object.(array) {
			subpath := fmt.Sprintf("%v[%v]", path, i)
			pos, id, current = m.remapSearchResult(v, subpath, pos, indexes, id, current)
			if i < len(object.(array))-1 {
				comma := ","
				id, current = m.findRanges(commaRange, comma, subpath, pos, indexes, id, current)
				pos += len(comma)
			}
		}
		id, current = m.findRanges(closeBracketRange, "]", path, pos, indexes, id, current)
		pos++ // ]
		return pos, id, current

	default:
		panic("unexpected object type")
	}
}

func (m *model) findRanges(kind rangeKind, s string, path string, pos int, indexes [][]int, id int, current *searchResult) (int, *searchResult) {
	for ; id < len(indexes); id++ {
		start, end := indexes[id][0]-pos, indexes[id][1]-pos
		if end <= 0 {
			current = nil
			continue
		}
		if start < len(s) {
			if current == nil {
				current = &searchResult{
					path:  path,
					index: len(m.searchResults),
				}
				m.searchResults = append(m.searchResults, current)
			}
			found := &foundRange{
				parent: current,
				path:   path,
				start:  max(start, 0),
				end:    min(end, len(s)),
				kind:   kind,
			}
			current.ranges = append(current.ranges, found)
			if end < len(s) {
				current = nil
			} else {
				break
			}
		} else {
			break
		}
	}
	return id, current
}

func (m *model) indexSearchResults() {
	m.highlightIndex = map[string]*rangeGroup{}
	for _, s := range m.searchResults {
		for _, r := range s.ranges {
			highlight, exist := m.highlightIndex[r.path]
			if !exist {
				highlight = &rangeGroup{}
				m.highlightIndex[r.path] = highlight
			}
			switch r.kind {
			case keyRange:
				highlight.key = append(highlight.key, r)
			case valueRange:
				highlight.value = append(highlight.value, r)
			case delimRange:
				highlight.delim = append(highlight.delim, r)
			case openBracketRange:
				highlight.openBracket = r
			case closeBracketRange:
				highlight.closeBracket = r
			case commaRange:
				highlight.comma = r
			}
		}
	}
}

func (m *model) jumpToSearchResult(at int) {
	if len(m.searchResults) == 0 {
		return
	}
	m.showCursor = false
	m.searchResultsCursor = at % len(m.searchResults)
	desiredPath := m.searchResults[m.searchResultsCursor].path
	lineNumber, ok := m.pathToLineNumber[desiredPath]
	if ok {
		m.cursor = m.pathToIndex[desiredPath]
		m.SetOffset(lineNumber)
		m.render()
	} else {
		m.expandToPath(desiredPath)
		m.render()
		m.jumpToSearchResult(at)
	}
}

func (m *model) expandToPath(path string) {
	m.expandedPaths[path] = true
	if path != "" {
		m.expandToPath(m.parents[path])
	}
}

func (m *model) nextSearchResult() {
	if len(m.searchResults) > 0 {
		m.jumpToSearchResult((m.searchResultsCursor + 1) % len(m.searchResults))
	}
}

func (m *model) prevSearchResult() {
	i := m.searchResultsCursor - 1
	if i < 0 {
		i = len(m.searchResults) - 1
	}
	m.jumpToSearchResult(i)
}

func (m *model) resultsCursorPath() string {
	if len(m.searchResults) == 0 {
		return "?"
	}
	return m.searchResults[m.searchResultsCursor].path
}
