package goja

import (
	"bytes"
	"github.com/dop251/goja/parser"
	"golang.org/x/text/collate"
	"golang.org/x/text/language"
	"golang.org/x/text/unicode/norm"
	"math"
	"strings"
	"unicode/utf8"
)

var (
	collator = collate.New(language.Und)
)

func (r *Runtime) builtin_String(call FunctionCall) Value {
	if len(call.Arguments) > 0 {
		arg := call.Arguments[0]
		if _, ok := arg.assertString(); ok {
			return arg
		}
		return arg.ToString()
	} else {
		return newStringValue("")
	}
}

func (r *Runtime) _newString(s valueString) *Object {
	v := &Object{runtime: r}

	o := &stringObject{}
	o.class = classString
	o.val = v
	o.extensible = true
	v.self = o
	o.prototype = r.global.StringPrototype
	if s != nil {
		o.value = s
	}
	o.init()
	return v
}

func (r *Runtime) builtin_newString(args []Value) *Object {
	var s valueString
	if len(args) > 0 {
		s = args[0].ToString()
	} else {
		s = stringEmpty
	}
	return r._newString(s)
}

func searchSubstringUTF8(str, search string) (ret [][]int) {
	searchPos := 0
	l := len(str)
	if searchPos < l {
		p := strings.Index(str[searchPos:], search)
		if p != -1 {
			p += searchPos
			searchPos = p + len(search)
			ret = append(ret, []int{p, searchPos})
		}
	}
	return
}

func (r *Runtime) stringproto_toStringValueOf(this Value, funcName string) Value {
	if str, ok := this.assertString(); ok {
		return str
	}
	if obj, ok := this.(*Object); ok {
		if strObj, ok := obj.self.(*stringObject); ok {
			return strObj.value
		}
	}
	r.typeErrorResult(true, "String.prototype.%s is called on incompatible receiver", funcName)
	return nil
}

func (r *Runtime) stringproto_toString(call FunctionCall) Value {
	return r.stringproto_toStringValueOf(call.This, "toString")
}

func (r *Runtime) stringproto_valueOf(call FunctionCall) Value {
	return r.stringproto_toStringValueOf(call.This, "valueOf")
}

func (r *Runtime) string_fromcharcode(call FunctionCall) Value {
	b := make([]byte, len(call.Arguments))
	for i, arg := range call.Arguments {
		chr := toUInt16(arg)
		if chr >= utf8.RuneSelf {
			bb := make([]uint16, len(call.Arguments))
			for j := 0; j < i; j++ {
				bb[j] = uint16(b[j])
			}
			bb[i] = chr
			i++
			for j, arg := range call.Arguments[i:] {
				bb[i+j] = toUInt16(arg)
			}
			return unicodeString(bb)
		}
		b[i] = byte(chr)
	}

	return asciiString(b)
}

func (r *Runtime) stringproto_charAt(call FunctionCall) Value {
	r.checkObjectCoercible(call.This)
	s := call.This.ToString()
	pos := call.Argument(0).ToInteger()
	if pos < 0 || pos >= s.length() {
		return stringEmpty
	}
	return newStringValue(string(s.charAt(pos)))
}

func (r *Runtime) stringproto_charCodeAt(call FunctionCall) Value {
	r.checkObjectCoercible(call.This)
	s := call.This.ToString()
	pos := call.Argument(0).ToInteger()
	if pos < 0 || pos >= s.length() {
		return _NaN
	}
	return intToValue(int64(s.charAt(pos) & 0xFFFF))
}

