package goja

import (
	"github.com/projectdiscovery/goja/unistring"
)

type nativeProxyHandler struct {
	handler *ProxyTrapConfig
}

func (h *nativeProxyHandler) getPrototypeOf(target *Object) (Value, bool) {
	if trap := h.handler.GetPrototypeOf; trap != nil {
		return trap(target), true
	}
	return nil, false
}

func (h *nativeProxyHandler) setPrototypeOf(target *Object, proto *Object) (bool, bool) {
	if trap := h.handler.SetPrototypeOf; trap != nil {
		return trap(target, proto), true
	}
	return false, false
}

func (h *nativeProxyHandler) isExtensible(target *Object) (bool, bool) {
	if trap := h.handler.IsExtensible; trap != nil {
		return trap(target), true
	}
	return false, false
}

func (h *nativeProxyHandler) preventExtensions(target *Object) (bool, bool) {
	if trap := h.handler.PreventExtensions; trap != nil {
		return trap(target), true
	}
	return false, false
}

func (h *nativeProxyHandler) getOwnPropertyDescriptorStr(target *Object, prop unistring.String) (Value, bool) {
	if trap := h.handler.GetOwnPropertyDescriptorIdx; trap != nil {
		if idx, ok := strToInt(prop); ok {
			desc := trap(target, idx)
			return desc.toValue(target.runtime), true
		}
	}
	if trap := h.handler.GetOwnPropertyDescriptor; trap != nil {
		desc := trap(target, prop.String())
		return desc.toValue(target.runtime), true
	}
	return nil, false
}

func (h *nativeProxyHandler) getOwnPropertyDescriptorIdx(target *Object, prop valueInt) (Value, bool) {
	if trap := h.handler.GetOwnPropertyDescriptorIdx; trap != nil {
		desc := trap(target, toIntStrict(int64(prop)))
		return desc.toValue(target.runtime), true
	}
	if trap := h.handler.GetOwnPropertyDescriptor; trap != nil {
		desc := trap(target, prop.String())
		return desc.toValue(target.runtime), true
	}
	return nil, false
}

func (h *nativeProxyHandler) getOwnPropertyDescriptorSym(target *Object, prop *Symbol) (Value, bool) {
	if trap := h.handler.GetOwnPropertyDescriptorSym; trap != nil {
		desc := trap(target, prop)
		return desc.toValue(target.runtime), true
	}
	return nil, false
}

func (h *nativeProxyHandler) definePropertyStr(target *Object, prop unistring.String, desc PropertyDescriptor) (bool, bool) {
	if trap := h.handler.DefinePropertyIdx; trap != nil {
		if idx, ok := strToInt(prop); ok {
			return trap(target, idx, desc), true
		}
	}
	if trap := h.handler.DefineProperty; trap != nil {
		return trap(target, prop.String(), desc), true
	}
	return false, false
}

func (h *nativeProxyHandler) definePropertyIdx(target *Object, prop valueInt, desc PropertyDescriptor) (bool, bool) {
	if trap := h.handler.DefinePropertyIdx; trap != nil {
		return trap(target, toIntStrict(int64(prop)), desc), true
	}
	if trap := h.handler.DefineProperty; trap != nil {
		return trap(target, prop.String(), desc), true
	}
	return false, false
}

func (h *nativeProxyHandler) definePropertySym(target *Object, prop *Symbol, desc PropertyDescriptor) (bool, bool) {
	if trap := h.handler.DefinePropertySym; trap != nil {
		return trap(target, prop, desc), true
	}
	return false, false
}

func (h *nativeProxyHandler) hasStr(target *Object, prop unistring.String) (bool, bool) {
	if trap := h.handler.HasIdx; trap != nil {
		if idx, ok := strToInt(prop); ok {
			return trap(target, idx), true
		}
	}
	if trap := h.handler.Has; trap != nil {
		return trap(target, prop.String()), true
	}
	return false, false
}

