package goja

import (
	"hash/maphash"
	"io"
	"math"
	"math/big"
	"reflect"
	"strconv"
	"strings"

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

type asciiString string

type asciiRuneReader struct {
	s   asciiString
	pos int
}

func (rr *asciiRuneReader) ReadRune() (r rune, size int, err error) {
	if rr.pos < len(rr.s) {
		r = rune(rr.s[rr.pos])
		size = 1
		rr.pos++
	} else {
		err = io.EOF
	}
	return
}

type asciiUtf16Reader struct {
	s   asciiString
	pos int
}

func (rr *asciiUtf16Reader) readChar() (c uint16, err error) {
	if rr.pos < len(rr.s) {
		c = uint16(rr.s[rr.pos])
		rr.pos++
	} else {
		err = io.EOF
	}
	return
}

func (rr *asciiUtf16Reader) ReadRune() (r rune, size int, err error) {
	if rr.pos < len(rr.s) {
		r = rune(rr.s[rr.pos])
		rr.pos++
		size = 1
	} else {
		err = io.EOF
	}
	return
}

func (s asciiString) Reader() io.RuneReader {
	return &asciiRuneReader{
		s: s,
	}
}

func (s asciiString) utf16Reader() utf16Reader {
	return &asciiUtf16Reader{
		s: s,
	}
}

func (s asciiString) utf16RuneReader() io.RuneReader {
	return &asciiUtf16Reader{
		s: s,
	}
}

func (s asciiString) utf16Runes() []rune {
	runes := make([]rune, len(s))
	for i := 0; i < len(s); i++ {
		runes[i] = rune(s[i])
	}
	return runes
}

// ss must be trimmed
func stringToInt(ss string) (int64, error) {
	if ss == "" {
		return 0, nil
	}
	if ss == "-0" {
		return 0, strconv.ErrSyntax
	}
	if len(ss) > 2 {
		switch ss[:2] {
		case "0x", "0X":
			return strconv.ParseInt(ss[2:], 16, 64)
		case "0b", "0B":
			return strconv.ParseInt(ss[2:], 2, 64)
		case "0o", "0O":
			return strconv.ParseInt(ss[2:], 8, 64)
		}
	}
	return strconv.ParseInt(ss, 10, 64)
}

func (s asciiString) _toInt(trimmed string) (int64, error) {
	return stringToInt(trimmed)
}

func isRangeErr(err error) bool {
	if err, ok := err.(*strconv.NumError); ok {
		return err.Err == strconv.ErrRange
	}
	return false
}

func (s asciiString) _toFloat(trimmed string) (float64, error) {
	if trimmed == "" {
		return 0, nil
	}
	if trimmed == "-0" {
		var f float64
		return -f, nil
	}

	// Go allows underscores in numbers, when parsed as floats, but ECMAScript expect them to be interpreted as NaN.
	if strings.ContainsRune(trimmed, '_') {
		return 0, strconv.ErrSyntax
	}

	// Hexadecimal floats are not supported by ECMAScript.
	if len(trimmed) >= 2 {
		var prefix string
		if trimmed[0] == '-' || trimmed[0] == '+' {
			prefix = trimmed[1:]
		} else {
			prefix = trimmed
		}
		if len(prefix) >= 2 && prefix[0] == '0' && (prefix[1] == 'x' || prefix[1] == 'X') {
			return 0, strconv.ErrSyntax
		}
	}

	f, err := strconv.ParseFloat(trimmed, 64)
	if err == nil && math.IsInf(f, 0) {
		ss := strings.ToLower(trimmed)
		if strings.HasPrefix(ss, "inf") || strings.HasPrefix(ss, "-inf") || strings.HasPrefix(ss, "+inf") {
			// We handle "Infinity" separately, prevent from being parsed as Infinity due to strconv.ParseFloat() permissive syntax
			return 0, strconv.ErrSyntax
		}
	}
	if isRangeErr(err) {
		err = nil
	}
	return f, err
}

func (s asciiString) ToInteger() int64 {
	ss := strings.TrimSpace(string(s))
	if ss == "" {
		return 0
	}
	if ss == "Infinity" || ss == "+Infinity" {
		return math.MaxInt64
	}
	if ss == "-Infinity" {
		return math.MinInt64
	}
	i, err := s._toInt(ss)
	if err != nil {
		f, err := s._toFloat(ss)
		if err == nil {
			return int64(f)
		}
	}
	return i
}

func (s asciiString) toString() String {
	return s
}

func (s asciiString) ToString() Value {
	return s
}

func (s asciiString) String() string {
	return string(s)
}

func (s asciiString) ToFloat() float64 {
	ss := strings.TrimSpace(string(s))
	if ss == "" {
		return 0
	}
	if ss == "Infinity" || ss == "+Infinity" {
		return math.Inf(1)
	}
	if ss == "-Infinity" {
		return math.Inf(-1)
	}
	f, err := s._toFloat(ss)
	if err != nil {
		i, err := s._toInt(ss)
		if err == nil {
			return float64(i)
		}
		f = math.NaN()
	}
	return f
}

func (s asciiString) ToBoolean() bool {
	return s != ""
}

func (s asciiString) ToNumber() Value {
	ss := strings.TrimSpace(string(s))
	if ss == "" {
		return intToValue(0)
	}
	if ss == "Infinity" || ss == "+Infinity" {
		return _positiveInf
	}
	if ss == "-Infinity" {
		return _negativeInf
	}

	if i, err := s._toInt(ss); err == nil {
		return intToValue(i)
	}

	if f, err := s._toFloat(ss); err == nil {
		return floatToValue(f)
	}

	return _NaN
}

func (s asciiString) ToObject(r *Runtime) *Object {
	return r._newString(s, r.getStringPrototype())
}

func (s asciiString) SameAs(other Value) bool {
	return s.StrictEquals(other)
}

func (s asciiString) Equals(other Value) bool {
	if s.StrictEquals(other) {
		return true
	}

	if o, ok := other.(valueInt); ok {
		if o1, e := s._toInt(strings.TrimSpace(string(s))); e == nil {
			return o1 == int64(o)
		}
		return false
	}

	if o, ok := other.(valueFloat); ok {
		return s.ToFloat() == float64(o)
	}

	if o, ok := other.(valueBool); ok {
		if o1, e := s._toFloat(strings.TrimSpace(string(s))); e == nil {
			return o1 == o.ToFloat()
		}
		return false
	}

	if o, ok := other.(*valueBigInt); ok {
		bigInt, err := stringToBigInt(s.toTrimmedUTF8())
		if err != nil {
			return false
		}
		return bigInt.Cmp((*big.Int)(o)) == 0
	}

	if o, ok := other.(*Object); ok {
		return s.Equals(o.toPrimitive())
	}
	return false
}

func (s asciiString) StrictEquals(other Value) bool {
	if otherStr, ok := other.(asciiString); ok {
		return s == otherStr
	}
	if otherStr, ok := other.(*importedString); ok {
		if otherStr.u == nil {
			return string(s) == otherStr.s
		}
	}
	return false
}

func (s asciiString) baseObject(r *Runtime) *Object {
	ss := r.getStringSingleton()
	ss.value = s
	ss.setLength()
	return ss.val
}

func (s asciiString) hash(hash *maphash.Hash) uint64 {
	_, _ = hash.WriteString(string(s))
	h := hash.Sum64()
	hash.Reset()
	return h
}

func (s asciiString) CharAt(idx int) uint16 {
	return uint16(s[idx])
}

func (s asciiString) Length() int {
	return len(s)
}

func (s asciiString) Concat(other String) String {
	a, u := devirtualizeString(other)
	if u != nil {
		b := make([]uint16, len(s)+len(u))
		b[0] = unistring.BOM
		for i := 0; i < len(s); i++ {
			b[i+1] = uint16(s[i])
		}
		copy(b[len(s)+1:], u[1:])
		return unicodeString(b)
	}
	return s + a
}

func (s asciiString) Substring(start, end int) String {
	return s[start:end]
}

func (s asciiString) CompareTo(other String) int {
	switch other := other.(type) {
	case asciiString:
		return strings.Compare(string(s), string(other))
	case unicodeString:
		return strings.Compare(string(s), other.String())
	case *importedString:
		return strings.Compare(string(s), other.s)
	default:
		panic(newTypeError("Internal bug: unknown string type: %T", other))
	}
}

func (s asciiString) index(substr String, start int) int {
	a, u := devirtualizeString(substr)
	if u == nil {
		if start > len(s) {
			return -1
		}
		p := strings.Index(string(s[start:]), string(a))
		if p >= 0 {
			return p + start
		}
	}
	return -1
}

func (s asciiString) lastIndex(substr String, pos int) int {
	a, u := devirtualizeString(substr)
	if u == nil {
		end := pos + len(a)
		var ss string
		if end > len(s) {
			ss = string(s)
		} else {
			ss = string(s[:end])
		}
		return strings.LastIndex(ss, string(a))
	}
	return -1
}

func (s asciiString) toLower() String {
	return asciiString(strings.ToLower(string(s)))
}

func (s asciiString) toUpper() String {
	return asciiString(strings.ToUpper(string(s)))
}

func (s asciiString) toTrimmedUTF8() string {
	return strings.TrimSpace(string(s))
}

func (s asciiString) string() unistring.String {
	return unistring.String(s)
}

func (s asciiString) Export() interface{} {
	return string(s)
}

func (s asciiString) ExportType() reflect.Type {
	return reflectTypeString
}