func (r *Runtime) stringproto_concat(call FunctionCall) Value {
	r.checkObjectCoercible(call.This)
	strs := make([]valueString, len(call.Arguments)+1)
	strs[0] = call.This.ToString()
	_, allAscii := strs[0].(asciiString)
	totalLen := strs[0].length()
	for i, arg := range call.Arguments {
		s := arg.ToString()
		if allAscii {
			_, allAscii = s.(asciiString)
		}
		strs[i+1] = s
		totalLen += s.length()
	}

	if allAscii {
		buf := bytes.NewBuffer(make([]byte, 0, totalLen))
		for _, s := range strs {
			buf.WriteString(s.String())
		}
		return asciiString(buf.String())
	} else {
		buf := make([]uint16, totalLen)
		pos := int64(0)
		for _, s := range strs {
			switch s := s.(type) {
			case asciiString:
				for i := 0; i < len(s); i++ {
					buf[pos] = uint16(s[i])
					pos++
				}
			case unicodeString:
				copy(buf[pos:], s)
				pos += s.length()
			}
		}
		return unicodeString(buf)
	}
}

func (r *Runtime) stringproto_indexOf(call FunctionCall) Value {
	r.checkObjectCoercible(call.This)
	value := call.This.ToString()
	target := call.Argument(0).ToString()
	pos := call.Argument(1).ToInteger()

	if pos < 0 {
		pos = 0
	} else {
		l := value.length()
		if pos > l {
			pos = l
		}
	}

	return intToValue(value.index(target, pos))
}

func (r *Runtime) stringproto_lastIndexOf(call FunctionCall) Value {
	r.checkObjectCoercible(call.This)
	value := call.This.ToString()
	target := call.Argument(0).ToString()
	numPos := call.Argument(1).ToNumber()

	var pos int64
	if f, ok := numPos.assertFloat(); ok && math.IsNaN(f) {
		pos = value.length()
	} else {
		pos = numPos.ToInteger()
		if pos < 0 {
			pos = 0
		} else {
			l := value.length()
			if pos > l {
				pos = l
			}
		}
	}

	return intToValue(value.lastIndex(target, pos))
}

func (r *Runtime) stringproto_localeCompare(call FunctionCall) Value {
	r.checkObjectCoercible(call.This)
	this := norm.NFD.String(call.This.String())
	that := norm.NFD.String(call.Argument(0).String())
	return intToValue(int64(collator.CompareString(this, that)))
}

func (r *Runtime) stringproto_match(call FunctionCall) Value {
	r.checkObjectCoercible(call.This)
	s := call.This.ToString()
	regexp := call.Argument(0)
	var rx *regexpObject
	if regexp, ok := regexp.(*Object); ok {
		rx, _ = regexp.self.(*regexpObject)
	}

	if rx == nil {
		rx = r.builtin_newRegExp([]Value{regexp}).self.(*regexpObject)
	}

	if rx.global {
		rx.putStr("lastIndex", intToValue(0), false)
		var a []Value
		var previousLastIndex int64
		for {
			match, result := rx.execRegexp(s)
			if !match {
				break
			}
			thisIndex := rx.getStr("lastIndex").ToInteger()
			if thisIndex == previousLastIndex {
				previousLastIndex++
				rx.putStr("lastIndex", intToValue(previousLastIndex), false)
			} else {
				previousLastIndex = thisIndex
			}
			a = append(a, s.substring(int64(result[0]), int64(result[1])))
		}
		if len(a) == 0 {
			return _null
		}
		return r.newArrayValues(a)
	} else {
		return rx.exec(s)
	}
}

