package goja

import (
	"math"
	"reflect"
	"regexp"
	"strconv"
)

var (
	valueFalse    Value = valueBool(false)
	valueTrue     Value = valueBool(true)
	_null         Value = valueNull{}
	_NaN          Value = valueFloat(math.NaN())
	_positiveInf  Value = valueFloat(math.Inf(+1))
	_negativeInf  Value = valueFloat(math.Inf(-1))
	_positiveZero Value
	_negativeZero Value = valueFloat(math.Float64frombits(0 | (1 << 63)))
	_epsilon            = valueFloat(2.2204460492503130808472633361816e-16)
	_undefined    Value = valueUndefined{}
)

var (
	reflectTypeInt    = reflect.TypeOf(int64(0))
	reflectTypeBool   = reflect.TypeOf(false)
	reflectTypeNil    = reflect.TypeOf(nil)
	reflectTypeFloat  = reflect.TypeOf(float64(0))
	reflectTypeMap    = reflect.TypeOf(map[string]interface{}{})
	reflectTypeArray  = reflect.TypeOf([]interface{}{})
	reflectTypeString = reflect.TypeOf("")
)

var intCache [256]Value

type Value interface {
	ToInteger() int64
	ToString() valueString
	String() string
	ToFloat() float64
	ToNumber() Value
	ToBoolean() bool
	ToObject(*Runtime) *Object
	SameAs(Value) bool
	Equals(Value) bool
	StrictEquals(Value) bool
	Export() interface{}
	ExportType() reflect.Type

	assertInt() (int64, bool)
	assertString() (valueString, bool)
	assertFloat() (float64, bool)

	baseObject(r *Runtime) *Object
}

type valueInt int64
type valueFloat float64
type valueBool bool
type valueNull struct{}
type valueUndefined struct {
	valueNull
}

type valueUnresolved struct {
	r   *Runtime
	ref string
}

type memberUnresolved struct {
	valueUnresolved
}

type valueProperty struct {
	value        Value
	writable     bool
	configurable bool
	enumerable   bool
	accessor     bool
	getterFunc   *Object
	setterFunc   *Object
}

func propGetter(o Value, v Value, r *Runtime) *Object {
	if v == _undefined {
		return nil
	}
	if obj, ok := v.(*Object); ok {
		if _, ok := obj.self.assertCallable(); ok {
			return obj
		}
	}
	r.typeErrorResult(true, "Getter must be a function: %s", v.ToString())
	return nil
}

func propSetter(o Value, v Value, r *Runtime) *Object {
	if v == _undefined {
		return nil
	}
	if obj, ok := v.(*Object); ok {
		if _, ok := obj.self.assertCallable(); ok {
			return obj
		}
	}
	r.typeErrorResult(true, "Setter must be a function: %s", v.ToString())
	return nil
}

func (i valueInt) ToInteger() int64 {
	return int64(i)
}

func (i valueInt) ToString() valueString {
	return asciiString(i.String())
}

func (i valueInt) String() string {
	return strconv.FormatInt(int64(i), 10)
}

func (i valueInt) ToFloat() float64 {
	return float64(int64(i))
}

func (i valueInt) ToBoolean() bool {
	return i != 0
}

func (i valueInt) ToObject(r *Runtime) *Object {
	return r.newPrimitiveObject(i, r.global.NumberPrototype, classNumber)
}

func (i valueInt) ToNumber() Value {
	return i
}

func (i valueInt) SameAs(other Value) bool {
	if otherInt, ok := other.assertInt(); ok {
		return int64(i) == otherInt
	}
	return false
}

func (i valueInt) Equals(other Value) bool {
	if o, ok := other.assertInt(); ok {
		return int64(i) == o
	}
	if o, ok := other.assertFloat(); ok {
		return float64(i) == o
	}
	if o, ok := other.assertString(); ok {
		return o.ToNumber().Equals(i)
	}
	if o, ok := other.(valueBool); ok {
		return int64(i) == o.ToInteger()
	}
	if o, ok := other.(*Object); ok {
		return i.Equals(o.self.toPrimitiveNumber())
	}
	return false
}