func (h *nativeProxyHandler) hasIdx(target *Object, prop valueInt) (bool, bool) {
	if trap := h.handler.HasIdx; trap != nil {
		return trap(target, toIntStrict(int64(prop))), true
	}
	if trap := h.handler.Has; trap != nil {
		return trap(target, prop.String()), true
	}
	return false, false
}

func (h *nativeProxyHandler) hasSym(target *Object, prop *Symbol) (bool, bool) {
	if trap := h.handler.HasSym; trap != nil {
		return trap(target, prop), true
	}
	return false, false
}

func (h *nativeProxyHandler) getStr(target *Object, prop unistring.String, receiver Value) (Value, bool) {
	if trap := h.handler.GetIdx; trap != nil {
		if idx, ok := strToInt(prop); ok {
			return trap(target, idx, receiver), true
		}
	}
	if trap := h.handler.Get; trap != nil {
		return trap(target, prop.String(), receiver), true
	}
	return nil, false
}

func (h *nativeProxyHandler) getIdx(target *Object, prop valueInt, receiver Value) (Value, bool) {
	if trap := h.handler.GetIdx; trap != nil {
		return trap(target, toIntStrict(int64(prop)), receiver), true
	}
	if trap := h.handler.Get; trap != nil {
		return trap(target, prop.String(), receiver), true
	}
	return nil, false
}

func (h *nativeProxyHandler) getSym(target *Object, prop *Symbol, receiver Value) (Value, bool) {
	if trap := h.handler.GetSym; trap != nil {
		return trap(target, prop, receiver), true
	}
	return nil, false
}

func (h *nativeProxyHandler) setStr(target *Object, prop unistring.String, value Value, receiver Value) (bool, bool) {
	if trap := h.handler.SetIdx; trap != nil {
		if idx, ok := strToInt(prop); ok {
			return trap(target, idx, value, receiver), true
		}
	}
	if trap := h.handler.Set; trap != nil {
		return trap(target, prop.String(), value, receiver), true
	}
	return false, false
}

func (h *nativeProxyHandler) setIdx(target *Object, prop valueInt, value Value, receiver Value) (bool, bool) {
	if trap := h.handler.SetIdx; trap != nil {
		return trap(target, toIntStrict(int64(prop)), value, receiver), true
	}
	if trap := h.handler.Set; trap != nil {
		return trap(target, prop.String(), value, receiver), true
	}
	return false, false
}

func (h *nativeProxyHandler) setSym(target *Object, prop *Symbol, value Value, receiver Value) (bool, bool) {
	if trap := h.handler.SetSym; trap != nil {
		return trap(target, prop, value, receiver), true
	}
	return false, false
}

func (h *nativeProxyHandler) deleteStr(target *Object, prop unistring.String) (bool, bool) {
	if trap := h.handler.DeletePropertyIdx; trap != nil {
		if idx, ok := strToInt(prop); ok {
			return trap(target, idx), true
		}
	}
	if trap := h.handler.DeleteProperty; trap != nil {
		return trap(target, prop.String()), true
	}
	return false, false
}

func (h *nativeProxyHandler) deleteIdx(target *Object, prop valueInt) (bool, bool) {
	if trap := h.handler.DeletePropertyIdx; trap != nil {
		return trap(target, toIntStrict(int64(prop))), true
	}
	if trap := h.handler.DeleteProperty; trap != nil {
		return trap(target, prop.String()), true
	}
	return false, false
}

func (h *nativeProxyHandler) deleteSym(target *Object, prop *Symbol) (bool, bool) {
	if trap := h.handler.DeletePropertySym; trap != nil {
		return trap(target, prop), true
	}
	return false, false
}

func (h *nativeProxyHandler) ownKeys(target *Object) (*Object, bool) {
	if trap := h.handler.OwnKeys; trap != nil {
		return trap(target), true
	}
	return nil, false
}

func (h *nativeProxyHandler) apply(target *Object, this Value, args []Value) (Value, bool) {
	if trap := h.handler.Apply; trap != nil {
		return trap(target, this, args), true
	}
	return nil, false
}