func (r *Runtime) stringproto_replace(call FunctionCall) Value {
	s := call.This.ToString()
	var str string
	var isASCII bool
	if astr, ok := s.(asciiString); ok {
		str = string(astr)
		isASCII = true
	} else {
		str = s.String()
	}
	searchValue := call.Argument(0)
	replaceValue := call.Argument(1)

	var found [][]int

	if searchValue, ok := searchValue.(*Object); ok {
		if regexp, ok := searchValue.self.(*regexpObject); ok {
			find := 1
			if regexp.global {
				find = -1
			}
			if isASCII {
				found = regexp.pattern.FindAllSubmatchIndexASCII(str, find)
			} else {
				found = regexp.pattern.FindAllSubmatchIndexUTF8(str, find)
			}
			if found == nil {
				return s
			}
		}
	}

	if found == nil {
		found = searchSubstringUTF8(str, searchValue.String())
	}

	if len(found) == 0 {
		return s
	}

	var buf bytes.Buffer
	lastIndex := 0

	var rcall func(FunctionCall) Value

	if replaceValue, ok := replaceValue.(*Object); ok {
		if c, ok := replaceValue.self.assertCallable(); ok {
			rcall = c
		}
	}

	if rcall != nil {
		for _, item := range found {
			if item[0] != lastIndex {
				buf.WriteString(str[lastIndex:item[0]])
			}
			matchCount := len(item) / 2
			argumentList := make([]Value, matchCount+2)
			for index := 0; index < matchCount; index++ {
				offset := 2 * index
				if item[offset] != -1 {
					if isASCII {
						argumentList[index] = asciiString(str[item[offset]:item[offset+1]])
					} else {
						argumentList[index] = newStringValue(str[item[offset]:item[offset+1]])
					}
				} else {
					argumentList[index] = _undefined
				}
			}
			argumentList[matchCount] = valueInt(item[0])
			argumentList[matchCount+1] = s
			replacement := rcall(FunctionCall{
				This:      _undefined,
				Arguments: argumentList,
			}).String()
			buf.WriteString(replacement)
			lastIndex = item[1]
		}
	} else {
		newstring := replaceValue.String()

		for _, item := range found {
			if item[0] != lastIndex {
				buf.WriteString(str[lastIndex:item[0]])
			}
			matches := len(item) / 2
			for i := 0; i < len(newstring); i++ {
				if newstring[i] == '$' && i < len(newstring)-1 {
					ch := newstring[i+1]
					switch ch {
					case '$':
						buf.WriteByte('$')
					case '`':
						buf.WriteString(str[0:item[0]])
					case '\'':
						buf.WriteString(str[item[1]:])
					case '&':
						buf.WriteString(str[item[0]:item[1]])
					default:
						matchNumber := 0
						l := 0
						for _, ch := range newstring[i+1:] {
							if ch >= '0' && ch <= '9' {
								m := matchNumber*10 + int(ch-'0')
								if m >= matches {
									break
								}
								matchNumber = m
								l++
							} else {
								break
							}
						}
						if l > 0 {
							offset := 2 * matchNumber
							if offset < len(item) && item[offset] != -1 {
								buf.WriteString(str[item[offset]:item[offset+1]])
							}
							i += l - 1
						} else {
							buf.WriteByte('$')
							buf.WriteByte(ch)
						}

					}
					i++
				} else {
					buf.WriteByte(newstring[i])
				}
			}
			lastIndex = item[1]
		}
	}

	if lastIndex != len(str) {
		buf.WriteString(str[lastIndex:])
	}

	return newStringValue(buf.String())
}

func (r *Runtime) stringproto_search(call FunctionCall) Value {
	r.checkObjectCoercible(call.This)
	s := call.This.ToString()
	regexp := call.Argument(0)
	var rx *regexpObject
	if regexp, ok := regexp.(*Object); ok {
		rx, _ = regexp.self.(*regexpObject)
	}

	if rx == nil {
		rx = r.builtin_newRegExp([]Value{regexp}).self.(*regexpObject)
	}

	match, result := rx.execRegexp(s)
	if !match {
		return intToValue(-1)
	}
	return intToValue(int64(result[0]))
}

func (r *Runtime) stringproto_slice(call FunctionCall) Value {
	r.checkObjectCoercible(call.This)
	s := call.This.ToString()

	l := s.length()
	start := call.Argument(0).ToInteger()
	var end int64
	if arg1 := call.Argument(1); arg1 != _undefined {
		end = arg1.ToInteger()
	} else {
		end = l
	}

	if start < 0 {
		start += l
		if start < 0 {
			start = 0
		}
	} else {
		if start > l {
			start = l
		}
	}

	if end < 0 {
		end += l
		if end < 0 {
			end = 0
		}
	} else {
		if end > l {
			end = l
		}
	}

	if end > start {
		return s.substring(start, end)
	}
	return stringEmpty
}