func (i valueInt) StrictEquals(other Value) bool {
	if otherInt, ok := other.assertInt(); ok {
		return int64(i) == otherInt
	} else if otherFloat, ok := other.assertFloat(); ok {
		return float64(i) == otherFloat
	}
	return false
}

func (i valueInt) assertInt() (int64, bool) {
	return int64(i), true
}

func (i valueInt) assertFloat() (float64, bool) {
	return 0, false
}

func (i valueInt) assertString() (valueString, bool) {
	return nil, false
}

func (i valueInt) baseObject(r *Runtime) *Object {
	return r.global.NumberPrototype
}

func (i valueInt) Export() interface{} {
	return int64(i)
}

func (i valueInt) ExportType() reflect.Type {
	return reflectTypeInt
}

func (o valueBool) ToInteger() int64 {
	if o {
		return 1
	}
	return 0
}

func (o valueBool) ToString() valueString {
	if o {
		return stringTrue
	}
	return stringFalse
}

func (o valueBool) String() string {
	if o {
		return "true"
	}
	return "false"
}

func (o valueBool) ToFloat() float64 {
	if o {
		return 1.0
	}
	return 0
}

func (o valueBool) ToBoolean() bool {
	return bool(o)
}

func (o valueBool) ToObject(r *Runtime) *Object {
	return r.newPrimitiveObject(o, r.global.BooleanPrototype, "Boolean")
}

func (o valueBool) ToNumber() Value {
	if o {
		return valueInt(1)
	}
	return valueInt(0)
}

func (o valueBool) SameAs(other Value) bool {
	if other, ok := other.(valueBool); ok {
		return o == other
	}
	return false
}

func (b valueBool) Equals(other Value) bool {
	if o, ok := other.(valueBool); ok {
		return b == o
	}

	if b {
		return other.Equals(intToValue(1))
	} else {
		return other.Equals(intToValue(0))
	}

}

func (o valueBool) StrictEquals(other Value) bool {
	if other, ok := other.(valueBool); ok {
		return o == other
	}
	return false
}

func (o valueBool) assertInt() (int64, bool) {
	return 0, false
}

func (o valueBool) assertFloat() (float64, bool) {
	return 0, false
}

func (o valueBool) assertString() (valueString, bool) {
	return nil, false
}

func (o valueBool) baseObject(r *Runtime) *Object {
	return r.global.BooleanPrototype
}

func (o valueBool) Export() interface{} {
	return bool(o)
}

func (o valueBool) ExportType() reflect.Type {
	return reflectTypeBool
}

func (n valueNull) ToInteger() int64 {
	return 0
}

func (n valueNull) ToString() valueString {
	return stringNull
}

func (n valueNull) String() string {
	return "null"
}

func (u valueUndefined) ToString() valueString {
	return stringUndefined
}

func (u valueUndefined) String() string {
	return "undefined"
}

func (u valueUndefined) ToNumber() Value {
	return _NaN
}

func (u valueUndefined) SameAs(other Value) bool {
	_, same := other.(valueUndefined)
	return same
}

func (u valueUndefined) StrictEquals(other Value) bool {
	_, same := other.(valueUndefined)
	return same
}

func (u valueUndefined) ToFloat() float64 {
	return math.NaN()
}

func (n valueNull) ToFloat() float64 {
	return 0
}

func (n valueNull) ToBoolean() bool {
	return false
}

func (n valueNull) ToObject(r *Runtime) *Object {
	r.typeErrorResult(true, "Cannot convert undefined or null to object")
	return nil
	//return r.newObject()
}

func (n valueNull) ToNumber() Value {
	return intToValue(0)
}

func (n valueNull) SameAs(other Value) bool {
	_, same := other.(valueNull)
	return same
}

func (n valueNull) Equals(other Value) bool {
	switch other.(type) {
	case valueUndefined, valueNull:
		return true
	}
	return false
}

