package goja

import "reflect"

const (
	classObject   = "Object"
	classArray    = "Array"
	classFunction = "Function"
	classNumber   = "Number"
	classString   = "String"
	classBoolean  = "Boolean"
	classError    = "Error"
	classRegExp   = "RegExp"
	classDate     = "Date"
)

type Object struct {
	runtime *Runtime
	self    objectImpl
}

type iterNextFunc func() (propIterItem, iterNextFunc)

type objectImpl interface {
	sortable
	className() string
	get(Value) Value
	getProp(Value) Value
	getPropStr(string) Value
	getStr(string) Value
	getOwnProp(string) Value
	put(Value, Value, bool)
	putStr(string, Value, bool)
	hasProperty(Value) bool
	hasPropertyStr(string) bool
	hasOwnProperty(Value) bool
	hasOwnPropertyStr(string) bool
	_putProp(name string, value Value, writable, enumerable, configurable bool) Value
	defineOwnProperty(name Value, descr objectImpl, throw bool) bool
	toPrimitiveNumber() Value
	toPrimitiveString() Value
	toPrimitive() Value
	assertCallable() (call func(FunctionCall) Value, ok bool)
	// defineOwnProperty(Value, property, bool) bool
	deleteStr(name string, throw bool) bool
	delete(name Value, throw bool) bool
	proto() *Object
	hasInstance(v Value) bool
	isExtensible() bool
	preventExtensions()
	enumerate(all, recusrive bool) iterNextFunc
	_enumerate(recursive bool) iterNextFunc
	export() interface{}
	exportType() reflect.Type
	equal(objectImpl) bool

	// clone(*_object, *_object, *_clone) *_object
	// marshalJSON() json.Marshaler

}

type baseObject struct {
	class      string
	val        *Object
	prototype  *Object
	extensible bool

	values    map[string]Value
	propNames []string
}

type primitiveValueObject struct {
	baseObject
	pValue Value
}

func (o *primitiveValueObject) export() interface{} {
	return o.pValue.Export()
}

func (o *primitiveValueObject) exportType() reflect.Type {
	return o.pValue.ExportType()
}

type FunctionCall struct {
	This      Value
	Arguments []Value
}

func (f FunctionCall) Argument(idx int) Value {
	if idx < len(f.Arguments) {
		return f.Arguments[idx]
	}
	return _undefined
}

func (o *baseObject) init() {
	o.values = make(map[string]Value)
}

func (o *baseObject) className() string {
	return o.class
}

func (o *baseObject) getPropStr(name string) Value {
	if val := o.getOwnProp(name); val != nil {
		return val
	}
	if o.prototype != nil {
		return o.prototype.self.getPropStr(name)
	}
	return nil
}

func (o *baseObject) getProp(n Value) Value {
	return o.val.self.getPropStr(n.String())
}

func (o *baseObject) hasProperty(n Value) bool {
	return o.val.self.getProp(n) != nil
}

func (o *baseObject) hasPropertyStr(name string) bool {
	return o.val.self.getPropStr(name) != nil
}

func (o *baseObject) _getStr(name string) Value {
	p := o.getOwnProp(name)

	if p == nil && o.prototype != nil {
		p = o.prototype.self.getPropStr(name)
	}

	if p, ok := p.(*valueProperty); ok {
		return p.get(o.val)
	}

	return p
}

func (o *baseObject) getStr(name string) Value {
	p := o.val.self.getPropStr(name)
	if p, ok := p.(*valueProperty); ok {
		return p.get(o.val)
	}

	return p
}

func (o *baseObject) get(n Value) Value {
	return o.getStr(n.String())
}

func (o *baseObject) checkDeleteProp(name string, prop *valueProperty, throw bool) bool {
	if !prop.configurable {
		o.val.runtime.typeErrorResult(throw, "Cannot delete property '%s' of %s", name, o.val.ToString())
		return false
	}
	return true
}

func (o *baseObject) checkDelete(name string, val Value, throw bool) bool {
	if val, ok := val.(*valueProperty); ok {
		return o.checkDeleteProp(name, val, throw)
	}
	return true
}

func (o *baseObject) _delete(name string) {
	delete(o.values, name)
	for i, n := range o.propNames {
		if n == name {
			copy(o.propNames[i:], o.propNames[i+1:])
			o.propNames = o.propNames[:len(o.propNames)-1]
			break
		}
	}
}

func (o *baseObject) deleteStr(name string, throw bool) bool {
	if val, exists := o.values[name]; exists {
		if !o.checkDelete(name, val, throw) {
			return false
		}
		o._delete(name)
		return true
	}
	return true
}

func (o *baseObject) delete(n Value, throw bool) bool {
	return o.deleteStr(n.String(), throw)
}

func (o *baseObject) put(n Value, val Value, throw bool) {
	o.putStr(n.String(), val, throw)
}

func (o *baseObject) getOwnProp(name string) Value {
	v := o.values[name]
	if v == nil && name == "__proto" {
		return o.prototype
	}
	return v
}