func (r *Runtime) stringproto_split(call FunctionCall) Value {
	r.checkObjectCoercible(call.This)
	s := call.This.ToString()

	separatorValue := call.Argument(0)
	limitValue := call.Argument(1)
	limit := -1
	if limitValue != _undefined {
		limit = int(toUInt32(limitValue))
	}

	if limit == 0 {
		return r.newArrayValues(nil)
	}

	if separatorValue == _undefined {
		return r.newArrayValues([]Value{s})
	}

	var search *regexpObject
	if o, ok := separatorValue.(*Object); ok {
		search, _ = o.self.(*regexpObject)
	}

	if search != nil {
		targetLength := s.length()
		valueArray := []Value{}
		result := search.pattern.FindAllSubmatchIndex(s, -1)
		lastIndex := 0
		found := 0

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

			if lastIndex != match[0] {
				valueArray = append(valueArray, s.substring(int64(lastIndex), int64(match[0])))
				found++
			} else if lastIndex == match[0] {
				if lastIndex != -1 {
					valueArray = append(valueArray, stringEmpty)
					found++
				}
			}

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

			captureCount := len(match) / 2
			for index := 1; index < captureCount; index++ {
				offset := index * 2
				var value Value
				if match[offset] != -1 {
					value = s.substring(int64(match[offset]), int64(match[offset+1]))
				} else {
					value = _undefined
				}
				valueArray = append(valueArray, value)
				found++
				if found == limit {
					goto RETURN
				}
			}
		}

		if found != limit {
			if int64(lastIndex) != targetLength {
				valueArray = append(valueArray, s.substring(int64(lastIndex), targetLength))
			} else {
				valueArray = append(valueArray, stringEmpty)
			}
		}

	RETURN:
		return r.newArrayValues(valueArray)

	} else {
		separator := separatorValue.String()

		excess := false
		str := s.String()
		if limit > len(str) {
			limit = len(str)
		}
		splitLimit := limit
		if limit > 0 {
			splitLimit = limit + 1
			excess = true
		}

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

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

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

		return r.newArrayValues(valueArray)
	}

}

func (r *Runtime) stringproto_substring(call FunctionCall) Value {
	r.checkObjectCoercible(call.This)
	s := call.This.ToString()

	l := s.length()
	intStart := call.Argument(0).ToInteger()
	var intEnd int64
	if end := call.Argument(1); end != _undefined {
		intEnd = end.ToInteger()
	} else {
		intEnd = l
	}
	if intStart < 0 {
		intStart = 0
	} else if intStart > l {
		intStart = l
	}

	if intEnd < 0 {
		intEnd = 0
	} else if intEnd > l {
		intEnd = l
	}

	if intStart > intEnd {
		intStart, intEnd = intEnd, intStart
	}

	return s.substring(intStart, intEnd)
}

func (r *Runtime) stringproto_toLowerCase(call FunctionCall) Value {
	r.checkObjectCoercible(call.This)
	s := call.This.ToString()

	return s.toLower()
}

func (r *Runtime) stringproto_toUpperCase(call FunctionCall) Value {
	r.checkObjectCoercible(call.This)
	s := call.This.ToString()

	return s.toUpper()
}

func (r *Runtime) stringproto_trim(call FunctionCall) Value {
	r.checkObjectCoercible(call.This)
	s := call.This.ToString()

	return newStringValue(strings.Trim(s.String(), parser.WhitespaceChars))
}