func (n valueNull) StrictEquals(other Value) bool {
	_, same := other.(valueNull)
	return same
}

func (n valueNull) assertInt() (int64, bool) {
	return 0, false
}

func (n valueNull) assertFloat() (float64, bool) {
	return 0, false
}

func (n valueNull) assertString() (valueString, bool) {
	return nil, false
}

func (n valueNull) baseObject(r *Runtime) *Object {
	return nil
}

func (n valueNull) Export() interface{} {
	return nil
}

func (n valueNull) ExportType() reflect.Type {
	return reflectTypeNil
}

func (p *valueProperty) ToInteger() int64 {
	return 0
}

func (p *valueProperty) ToString() valueString {
	return stringEmpty
}

func (p *valueProperty) String() string {
	return ""
}

func (p *valueProperty) ToFloat() float64 {
	return math.NaN()
}

func (p *valueProperty) ToBoolean() bool {
	return false
}

func (p *valueProperty) ToObject(r *Runtime) *Object {
	return nil
}

func (p *valueProperty) ToNumber() Value {
	return nil
}

func (p *valueProperty) assertInt() (int64, bool) {
	return 0, false
}

func (p *valueProperty) assertFloat() (float64, bool) {
	return 0, false
}

func (p *valueProperty) assertString() (valueString, bool) {
	return nil, false
}

func (p *valueProperty) isWritable() bool {
	return p.writable || p.setterFunc != nil
}

func (p *valueProperty) get(this Value) Value {
	if p.getterFunc == nil {
		if p.value != nil {
			return p.value
		}
		return _undefined
	}
	call, _ := p.getterFunc.self.assertCallable()
	return call(FunctionCall{
		This: this,
	})
}

func (p *valueProperty) set(this, v Value) {
	if p.setterFunc == nil {
		p.value = v
		return
	}
	call, _ := p.setterFunc.self.assertCallable()
	call(FunctionCall{
		This:      this,
		Arguments: []Value{v},
	})
}

func (p *valueProperty) SameAs(other Value) bool {
	if otherProp, ok := other.(*valueProperty); ok {
		return p == otherProp
	}
	return false
}

func (p *valueProperty) Equals(other Value) bool {
	return false
}

func (p *valueProperty) StrictEquals(other Value) bool {
	return false
}

func (n *valueProperty) baseObject(r *Runtime) *Object {
	r.typeErrorResult(true, "BUG: baseObject() is called on valueProperty") // TODO error message
	return nil
}

func (n *valueProperty) Export() interface{} {
	panic("Cannot export valueProperty")
}

func (n *valueProperty) ExportType() reflect.Type {
	panic("Cannot export valueProperty")
}

func (f valueFloat) ToInteger() int64 {
	switch {
	case math.IsNaN(float64(f)):
		return 0
	case math.IsInf(float64(f), 1):
		return int64(math.MaxInt64)
	case math.IsInf(float64(f), -1):
		return int64(math.MinInt64)
	}
	return int64(f)
}

func (f valueFloat) ToString() valueString {
	return asciiString(f.String())
}

var matchLeading0Exponent = regexp.MustCompile(`([eE][\+\-])0+([1-9])`) // 1e-07 => 1e-7

func (f valueFloat) String() string {
	value := float64(f)
	if math.IsNaN(value) {
		return "NaN"
	} else if math.IsInf(value, 0) {
		if math.Signbit(value) {
			return "-Infinity"
		}
		return "Infinity"
	} else if f == _negativeZero {
		return "0"
	}
	exponent := math.Log10(math.Abs(value))
	if exponent >= 21 || exponent < -6 {
		return matchLeading0Exponent.ReplaceAllString(strconv.FormatFloat(value, 'g', -1, 64), "$1$2")
	}
	return strconv.FormatFloat(value, 'f', -1, 64)
}

func (f valueFloat) ToFloat() float64 {
	return float64(f)
}

