package goja

import (
	"io"
	"strconv"
	"strings"
	"unicode/utf16"
	"unicode/utf8"

	"github.com/dop251/goja/unistring"
)

const (
	__proto__ = "__proto__"
)

var (
	stringTrue        valueString = asciiString("true")
	stringFalse       valueString = asciiString("false")
	stringNull        valueString = asciiString("null")
	stringUndefined   valueString = asciiString("undefined")
	stringObjectC     valueString = asciiString("object")
	stringFunction    valueString = asciiString("function")
	stringBoolean     valueString = asciiString("boolean")
	stringString      valueString = asciiString("string")
	stringSymbol      valueString = asciiString("symbol")
	stringNumber      valueString = asciiString("number")
	stringNaN         valueString = asciiString("NaN")
	stringInfinity                = asciiString("Infinity")
	stringNegInfinity             = asciiString("-Infinity")
	stringBound_      valueString = asciiString("bound ")
	stringEmpty       valueString = asciiString("")

	stringError          valueString = asciiString("Error")
	stringAggregateError valueString = asciiString("AggregateError")
	stringTypeError      valueString = asciiString("TypeError")
	stringReferenceError valueString = asciiString("ReferenceError")
	stringSyntaxError    valueString = asciiString("SyntaxError")
	stringRangeError     valueString = asciiString("RangeError")
	stringEvalError      valueString = asciiString("EvalError")
	stringURIError       valueString = asciiString("URIError")
	stringGoError        valueString = asciiString("GoError")

	stringObjectNull      valueString = asciiString("[object Null]")
	stringObjectObject    valueString = asciiString("[object Object]")
	stringObjectUndefined valueString = asciiString("[object Undefined]")
	stringInvalidDate     valueString = asciiString("Invalid Date")
)

type valueString interface {
	Value
	charAt(int) rune
	length() int
	concat(valueString) valueString
	substring(start, end int) valueString
	compareTo(valueString) int
	reader(start int) io.RuneReader
	utf16Reader(start int) io.RuneReader
	utf16Runes() []rune
	index(valueString, int) int
	lastIndex(valueString, int) int
	toLower() valueString
	toUpper() valueString
	toTrimmedUTF8() string
}

type stringIterObject struct {
	baseObject
	reader io.RuneReader
}

func isUTF16FirstSurrogate(r rune) bool {
	return r >= 0xD800 && r <= 0xDBFF
}

func isUTF16SecondSurrogate(r rune) bool {
	return r >= 0xDC00 && r <= 0xDFFF
}

func (si *stringIterObject) next() Value {
	if si.reader == nil {
		return si.val.runtime.createIterResultObject(_undefined, true)
	}
	r, _, err := si.reader.ReadRune()
	if err == io.EOF {
		si.reader = nil
		return si.val.runtime.createIterResultObject(_undefined, true)
	}
	return si.val.runtime.createIterResultObject(stringFromRune(r), false)
}

func stringFromRune(r rune) valueString {
	if r < utf8.RuneSelf {
		var sb strings.Builder
		sb.Grow(1)
		sb.WriteByte(byte(r))
		return asciiString(sb.String())
	}
	var sb unicodeStringBuilder
	if r <= 0xFFFF {
		sb.Grow(1)
	} else {
		sb.Grow(2)
	}
	sb.WriteRune(r)
	return sb.String()
}

func (r *Runtime) createStringIterator(s valueString) Value {
	o := &Object{runtime: r}

	si := &stringIterObject{
		reader: &lenientUtf16Decoder{utf16Reader: s.utf16Reader(0)},
	}
	si.class = classStringIterator
	si.val = o
	si.extensible = true
	o.self = si
	si.prototype = r.global.StringIteratorPrototype
	si.init()

	return o
}

type stringObject struct {
	baseObject
	value      valueString
	length     int
	lengthProp valueProperty
}

func newStringValue(s string) valueString {
	utf16Size := 0
	ascii := true
	for _, chr := range s {
		utf16Size++
		if chr >= utf8.RuneSelf {
			ascii = false
			if chr > 0xFFFF {
				utf16Size++
			}
		}
	}
	if ascii {
		return asciiString(s)
	}
	buf := make([]uint16, utf16Size+1)
	buf[0] = unistring.BOM
	c := 1
	for _, chr := range s {
		if chr <= 0xFFFF {
			buf[c] = uint16(chr)
		} else {
			first, second := utf16.EncodeRune(chr)
			buf[c] = uint16(first)
			c++
			buf[c] = uint16(second)
		}
		c++
	}
	return unicodeString(buf)
}

func stringValueFromRaw(raw unistring.String) valueString {
	if b := raw.AsUtf16(); b != nil {
		return unicodeString(b)
	}
	return asciiString(raw)
}

func (s *stringObject) init() {
	s.baseObject.init()
	s.setLength()
}

func (s *stringObject) setLength() {
	if s.value != nil {
		s.length = s.value.length()
	}
	s.lengthProp.value = intToValue(int64(s.length))
	s._put("length", &s.lengthProp)
}