func (h *nativeProxyHandler) construct(target *Object, args []Value, newTarget *Object) (Value, bool) {
	if trap := h.handler.Construct; trap != nil {
		return trap(target, args, newTarget), true
	}
	return nil, false
}

func (h *nativeProxyHandler) toObject(runtime *Runtime) *Object {
	return runtime.ToValue(h.handler).ToObject(runtime)
}

func (r *Runtime) newNativeProxyHandler(nativeHandler *ProxyTrapConfig) proxyHandler {
	return &nativeProxyHandler{handler: nativeHandler}
}

// ProxyTrapConfig provides a simplified Go-friendly API for implementing Proxy traps.
// If an *Idx trap is defined it gets called for integer property keys, including negative ones. Note that
// this only includes string property keys that represent a canonical integer
// (i.e. "0", "123", but not "00", "01", " 1" or "-0").
// For efficiency strings representing integers exceeding 2^53 are not checked to see if they are canonical,
// i.e. the *Idx traps will receive "9007199254740993" as well as "9007199254740994", even though the former is not
// a canonical representation in ECMAScript (Number("9007199254740993") === 9007199254740992).
// See https://262.ecma-international.org/#sec-canonicalnumericindexstring
// If an *Idx trap is not set, the corresponding string one is used.
type ProxyTrapConfig struct {
	// A trap for Object.getPrototypeOf, Reflect.getPrototypeOf, __proto__, Object.prototype.isPrototypeOf, instanceof
	GetPrototypeOf func(target *Object) (prototype *Object)

	// A trap for Object.setPrototypeOf, Reflect.setPrototypeOf
	SetPrototypeOf func(target *Object, prototype *Object) (success bool)

	// A trap for Object.isExtensible, Reflect.isExtensible
	IsExtensible func(target *Object) (success bool)

	// A trap for Object.preventExtensions, Reflect.preventExtensions
	PreventExtensions func(target *Object) (success bool)

	// A trap for Object.getOwnPropertyDescriptor, Reflect.getOwnPropertyDescriptor (string properties)
	GetOwnPropertyDescriptor func(target *Object, prop string) (propertyDescriptor PropertyDescriptor)

	// A trap for Object.getOwnPropertyDescriptor, Reflect.getOwnPropertyDescriptor (integer properties)
	GetOwnPropertyDescriptorIdx func(target *Object, prop int) (propertyDescriptor PropertyDescriptor)

	// A trap for Object.getOwnPropertyDescriptor, Reflect.getOwnPropertyDescriptor (Symbol properties)
	GetOwnPropertyDescriptorSym func(target *Object, prop *Symbol) (propertyDescriptor PropertyDescriptor)

	// A trap for Object.defineProperty, Reflect.defineProperty (string properties)
	DefineProperty func(target *Object, key string, propertyDescriptor PropertyDescriptor) (success bool)

	// A trap for Object.defineProperty, Reflect.defineProperty (integer properties)
	DefinePropertyIdx func(target *Object, key int, propertyDescriptor PropertyDescriptor) (success bool)

	// A trap for Object.defineProperty, Reflect.defineProperty (Symbol properties)
	DefinePropertySym func(target *Object, key *Symbol, propertyDescriptor PropertyDescriptor) (success bool)

	// A trap for the in operator, with operator, Reflect.has (string properties)
	Has func(target *Object, property string) (available bool)

	// A trap for the in operator, with operator, Reflect.has (integer properties)
	HasIdx func(target *Object, property int) (available bool)

	// A trap for the in operator, with operator, Reflect.has (Symbol properties)
	HasSym func(target *Object, property *Symbol) (available bool)

	// A trap for getting property values, Reflect.get (string properties)
	Get func(target *Object, property string, receiver Value) (value Value)

	// A trap for getting property values, Reflect.get (integer properties)
	GetIdx func(target *Object, property int, receiver Value) (value Value)

	// A trap for getting property values, Reflect.get (Symbol properties)
	GetSym func(target *Object, property *Symbol, receiver Value) (value Value)

	// A trap for setting property values, Reflect.set (string properties)
	Set func(target *Object, property string, value Value, receiver Value) (success bool)

	// A trap for setting property values, Reflect.set (integer properties)
	SetIdx func(target *Object, property int, value Value, receiver Value) (success bool)

	// A trap for setting property values, Reflect.set (Symbol properties)
	SetSym func(target *Object, property *Symbol, value Value, receiver Value) (success bool)

	// A trap for the delete operator, Reflect.deleteProperty (string properties)
	DeleteProperty func(target *Object, property string) (success bool)

	// A trap for the delete operator, Reflect.deleteProperty (integer properties)
	DeletePropertyIdx func(target *Object, property int) (success bool)

	// A trap for the delete operator, Reflect.deleteProperty (Symbol properties)
	DeletePropertySym func(target *Object, property *Symbol) (success bool)

	// A trap for Object.getOwnPropertyNames, Object.getOwnPropertySymbols, Object.keys, Reflect.ownKeys
	OwnKeys func(target *Object) (object *Object)

	// A trap for a function call, Function.prototype.apply, Function.prototype.call, Reflect.apply
	Apply func(target *Object, this Value, argumentsList []Value) (value Value)

	// A trap for the new operator, Reflect.construct
	Construct func(target *Object, argumentsList []Value, newTarget *Object) (value *Object)
}