func (r *Runtime) stringproto_substr(call FunctionCall) Value {
	s := call.This.ToString()
	start := call.Argument(0).ToInteger()
	var length int64
	sl := int64(s.length())
	if arg := call.Argument(1); arg != _undefined {
		length = arg.ToInteger()
	} else {
		length = sl
	}

	if start < 0 {
		start = max(sl+start, 0)
	}

	length = min(max(length, 0), sl-start)
	if length <= 0 {
		return stringEmpty
	}

	return s.substring(start, start+length)
}

func (r *Runtime) initString() {
	r.global.StringPrototype = r.builtin_newString([]Value{stringEmpty})

	o := r.global.StringPrototype.self
	o.(*stringObject).prototype = r.global.ObjectPrototype
	o._putProp("toString", r.newNativeFunc(r.stringproto_toString, nil, "toString", nil, 0), true, false, true)
	o._putProp("valueOf", r.newNativeFunc(r.stringproto_valueOf, nil, "valueOf", nil, 0), true, false, true)
	o._putProp("charAt", r.newNativeFunc(r.stringproto_charAt, nil, "charAt", nil, 1), true, false, true)
	o._putProp("charCodeAt", r.newNativeFunc(r.stringproto_charCodeAt, nil, "charCodeAt", nil, 1), true, false, true)
	o._putProp("concat", r.newNativeFunc(r.stringproto_concat, nil, "concat", nil, 1), true, false, true)
	o._putProp("indexOf", r.newNativeFunc(r.stringproto_indexOf, nil, "indexOf", nil, 1), true, false, true)
	o._putProp("lastIndexOf", r.newNativeFunc(r.stringproto_lastIndexOf, nil, "lastIndexOf", nil, 1), true, false, true)
	o._putProp("localeCompare", r.newNativeFunc(r.stringproto_localeCompare, nil, "localeCompare", nil, 1), true, false, true)
	o._putProp("match", r.newNativeFunc(r.stringproto_match, nil, "match", nil, 1), true, false, true)
	o._putProp("replace", r.newNativeFunc(r.stringproto_replace, nil, "replace", nil, 2), true, false, true)
	o._putProp("search", r.newNativeFunc(r.stringproto_search, nil, "search", nil, 1), true, false, true)
	o._putProp("slice", r.newNativeFunc(r.stringproto_slice, nil, "slice", nil, 2), true, false, true)
	o._putProp("split", r.newNativeFunc(r.stringproto_split, nil, "split", nil, 2), true, false, true)
	o._putProp("substring", r.newNativeFunc(r.stringproto_substring, nil, "substring", nil, 2), true, false, true)
	o._putProp("toLowerCase", r.newNativeFunc(r.stringproto_toLowerCase, nil, "toLowerCase", nil, 0), true, false, true)
	o._putProp("toLocaleLowerCase", r.newNativeFunc(r.stringproto_toLowerCase, nil, "toLocaleLowerCase", nil, 0), true, false, true)
	o._putProp("toUpperCase", r.newNativeFunc(r.stringproto_toUpperCase, nil, "toUpperCase", nil, 0), true, false, true)
	o._putProp("toLocaleUpperCase", r.newNativeFunc(r.stringproto_toUpperCase, nil, "toLocaleUpperCase", nil, 0), true, false, true)
	o._putProp("trim", r.newNativeFunc(r.stringproto_trim, nil, "trim", nil, 0), true, false, true)

	// Annex B
	o._putProp("substr", r.newNativeFunc(r.stringproto_substr, nil, "substr", nil, 2), true, false, true)

	r.global.String = r.newNativeFunc(r.builtin_String, r.builtin_newString, "String", r.global.StringPrototype, 1)
	o = r.global.String.self
	o._putProp("fromCharCode", r.newNativeFunc(r.string_fromcharcode, nil, "fromCharCode", nil, 1), true, false, true)

	r.addToGlobal("String", r.global.String)

	r.stringSingleton = r.builtin_new(r.global.String, nil).self.(*stringObject)
}
