package goja

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

const propNameStack = "stack"

type errorObject struct {
	baseObject
	stack          []StackFrame
	stackPropAdded bool
}

func (e *errorObject) formatStack() String {
	var b StringBuilder
	val := writeErrorString(&b, e.val)
	if val != nil {
		b.WriteString(val)
	}
	b.WriteRune('\n')

	for _, frame := range e.stack {
		b.writeASCII("\tat ")
		frame.WriteToValueBuilder(&b)
		b.WriteRune('\n')
	}
	return b.String()
}

func (e *errorObject) addStackProp() Value {
	if !e.stackPropAdded {
		res := e._putProp(propNameStack, e.formatStack(), true, false, true)
		if len(e.propNames) > 1 {
			// reorder property names to ensure 'stack' is the first one
			copy(e.propNames[1:], e.propNames)
			e.propNames[0] = propNameStack
		}
		e.stackPropAdded = true
		return res
	}
	return nil
}

func (e *errorObject) getStr(p unistring.String, receiver Value) Value {
	return e.getStrWithOwnProp(e.getOwnPropStr(p), p, receiver)
}

func (e *errorObject) getOwnPropStr(name unistring.String) Value {
	res := e.baseObject.getOwnPropStr(name)
	if res == nil && name == propNameStack {
		return e.addStackProp()
	}

	return res
}

func (e *errorObject) setOwnStr(name unistring.String, val Value, throw bool) bool {
	if name == propNameStack {
		e.addStackProp()
	}
	return e.baseObject.setOwnStr(name, val, throw)
}

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

func (e *errorObject) deleteStr(name unistring.String, throw bool) bool {
	if name == propNameStack {
		e.addStackProp()
	}
	return e.baseObject.deleteStr(name, throw)
}

func (e *errorObject) defineOwnPropertyStr(name unistring.String, desc PropertyDescriptor, throw bool) bool {
	if name == propNameStack {
		e.addStackProp()
	}
	return e.baseObject.defineOwnPropertyStr(name, desc, throw)
}

func (e *errorObject) hasOwnPropertyStr(name unistring.String) bool {
	if e.baseObject.hasOwnPropertyStr(name) {
		return true
	}

	return name == propNameStack && !e.stackPropAdded
}

func (e *errorObject) stringKeys(all bool, accum []Value) []Value {
	if all && !e.stackPropAdded {
		accum = append(accum, asciiString(propNameStack))
	}
	return e.baseObject.stringKeys(all, accum)
}

func (e *errorObject) iterateStringKeys() iterNextFunc {
	e.addStackProp()
	return e.baseObject.iterateStringKeys()
}

func (e *errorObject) init() {
	e.baseObject.init()
	vm := e.val.runtime.vm
	e.stack = vm.captureStack(make([]StackFrame, 0, len(vm.callStack)+1), 0)
}

func (r *Runtime) newErrorObject(proto *Object, class string) *errorObject {
	obj := &Object{runtime: r}
	o := &errorObject{
		baseObject: baseObject{
			class:      class,
			val:        obj,
			extensible: true,
			prototype:  proto,
		},
	}
	obj.self = o
	o.init()
	return o
}

func (r *Runtime) builtin_Error(args []Value, proto *Object) *Object {
	obj := r.newErrorObject(proto, classError)
	if len(args) > 0 && args[0] != _undefined {
		obj._putProp("message", args[0].ToString(), true, false, true)
	}
	if len(args) > 1 && args[1] != _undefined {
		if options, ok := args[1].(*Object); ok {
			if options.hasProperty(asciiString("cause")) {
				obj.defineOwnPropertyStr("cause", PropertyDescriptor{
					Writable:     FLAG_TRUE,
					Enumerable:   FLAG_FALSE,
					Configurable: FLAG_TRUE,
					Value:        options.Get("cause"),
				}, true)
			}
		}
	}
	return obj.val
}

func (r *Runtime) builtin_AggregateError(args []Value, proto *Object) *Object {
	obj := r.newErrorObject(proto, classError)
	if len(args) > 1 && args[1] != nil && args[1] != _undefined {
		obj._putProp("message", args[1].toString(), true, false, true)
	}
	var errors []Value
	if len(args) > 0 {
		errors = r.iterableToList(args[0], nil)
	}
	obj._putProp("errors", r.newArrayValues(errors), true, false, true)

	if len(args) > 2 && args[2] != _undefined {
		if options, ok := args[2].(*Object); ok {
			if options.hasProperty(asciiString("cause")) {
				obj.defineOwnPropertyStr("cause", PropertyDescriptor{
					Writable:     FLAG_TRUE,
					Enumerable:   FLAG_FALSE,
					Configurable: FLAG_TRUE,
					Value:        options.Get("cause"),
				}, true)
			}
		}
	}

	return obj.val
}