func (r *Runtime) newProxy(args []Value, proto *Object) *Object {
	if len(args) >= 2 {
		if target, ok := args[0].(*Object); ok {
			if proxyHandler, ok := args[1].(*Object); ok {
				return r.newProxyObject(target, proxyHandler, proto).val
			}
		}
	}
	panic(r.NewTypeError("Cannot create proxy with a non-object as target or handler"))
}

func (r *Runtime) builtin_newProxy(args []Value, newTarget *Object) *Object {
	if newTarget == nil {
		panic(r.needNew("Proxy"))
	}
	return r.newProxy(args, r.getPrototypeFromCtor(newTarget, r.getProxy(), r.global.ObjectPrototype))
}

func (r *Runtime) NewProxy(target *Object, nativeHandler *ProxyTrapConfig) Proxy {
	if p, ok := target.self.(*proxyObject); ok {
		if p.handler == nil {
			panic(r.NewTypeError("Cannot create proxy with a revoked proxy as target"))
		}
	}
	handler := r.newNativeProxyHandler(nativeHandler)
	proxy := r._newProxyObject(target, handler, nil)
	return Proxy{proxy: proxy}
}

func (r *Runtime) builtin_proxy_revocable(call FunctionCall) Value {
	if len(call.Arguments) >= 2 {
		if target, ok := call.Argument(0).(*Object); ok {
			if proxyHandler, ok := call.Argument(1).(*Object); ok {
				proxy := r.newProxyObject(target, proxyHandler, nil)
				revoke := r.newNativeFunc(func(FunctionCall) Value {
					proxy.revoke()
					return _undefined
				}, "", 0)
				ret := r.NewObject()
				ret.self._putProp("proxy", proxy.val, true, true, true)
				ret.self._putProp("revoke", revoke, true, true, true)
				return ret
			}
		}
	}
	panic(r.NewTypeError("Cannot create proxy with a non-object as target or handler"))
}

func (r *Runtime) createProxy(val *Object) objectImpl {
	o := r.newNativeConstructOnly(val, r.builtin_newProxy, nil, "Proxy", 2)

	o._putProp("revocable", r.newNativeFunc(r.builtin_proxy_revocable, "revocable", 2), true, false, true)
	return o
}

func (r *Runtime) getProxy() *Object {
	ret := r.global.Proxy
	if ret == nil {
		ret = &Object{runtime: r}
		r.global.Proxy = ret
		r.createProxy(ret)
	}
	return ret
}
