package otto

import (
	"bytes"
	"regexp"
	"strconv"
	"strings"
	"unicode/utf8"
)

// String

func stringValueFromStringArgumentList(argumentList []Value) Value {
	if len(argumentList) > 0 {
		return toValue_string(argumentList[0].string())
	}
	return toValue_string("")
}

func builtinString(call FunctionCall) Value {
	return stringValueFromStringArgumentList(call.ArgumentList)
}

func builtinNewString(self *_object, argumentList []Value) Value {
	return toValue_object(self.runtime.newString(stringValueFromStringArgumentList(argumentList)))
}

func builtinString_toString(call FunctionCall) Value {
	return call.thisClassObject("String").primitiveValue()
}
func builtinString_valueOf(call FunctionCall) Value {
	return call.thisClassObject("String").primitiveValue()
}

func builtinString_fromCharCode(call FunctionCall) Value {
	chrList := make([]uint16, len(call.ArgumentList))
	for index, value := range call.ArgumentList {
		chrList[index] = toUint16(value)
	}
	return toValue_string16(chrList)
}

func builtinString_charAt(call FunctionCall) Value {
	checkObjectCoercible(call.runtime, call.This)
	idx := int(call.Argument(0).number().int64)
	chr := stringAt(call.This._object().stringValue(), idx)
	if chr == utf8.RuneError {
		return toValue_string("")
	}
	return toValue_string(string(chr))
}

func builtinString_charCodeAt(call FunctionCall) Value {
	checkObjectCoercible(call.runtime, call.This)
	idx := int(call.Argument(0).number().int64)
	chr := stringAt(call.This._object().stringValue(), idx)
	if chr == utf8.RuneError {
		return NaNValue()
	}
	return toValue_uint16(uint16(chr))
}

func builtinString_concat(call FunctionCall) Value {
	checkObjectCoercible(call.runtime, call.This)
	var value bytes.Buffer
	value.WriteString(call.This.string())
	for _, item := range call.ArgumentList {
		value.WriteString(item.string())
	}
	return toValue_string(value.String())
}

func builtinString_indexOf(call FunctionCall) Value {
	checkObjectCoercible(call.runtime, call.This)
	value := call.This.string()
	target := call.Argument(0).string()
	if 2 > len(call.ArgumentList) {
		return toValue_int(strings.Index(value, target))
	}
	start := toIntegerFloat(call.Argument(1))
	if 0 > start {
		start = 0
	} else if start >= float64(len(value)) {
		if target == "" {
			return toValue_int(len(value))
		}
		return toValue_int(-1)
	}
	index := strings.Index(value[int(start):], target)
	if index >= 0 {
		index += int(start)
	}
	return toValue_int(index)
}

func builtinString_lastIndexOf(call FunctionCall) Value {
	checkObjectCoercible(call.runtime, call.This)
	value := call.This.string()
	target := call.Argument(0).string()
	if 2 > len(call.ArgumentList) || call.ArgumentList[1].IsUndefined() {
		return toValue_int(strings.LastIndex(value, target))
	}
	length := len(value)
	if length == 0 {
		return toValue_int(strings.LastIndex(value, target))
	}
	start := call.ArgumentList[1].number()
	if start.kind == numberInfinity { // FIXME
		// startNumber is infinity, so start is the end of string (start = length)
		return toValue_int(strings.LastIndex(value, target))
	}
	if 0 > start.int64 {
		start.int64 = 0
	}
	end := int(start.int64) + len(target)
	if end > length {
		end = length
	}
	return toValue_int(strings.LastIndex(value[:end], target))
}

func builtinString_match(call FunctionCall) Value {
	checkObjectCoercible(call.runtime, call.This)
	target := call.This.string()
	matcherValue := call.Argument(0)
	matcher := matcherValue._object()
	if !matcherValue.IsObject() || matcher.class != "RegExp" {
		matcher = call.runtime.newRegExp(matcherValue, Value{})
	}
	global := matcher.get("global").bool()
	if !global {
		match, result := execRegExp(matcher, target)
		if !match {
			return nullValue
		}
		return toValue_object(execResultToArray(call.runtime, target, result))
	}

	{
		result := matcher.regExpValue().regularExpression.FindAllStringIndex(target, -1)
		matchCount := len(result)
		if result == nil {
			matcher.put("lastIndex", toValue_int(0), true)
			return Value{} // !match
		}
		matchCount = len(result)
		valueArray := make([]Value, matchCount)
		for index := 0; index < matchCount; index++ {
			valueArray[index] = toValue_string(target[result[index][0]:result[index][1]])
		}
		matcher.put("lastIndex", toValue_int(result[matchCount-1][1]), true)
		return toValue_object(call.runtime.newArrayOf(valueArray))
	}
}

var builtinString_replace_Regexp = regexp.MustCompile("\\$(?:[\\$\\&\\'\\`1-9]|0[1-9]|[1-9][0-9])")