func (s *stringObject) getStr(name unistring.String, receiver Value) Value {
	if i := strToGoIdx(name); i >= 0 && i < s.length {
		return s._getIdx(i)
	}
	return s.baseObject.getStr(name, receiver)
}

func (s *stringObject) getIdx(idx valueInt, receiver Value) Value {
	i := int(idx)
	if i >= 0 && i < s.length {
		return s._getIdx(i)
	}
	return s.baseObject.getStr(idx.string(), receiver)
}

func (s *stringObject) getOwnPropStr(name unistring.String) Value {
	if i := strToGoIdx(name); i >= 0 && i < s.length {
		val := s._getIdx(i)
		return &valueProperty{
			value:      val,
			enumerable: true,
		}
	}

	return s.baseObject.getOwnPropStr(name)
}

func (s *stringObject) getOwnPropIdx(idx valueInt) Value {
	i := int64(idx)
	if i >= 0 {
		if i < int64(s.length) {
			val := s._getIdx(int(i))
			return &valueProperty{
				value:      val,
				enumerable: true,
			}
		}
		return nil
	}

	return s.baseObject.getOwnPropStr(idx.string())
}

func (s *stringObject) _getIdx(idx int) Value {
	return s.value.substring(idx, idx+1)
}

func (s *stringObject) setOwnStr(name unistring.String, val Value, throw bool) bool {
	if i := strToGoIdx(name); i >= 0 && i < s.length {
		s.val.runtime.typeErrorResult(throw, "Cannot assign to read only property '%d' of a String", i)
		return false
	}

	return s.baseObject.setOwnStr(name, val, throw)
}

func (s *stringObject) setOwnIdx(idx valueInt, val Value, throw bool) bool {
	i := int64(idx)
	if i >= 0 && i < int64(s.length) {
		s.val.runtime.typeErrorResult(throw, "Cannot assign to read only property '%d' of a String", i)
		return false
	}

	return s.baseObject.setOwnStr(idx.string(), val, throw)
}

func (s *stringObject) setForeignStr(name unistring.String, val, receiver Value, throw bool) (bool, bool) {
	return s._setForeignStr(name, s.getOwnPropStr(name), val, receiver, throw)
}

func (s *stringObject) setForeignIdx(idx valueInt, val, receiver Value, throw bool) (bool, bool) {
	return s._setForeignIdx(idx, s.getOwnPropIdx(idx), val, receiver, throw)
}

func (s *stringObject) defineOwnPropertyStr(name unistring.String, descr PropertyDescriptor, throw bool) bool {
	if i := strToGoIdx(name); i >= 0 && i < s.length {
		_, ok := s._defineOwnProperty(name, &valueProperty{enumerable: true}, descr, throw)
		return ok
	}

	return s.baseObject.defineOwnPropertyStr(name, descr, throw)
}

func (s *stringObject) defineOwnPropertyIdx(idx valueInt, descr PropertyDescriptor, throw bool) bool {
	i := int64(idx)
	if i >= 0 && i < int64(s.length) {
		s.val.runtime.typeErrorResult(throw, "Cannot redefine property: %d", i)
		return false
	}

	return s.baseObject.defineOwnPropertyStr(idx.string(), descr, throw)
}

type stringPropIter struct {
	str         valueString // separate, because obj can be the singleton
	obj         *stringObject
	idx, length int
}

func (i *stringPropIter) next() (propIterItem, iterNextFunc) {
	if i.idx < i.length {
		name := strconv.Itoa(i.idx)
		i.idx++
		return propIterItem{name: asciiString(name), enumerable: _ENUM_TRUE}, i.next
	}

	return i.obj.baseObject.iterateStringKeys()()
}

func (s *stringObject) iterateStringKeys() iterNextFunc {
	return (&stringPropIter{
		str:    s.value,
		obj:    s,
		length: s.length,
	}).next
}

func (s *stringObject) stringKeys(all bool, accum []Value) []Value {
	for i := 0; i < s.length; i++ {
		accum = append(accum, asciiString(strconv.Itoa(i)))
	}

	return s.baseObject.stringKeys(all, accum)
}

func (s *stringObject) deleteStr(name unistring.String, throw bool) bool {
	if i := strToGoIdx(name); i >= 0 && i < s.length {
		s.val.runtime.typeErrorResult(throw, "Cannot delete property '%d' of a String", i)
		return false
	}

	return s.baseObject.deleteStr(name, throw)
}

func (s *stringObject) deleteIdx(idx valueInt, throw bool) bool {
	i := int64(idx)
	if i >= 0 && i < int64(s.length) {
		s.val.runtime.typeErrorResult(throw, "Cannot delete property '%d' of a String", i)
		return false
	}

	return s.baseObject.deleteStr(idx.string(), throw)
}

func (s *stringObject) hasOwnPropertyStr(name unistring.String) bool {
	if i := strToGoIdx(name); i >= 0 && i < s.length {
		return true
	}
	return s.baseObject.hasOwnPropertyStr(name)
}

func (s *stringObject) hasOwnPropertyIdx(idx valueInt) bool {
	i := int64(idx)
	if i >= 0 && i < int64(s.length) {
		return true
	}
	return s.baseObject.hasOwnPropertyStr(idx.string())
}