func (f valueFloat) ToBoolean() bool {
	return float64(f) != 0.0 && !math.IsNaN(float64(f))
}

func (f valueFloat) ToObject(r *Runtime) *Object {
	return r.newPrimitiveObject(f, r.global.NumberPrototype, "Number")
}

func (f valueFloat) ToNumber() Value {
	return f
}

func (f valueFloat) SameAs(other Value) bool {
	if o, ok := other.assertFloat(); ok {
		this := float64(f)
		if math.IsNaN(this) && math.IsNaN(o) {
			return true
		} else {
			ret := this == o
			if ret && this == 0 {
				ret = math.Signbit(this) == math.Signbit(o)
			}
			return ret
		}
	} else if o, ok := other.assertInt(); ok {
		this := float64(f)
		ret := this == float64(o)
		if ret && this == 0 {
			ret = !math.Signbit(this)
		}
		return ret
	}
	return false
}

func (f valueFloat) Equals(other Value) bool {
	if o, ok := other.assertFloat(); ok {
		return float64(f) == o
	}

	if o, ok := other.assertInt(); ok {
		return float64(f) == float64(o)
	}

	if _, ok := other.assertString(); ok {
		return float64(f) == other.ToFloat()
	}

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

	if o, ok := other.(*Object); ok {
		return f.Equals(o.self.toPrimitiveNumber())
	}

	return false
}

func (f valueFloat) StrictEquals(other Value) bool {
	if o, ok := other.assertFloat(); ok {
		return float64(f) == o
	} else if o, ok := other.assertInt(); ok {
		return float64(f) == float64(o)
	}
	return false
}

func (f valueFloat) assertInt() (int64, bool) {
	return 0, false
}

func (f valueFloat) assertFloat() (float64, bool) {
	return float64(f), true
}

func (f valueFloat) assertString() (valueString, bool) {
	return nil, false
}

func (f valueFloat) baseObject(r *Runtime) *Object {
	return r.global.NumberPrototype
}

func (f valueFloat) Export() interface{} {
	return float64(f)
}

func (f valueFloat) ExportType() reflect.Type {
	return reflectTypeFloat
}

func (o *Object) ToInteger() int64 {
	return o.self.toPrimitiveNumber().ToNumber().ToInteger()
}

func (o *Object) ToString() valueString {
	return o.self.toPrimitiveString().ToString()
}

func (o *Object) String() string {
	return o.self.toPrimitiveString().String()
}

func (o *Object) ToFloat() float64 {
	return o.self.toPrimitiveNumber().ToFloat()
}

func (o *Object) ToBoolean() bool {
	return true
}

func (o *Object) ToObject(r *Runtime) *Object {
	return o
}

func (o *Object) ToNumber() Value {
	return o.self.toPrimitiveNumber().ToNumber()
}

func (o *Object) SameAs(other Value) bool {
	if other, ok := other.(*Object); ok {
		return o == other
	}
	return false
}

func (o *Object) Equals(other Value) bool {
	if other, ok := other.(*Object); ok {
		return o == other || o.self.equal(other.self)
	}

	if _, ok := other.assertInt(); ok {
		return o.self.toPrimitive().Equals(other)
	}

	if _, ok := other.assertFloat(); ok {
		return o.self.toPrimitive().Equals(other)
	}

	if other, ok := other.(valueBool); ok {
		return o.Equals(other.ToNumber())
	}

	if _, ok := other.assertString(); ok {
		return o.self.toPrimitive().Equals(other)
	}
	return false
}

func (o *Object) StrictEquals(other Value) bool {
	if other, ok := other.(*Object); ok {
		return o == other || o.self.equal(other.self)
	}
	return false
}

func (o *Object) assertInt() (int64, bool) {
	return 0, false
}

func (o *Object) assertFloat() (float64, bool) {
	return 0, false
}

func (o *Object) assertString() (valueString, bool) {
	return nil, false
}

func (o *Object) baseObject(r *Runtime) *Object {
	return o
}