func builtinString_findAndReplaceString(input []byte, lastIndex int, match []int, target []byte, replaceValue []byte) (output []byte) {
	matchCount := len(match) / 2
	output = input
	if match[0] != lastIndex {
		output = append(output, target[lastIndex:match[0]]...)
	}
	replacement := builtinString_replace_Regexp.ReplaceAllFunc(replaceValue, func(part []byte) []byte {
		// TODO Check if match[0] or match[1] can be -1 in this scenario
		switch part[1] {
		case '$':
			return []byte{'$'}
		case '&':
			return target[match[0]:match[1]]
		case '`':
			return target[:match[0]]
		case '\'':
			return target[match[1]:len(target)]
		}
		matchNumberParse, error := strconv.ParseInt(string(part[1:]), 10, 64)
		matchNumber := int(matchNumberParse)
		if error != nil || matchNumber >= matchCount {
			return []byte{}
		}
		offset := 2 * matchNumber
		if match[offset] != -1 {
			return target[match[offset]:match[offset+1]]
		}
		return []byte{} // The empty string
	})
	output = append(output, replacement...)
	return output
}

func builtinString_replace(call FunctionCall) Value {
	checkObjectCoercible(call.runtime, call.This)
	target := []byte(call.This.string())
	searchValue := call.Argument(0)
	searchObject := searchValue._object()

	// TODO If a capture is -1?
	var search *regexp.Regexp
	global := false
	find := 1
	if searchValue.IsObject() && searchObject.class == "RegExp" {
		regExp := searchObject.regExpValue()
		search = regExp.regularExpression
		if regExp.global {
			find = -1
		}
	} else {
		search = regexp.MustCompile(regexp.QuoteMeta(searchValue.string()))
	}

	found := search.FindAllSubmatchIndex(target, find)
	if found == nil {
		return toValue_string(string(target)) // !match
	}

	{
		lastIndex := 0
		result := []byte{}

		replaceValue := call.Argument(1)
		if replaceValue.isCallable() {
			target := string(target)
			replace := replaceValue._object()
			for _, match := range found {
				if match[0] != lastIndex {
					result = append(result, target[lastIndex:match[0]]...)
				}
				matchCount := len(match) / 2
				argumentList := make([]Value, matchCount+2)
				for index := 0; index < matchCount; index++ {
					offset := 2 * index
					if match[offset] != -1 {
						argumentList[index] = toValue_string(target[match[offset]:match[offset+1]])
					} else {
						argumentList[index] = Value{}
					}
				}
				argumentList[matchCount+0] = toValue_int(match[0])
				argumentList[matchCount+1] = toValue_string(target)
				replacement := replace.call(Value{}, argumentList, false, nativeFrame).string()
				result = append(result, []byte(replacement)...)
				lastIndex = match[1]
			}

		} else {
			replace := []byte(replaceValue.string())
			for _, match := range found {
				result = builtinString_findAndReplaceString(result, lastIndex, match, target, replace)
				lastIndex = match[1]
			}
		}

		if lastIndex != len(target) {
			result = append(result, target[lastIndex:]...)
		}

		if global && searchObject != nil {
			searchObject.put("lastIndex", toValue_int(lastIndex), true)
		}

		return toValue_string(string(result))
	}
}

func builtinString_search(call FunctionCall) Value {
	checkObjectCoercible(call.runtime, call.This)
	target := call.This.string()
	searchValue := call.Argument(0)
	search := searchValue._object()
	if !searchValue.IsObject() || search.class != "RegExp" {
		search = call.runtime.newRegExp(searchValue, Value{})
	}
	result := search.regExpValue().regularExpression.FindStringIndex(target)
	if result == nil {
		return toValue_int(-1)
	}
	return toValue_int(result[0])
}

func stringSplitMatch(target string, targetLength int64, index uint, search string, searchLength int64) (bool, uint) {
	if int64(index)+searchLength > searchLength {
		return false, 0
	}
	found := strings.Index(target[index:], search)
	if 0 > found {
		return false, 0
	}
	return true, uint(found)
}