func (o *baseObject) putStr(name string, val Value, throw bool) {
	if v, exists := o.values[name]; exists {
		if prop, ok := v.(*valueProperty); ok {
			if !prop.isWritable() {
				o.val.runtime.typeErrorResult(throw, "Cannot assign to read only property '%s'", name)
				return
			}
			prop.set(o.val, val)
			return
		}
		o.values[name] = val
		return
	}

	if name == "__proto__" {
		if !o.extensible {
			o.val.runtime.typeErrorResult(throw, "%s is not extensible", o.val)
			return
		}
		if val == _undefined || val == _null {
			o.prototype = nil
			return
		} else {
			if val, ok := val.(*Object); ok {
				o.prototype = val
			}
		}
		return
	}

	var pprop Value
	if proto := o.prototype; proto != nil {
		pprop = proto.self.getPropStr(name)
	}

	if pprop != nil {
		if prop, ok := pprop.(*valueProperty); ok {
			if !prop.isWritable() {
				o.val.runtime.typeErrorResult(throw)
				return
			}
			if prop.accessor {
				prop.set(o.val, val)
				return
			}
		}
	} else {
		if !o.extensible {
			o.val.runtime.typeErrorResult(throw)
			return
		}
	}

	o.values[name] = val
	o.propNames = append(o.propNames, name)
}

func (o *baseObject) hasOwnProperty(n Value) bool {
	v := o.values[n.String()]
	return v != nil
}

func (o *baseObject) hasOwnPropertyStr(name string) bool {
	v := o.values[name]
	return v != nil
}

func (o *baseObject) _defineOwnProperty(name Value, existingValue Value, descr objectImpl, throw bool) (val Value, ok bool) {
	var hasWritable, hasEnumerable, hasConfigurable bool
	var writable, enumerable, configurable bool

	value := descr.getStr("value")

	if p := descr.getStr("writable"); p != nil {
		hasWritable = true
		writable = p.ToBoolean()
	}
	if p := descr.getStr("enumerable"); p != nil {
		hasEnumerable = true
		enumerable = p.ToBoolean()
	}
	if p := descr.getStr("configurable"); p != nil {
		hasConfigurable = true
		configurable = p.ToBoolean()
	}

	getter := descr.getStr("get")
	setter := descr.getStr("set")

	if (getter != nil || setter != nil) && (value != nil || hasWritable) {
		o.val.runtime.typeErrorResult(throw, "Invalid property descriptor. Cannot both specify accessors and a value or writable attribute")
		return nil, false
	}

	getterObj, _ := getter.(*Object)
	setterObj, _ := setter.(*Object)

	var existing *valueProperty

	if existingValue == nil {
		if !o.extensible {
			o.val.runtime.typeErrorResult(throw)
			return nil, false
		}
		existing = &valueProperty{}
	} else {
		if existing, ok = existingValue.(*valueProperty); !ok {
			existing = &valueProperty{
				writable:     true,
				enumerable:   true,
				configurable: true,
				value:        existingValue,
			}
		}

		if !existing.configurable {
			if configurable {
				goto Reject
			}
			if hasEnumerable && enumerable != existing.enumerable {
				goto Reject
			}
		}
		if existing.accessor && value != nil || !existing.accessor && (getterObj != nil || setterObj != nil) {
			if !existing.configurable {
				goto Reject
			}
		} else if !existing.accessor {
			if !existing.configurable {
				if !existing.writable {
					if writable {
						goto Reject
					}
					if value != nil && !value.SameAs(existing.value) {
						goto Reject
					}
				}
			}
		} else {
			if !existing.configurable {
				if getter != nil && existing.getterFunc != getterObj || setter != nil && existing.setterFunc != setterObj {
					goto Reject
				}
			}
		}
	}

	if writable && enumerable && configurable && value != nil {
		return value, true
	}

	if hasWritable {
		existing.writable = writable
	}
	if hasEnumerable {
		existing.enumerable = enumerable
	}
	if hasConfigurable {
		existing.configurable = configurable
	}

	if value != nil {
		existing.value = value
		existing.getterFunc = nil
		existing.setterFunc = nil
	}

	if value != nil || hasWritable {
		existing.accessor = false
	}

	if getter != nil {
		existing.getterFunc = propGetter(o.val, getter, o.val.runtime)
		existing.value = nil
		existing.accessor = true
	}

	if setter != nil {
		existing.setterFunc = propSetter(o.val, setter, o.val.runtime)
		existing.value = nil
		existing.accessor = true
	}

	if !existing.accessor && existing.value == nil {
		existing.value = _undefined
	}

	return existing, true

Reject:
	o.val.runtime.typeErrorResult(throw, "Cannot redefine property: %s", name.ToString())
	return nil, false
}

func (o *baseObject) defineOwnProperty(n Value, descr objectImpl, throw bool) bool {
	name := n.String()
	val := o.values[name]
	if v, ok := o._defineOwnProperty(n, val, descr, throw); ok {
		o.values[name] = v
		if val == nil {
			o.propNames = append(o.propNames, name)
		}
		return true
	}
	return false
}

func (o *baseObject) _put(name string, v Value) {
	if _, exists := o.values[name]; !exists {
		o.propNames = append(o.propNames, name)
	}

	o.values[name] = v
}