func (o *Object) Export() interface{} {
	return o.self.export()
}

func (o *Object) ExportType() reflect.Type {
	return o.self.exportType()
}

func (o *Object) Get(name string) Value {
	return o.self.getStr(name)
}

func (o *Object) Keys() (keys []string) {
	for item, f := o.self.enumerate(false, false)(); f != nil; item, f = f() {
		keys = append(keys, item.name)
	}

	return
}

// DefineDataProperty is a Go equivalent of Object.defineProperty(o, name, {value: value, writable: writable,
// configurable: configurable, enumerable: enumerable})
func (o *Object) DefineDataProperty(name string, value Value, writable, configurable, enumerable Flag) error {
	return tryFunc(func() {
		o.self.defineOwnProperty(newStringValue(name), propertyDescr{
			Value:        value,
			Writable:     writable,
			Configurable: configurable,
			Enumerable:   enumerable,
		}, true)
	})
}

// DefineAccessorProperty is a Go equivalent of Object.defineProperty(o, name, {get: getter, set: setter,
// configurable: configurable, enumerable: enumerable})
func (o *Object) DefineAccessorProperty(name string, getter, setter Value, configurable, enumerable Flag) error {
	return tryFunc(func() {
		o.self.defineOwnProperty(newStringValue(name), propertyDescr{
			Getter:       getter,
			Setter:       setter,
			Configurable: configurable,
			Enumerable:   enumerable,
		}, true)
	})
}

func (o *Object) Set(name string, value interface{}) error {
	return tryFunc(func() {
		o.self.putStr(name, o.runtime.ToValue(value), true)
	})
}

// MarshalJSON returns JSON representation of the Object. It is equivalent to JSON.stringify(o).
// Note, this implements json.Marshaler so that json.Marshal() can be used without the need to Export().
func (o *Object) MarshalJSON() ([]byte, error) {
	ctx := _builtinJSON_stringifyContext{
		r: o.runtime,
	}
	ex := o.runtime.vm.try(func() {
		if !ctx.do(o) {
			ctx.buf.WriteString("null")
		}
	})
	if ex != nil {
		return nil, ex
	}
	return ctx.buf.Bytes(), nil
}

func (o valueUnresolved) throw() {
	o.r.throwReferenceError(o.ref)
}

func (o valueUnresolved) ToInteger() int64 {
	o.throw()
	return 0
}

func (o valueUnresolved) ToString() valueString {
	o.throw()
	return nil
}

func (o valueUnresolved) String() string {
	o.throw()
	return ""
}

func (o valueUnresolved) ToFloat() float64 {
	o.throw()
	return 0
}

func (o valueUnresolved) ToBoolean() bool {
	o.throw()
	return false
}

func (o valueUnresolved) ToObject(r *Runtime) *Object {
	o.throw()
	return nil
}

func (o valueUnresolved) ToNumber() Value {
	o.throw()
	return nil
}

func (o valueUnresolved) SameAs(other Value) bool {
	o.throw()
	return false
}

func (o valueUnresolved) Equals(other Value) bool {
	o.throw()
	return false
}

func (o valueUnresolved) StrictEquals(other Value) bool {
	o.throw()
	return false
}

func (o valueUnresolved) assertInt() (int64, bool) {
	o.throw()
	return 0, false
}

func (o valueUnresolved) assertFloat() (float64, bool) {
	o.throw()
	return 0, false
}

func (o valueUnresolved) assertString() (valueString, bool) {
	o.throw()
	return nil, false
}

func (o valueUnresolved) baseObject(r *Runtime) *Object {
	o.throw()
	return nil
}

func (o valueUnresolved) Export() interface{} {
	o.throw()
	return nil
}

func (o valueUnresolved) ExportType() reflect.Type {
	o.throw()
	return nil
}

func init() {
	for i := 0; i < 256; i++ {
		intCache[i] = valueInt(i - 128)
	}
	_positiveZero = intToValue(0)
}