func builtinString_split(call FunctionCall) Value {
	checkObjectCoercible(call.runtime, call.This)
	target := call.This.string()

	separatorValue := call.Argument(0)
	limitValue := call.Argument(1)
	limit := -1
	if limitValue.IsDefined() {
		limit = int(toUint32(limitValue))
	}

	if limit == 0 {
		return toValue_object(call.runtime.newArray(0))
	}

	if separatorValue.IsUndefined() {
		return toValue_object(call.runtime.newArrayOf([]Value{toValue_string(target)}))
	}

	if separatorValue.isRegExp() {
		targetLength := len(target)
		search := separatorValue._object().regExpValue().regularExpression
		valueArray := []Value{}
		result := search.FindAllStringSubmatchIndex(target, -1)
		lastIndex := 0
		found := 0

		for _, match := range result {
			if match[0] == match[1] {
				// FIXME Ugh, this is a hack
				if match[0] == 0 || match[0] == targetLength {
					continue
				}
			}

			if lastIndex != match[0] {
				valueArray = append(valueArray, toValue_string(target[lastIndex:match[0]]))
				found++
			} else if lastIndex == match[0] {
				if lastIndex != -1 {
					valueArray = append(valueArray, toValue_string(""))
					found++
				}
			}

			lastIndex = match[1]
			if found == limit {
				goto RETURN
			}

			captureCount := len(match) / 2
			for index := 1; index < captureCount; index++ {
				offset := index * 2
				value := Value{}
				if match[offset] != -1 {
					value = toValue_string(target[match[offset]:match[offset+1]])
				}
				valueArray = append(valueArray, value)
				found++
				if found == limit {
					goto RETURN
				}
			}
		}

		if found != limit {
			if lastIndex != targetLength {
				valueArray = append(valueArray, toValue_string(target[lastIndex:targetLength]))
			} else {
				valueArray = append(valueArray, toValue_string(""))
			}
		}

	RETURN:
		return toValue_object(call.runtime.newArrayOf(valueArray))

	} else {
		separator := separatorValue.string()

		splitLimit := limit
		excess := false
		if limit > 0 {
			splitLimit = limit + 1
			excess = true
		}

		split := strings.SplitN(target, separator, splitLimit)

		if excess && len(split) > limit {
			split = split[:limit]
		}

		valueArray := make([]Value, len(split))
		for index, value := range split {
			valueArray[index] = toValue_string(value)
		}

		return toValue_object(call.runtime.newArrayOf(valueArray))
	}
}

func builtinString_slice(call FunctionCall) Value {
	checkObjectCoercible(call.runtime, call.This)
	target := call.This.string()

	length := int64(len(target))
	start, end := rangeStartEnd(call.ArgumentList, length, false)
	if end-start <= 0 {
		return toValue_string("")
	}
	return toValue_string(target[start:end])
}

func builtinString_substring(call FunctionCall) Value {
	checkObjectCoercible(call.runtime, call.This)
	target := call.This.string()

	length := int64(len(target))
	start, end := rangeStartEnd(call.ArgumentList, length, true)
	if start > end {
		start, end = end, start
	}
	return toValue_string(target[start:end])
}

func builtinString_substr(call FunctionCall) Value {
	target := call.This.string()

	size := int64(len(target))
	start, length := rangeStartLength(call.ArgumentList, size)

	if start >= size {
		return toValue_string("")
	}

	if length <= 0 {
		return toValue_string("")
	}

	if start+length >= size {
		// Cap length to be to the end of the string
		// start = 3, length = 5, size = 4 [0, 1, 2, 3]
		// 4 - 3 = 1
		// target[3:4]
		length = size - start
	}

	return toValue_string(target[start : start+length])
}

func builtinString_toLowerCase(call FunctionCall) Value {
	checkObjectCoercible(call.runtime, call.This)
	return toValue_string(strings.ToLower(call.This.string()))
}

func builtinString_toUpperCase(call FunctionCall) Value {
	checkObjectCoercible(call.runtime, call.This)
	return toValue_string(strings.ToUpper(call.This.string()))
}

// 7.2 Table 2 — Whitespace Characters & 7.3 Table 3 - Line Terminator Characters
const builtinString_trim_whitespace = "\u0009\u000A\u000B\u000C\u000D\u0020\u00A0\u1680\u180E\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u2028\u2029\u202F\u205F\u3000\uFEFF"

func builtinString_trim(call FunctionCall) Value {
	checkObjectCoercible(call.runtime, call.This)
	return toValue(strings.Trim(call.This.string(),
		builtinString_trim_whitespace))
}

// Mozilla extension, not ECMAScript 5
func builtinString_trimLeft(call FunctionCall) Value {
	checkObjectCoercible(call.runtime, call.This)
	return toValue(strings.TrimLeft(call.This.string(),
		builtinString_trim_whitespace))
}

// Mozilla extension, not ECMAScript 5
func builtinString_trimRight(call FunctionCall) Value {
	checkObjectCoercible(call.runtime, call.This)
	return toValue(strings.TrimRight(call.This.string(),
		builtinString_trim_whitespace))
}

func builtinString_localeCompare(call FunctionCall) Value {
	checkObjectCoercible(call.runtime, call.This)
	this := call.This.string()
	that := call.Argument(0).string()
	if this < that {
		return toValue_int(-1)
	} else if this == that {
		return toValue_int(0)
	}
	return toValue_int(1)
}

/*
An alternate version of String.trim
func builtinString_trim(call FunctionCall) Value {
	checkObjectCoercible(call.This)
	return toValue_string(strings.TrimFunc(call.string(.This), isWhiteSpaceOrLineTerminator))
}
*/

func builtinString_toLocaleLowerCase(call FunctionCall) Value {
	return builtinString_toLowerCase(call)
}

func builtinString_toLocaleUpperCase(call FunctionCall) Value {
	return builtinString_toUpperCase(call)
}