func (o *baseObject) _putProp(name string, value Value, writable, enumerable, configurable bool) Value {
	if writable && enumerable && configurable {
		o._put(name, value)
		return value
	} else {
		p := &valueProperty{
			value:        value,
			writable:     writable,
			enumerable:   enumerable,
			configurable: configurable,
		}
		o._put(name, p)
		return p
	}
}

func (o *baseObject) tryPrimitive(methodName string) Value {
	if method, ok := o.getStr(methodName).(*Object); ok {
		if call, ok := method.self.assertCallable(); ok {
			v := call(FunctionCall{
				This: o.val,
			})
			if _, fail := v.(*Object); !fail {
				return v
			}
		}
	}
	return nil
}

func (o *baseObject) toPrimitiveNumber() Value {
	if v := o.tryPrimitive("valueOf"); v != nil {
		return v
	}

	if v := o.tryPrimitive("toString"); v != nil {
		return v
	}

	o.val.runtime.typeErrorResult(true, "Could not convert %v to primitive", o)
	return nil
}

func (o *baseObject) toPrimitiveString() Value {
	if v := o.tryPrimitive("toString"); v != nil {
		return v
	}

	if v := o.tryPrimitive("valueOf"); v != nil {
		return v
	}

	o.val.runtime.typeErrorResult(true, "Could not convert %v to primitive", o)
	return nil
}

func (o *baseObject) toPrimitive() Value {
	return o.toPrimitiveNumber()
}

func (o *baseObject) assertCallable() (func(FunctionCall) Value, bool) {
	return nil, false
}

func (o *baseObject) proto() *Object {
	return o.prototype
}

func (o *baseObject) isExtensible() bool {
	return o.extensible
}

func (o *baseObject) preventExtensions() {
	o.extensible = false
}

func (o *baseObject) sortLen() int64 {
	return toLength(o.val.self.getStr("length"))
}

func (o *baseObject) sortGet(i int64) Value {
	return o.val.self.get(intToValue(i))
}

func (o *baseObject) swap(i, j int64) {
	ii := intToValue(i)
	jj := intToValue(j)

	x := o.val.self.get(ii)
	y := o.val.self.get(jj)

	o.val.self.put(ii, y, false)
	o.val.self.put(jj, x, false)
}

func (o *baseObject) export() interface{} {
	m := make(map[string]interface{})

	for item, f := o.enumerate(false, false)(); f != nil; item, f = f() {
		v := item.value
		if v == nil {
			v = o.getStr(item.name)
		}
		if v != nil {
			m[item.name] = v.Export()
		} else {
			m[item.name] = nil
		}
	}
	return m
}

func (o *baseObject) exportType() reflect.Type {
	return reflectTypeMap
}

type enumerableFlag int

const (
	_ENUM_UNKNOWN enumerableFlag = iota
	_ENUM_FALSE
	_ENUM_TRUE
)

type propIterItem struct {
	name       string
	value      Value // set only when enumerable == _ENUM_UNKNOWN
	enumerable enumerableFlag
}

type objectPropIter struct {
	o         *baseObject
	propNames []string
	recursive bool
	idx       int
}

type propFilterIter struct {
	wrapped iterNextFunc
	all     bool
	seen    map[string]bool
}

func (i *propFilterIter) next() (propIterItem, iterNextFunc) {
	for {
		var item propIterItem
		item, i.wrapped = i.wrapped()
		if i.wrapped == nil {
			return propIterItem{}, nil
		}

		if !i.seen[item.name] {
			i.seen[item.name] = true
			if !i.all {
				if item.enumerable == _ENUM_FALSE {
					continue
				}
				if item.enumerable == _ENUM_UNKNOWN {
					if prop, ok := item.value.(*valueProperty); ok {
						if !prop.enumerable {
							continue
						}
					}
				}
			}
			return item, i.next
		}
	}
}

func (i *objectPropIter) next() (propIterItem, iterNextFunc) {
	for i.idx < len(i.propNames) {
		name := i.propNames[i.idx]
		i.idx++
		prop := i.o.values[name]
		if prop != nil {
			return propIterItem{name: name, value: prop}, i.next
		}
	}

	if i.recursive && i.o.prototype != nil {
		return i.o.prototype.self._enumerate(i.recursive)()
	}
	return propIterItem{}, nil
}

func (o *baseObject) _enumerate(recusrive bool) iterNextFunc {
	propNames := make([]string, len(o.propNames))
	copy(propNames, o.propNames)
	return (&objectPropIter{
		o:         o,
		propNames: propNames,
		recursive: recusrive,
	}).next
}

func (o *baseObject) enumerate(all, recursive bool) iterNextFunc {
	return (&propFilterIter{
		wrapped: o._enumerate(recursive),
		all:     all,
		seen:    make(map[string]bool),
	}).next
}

func (o *baseObject) equal(other objectImpl) bool {
	// Rely on parent reference comparison
	return false
}

func (o *baseObject) hasInstance(v Value) bool {
	o.val.runtime.typeErrorResult(true, "Expecting a function in instanceof check, but got %s", o.val.ToString())
	panic("Unreachable")
}