func writeErrorString(sb *StringBuilder, obj *Object) String {
	var nameStr, msgStr String
	name := obj.self.getStr("name", nil)
	if name == nil || name == _undefined {
		nameStr = asciiString("Error")
	} else {
		nameStr = name.toString()
	}
	msg := obj.self.getStr("message", nil)
	if msg == nil || msg == _undefined {
		msgStr = stringEmpty
	} else {
		msgStr = msg.toString()
	}
	if nameStr.Length() == 0 {
		return msgStr
	}
	if msgStr.Length() == 0 {
		return nameStr
	}
	sb.WriteString(nameStr)
	sb.WriteString(asciiString(": "))
	sb.WriteString(msgStr)
	return nil
}

func (r *Runtime) error_toString(call FunctionCall) Value {
	var sb StringBuilder
	val := writeErrorString(&sb, r.toObject(call.This))
	if val != nil {
		return val
	}
	return sb.String()
}

func (r *Runtime) createErrorPrototype(name String, ctor *Object) *Object {
	o := r.newBaseObject(r.getErrorPrototype(), classObject)
	o._putProp("message", stringEmpty, true, false, true)
	o._putProp("name", name, true, false, true)
	o._putProp("constructor", ctor, true, false, true)
	return o.val
}

func (r *Runtime) getErrorPrototype() *Object {
	ret := r.global.ErrorPrototype
	if ret == nil {
		ret = r.NewObject()
		r.global.ErrorPrototype = ret
		o := ret.self
		o._putProp("message", stringEmpty, true, false, true)
		o._putProp("name", stringError, true, false, true)
		o._putProp("toString", r.newNativeFunc(r.error_toString, "toString", 0), true, false, true)
		o._putProp("constructor", r.getError(), true, false, true)
	}
	return ret
}

func (r *Runtime) getError() *Object {
	ret := r.global.Error
	if ret == nil {
		ret = &Object{runtime: r}
		r.global.Error = ret
		r.newNativeFuncConstruct(ret, r.builtin_Error, "Error", r.getErrorPrototype(), 1)
	}
	return ret
}

func (r *Runtime) getAggregateError() *Object {
	ret := r.global.AggregateError
	if ret == nil {
		ret = &Object{runtime: r}
		r.global.AggregateError = ret
		r.newNativeFuncConstructProto(ret, r.builtin_AggregateError, "AggregateError", r.createErrorPrototype(stringAggregateError, ret), r.getError(), 2)
	}
	return ret
}

func (r *Runtime) getTypeError() *Object {
	ret := r.global.TypeError
	if ret == nil {
		ret = &Object{runtime: r}
		r.global.TypeError = ret
		r.newNativeFuncConstructProto(ret, r.builtin_Error, "TypeError", r.createErrorPrototype(stringTypeError, ret), r.getError(), 1)
	}
	return ret
}

func (r *Runtime) getReferenceError() *Object {
	ret := r.global.ReferenceError
	if ret == nil {
		ret = &Object{runtime: r}
		r.global.ReferenceError = ret
		r.newNativeFuncConstructProto(ret, r.builtin_Error, "ReferenceError", r.createErrorPrototype(stringReferenceError, ret), r.getError(), 1)
	}
	return ret
}

func (r *Runtime) getSyntaxError() *Object {
	ret := r.global.SyntaxError
	if ret == nil {
		ret = &Object{runtime: r}
		r.global.SyntaxError = ret
		r.newNativeFuncConstructProto(ret, r.builtin_Error, "SyntaxError", r.createErrorPrototype(stringSyntaxError, ret), r.getError(), 1)
	}
	return ret
}

func (r *Runtime) getRangeError() *Object {
	ret := r.global.RangeError
	if ret == nil {
		ret = &Object{runtime: r}
		r.global.RangeError = ret
		r.newNativeFuncConstructProto(ret, r.builtin_Error, "RangeError", r.createErrorPrototype(stringRangeError, ret), r.getError(), 1)
	}
	return ret
}

func (r *Runtime) getEvalError() *Object {
	ret := r.global.EvalError
	if ret == nil {
		ret = &Object{runtime: r}
		r.global.EvalError = ret
		r.newNativeFuncConstructProto(ret, r.builtin_Error, "EvalError", r.createErrorPrototype(stringEvalError, ret), r.getError(), 1)
	}
	return ret
}

func (r *Runtime) getURIError() *Object {
	ret := r.global.URIError
	if ret == nil {
		ret = &Object{runtime: r}
		r.global.URIError = ret
		r.newNativeFuncConstructProto(ret, r.builtin_Error, "URIError", r.createErrorPrototype(stringURIError, ret), r.getError(), 1)
	}
	return ret
}

func (r *Runtime) getGoError() *Object {
	ret := r.global.GoError
	if ret == nil {
		ret = &Object{runtime: r}
		r.global.GoError = ret
		r.newNativeFuncConstructProto(ret, r.builtin_Error, "GoError", r.createErrorPrototype(stringGoError, ret), r.getError(), 1)
	}
	return ret
}
