package goja

import (
	"reflect"
	"strconv"

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

type objectGoArrayReflect struct {
	objectGoReflect
	lengthProp valueProperty

	valueCache valueArrayCache

	putIdx func(idx int, v Value, throw bool) bool
}

type valueArrayCache []reflectValueWrapper

func (c *valueArrayCache) get(idx int) reflectValueWrapper {
	if idx < len(*c) {
		return (*c)[idx]
	}
	return nil
}

func (c *valueArrayCache) grow(newlen int) {
	oldcap := cap(*c)
	if oldcap < newlen {
		a := make([]reflectValueWrapper, newlen, growCap(newlen, len(*c), oldcap))
		copy(a, *c)
		*c = a
	} else {
		*c = (*c)[:newlen]
	}
}

func (c *valueArrayCache) put(idx int, w reflectValueWrapper) {
	if len(*c) <= idx {
		c.grow(idx + 1)
	}
	(*c)[idx] = w
}

func (c *valueArrayCache) shrink(newlen int) {
	if len(*c) > newlen {
		tail := (*c)[newlen:]
		for i, item := range tail {
			if item != nil {
				copyReflectValueWrapper(item)
				tail[i] = nil
			}
		}
		*c = (*c)[:newlen]
	}
}

func (o *objectGoArrayReflect) _init() {
	o.objectGoReflect.init()
	o.class = classArray
	o.prototype = o.val.runtime.getArrayPrototype()
	o.baseObject._put("length", &o.lengthProp)
}

func (o *objectGoArrayReflect) init() {
	o._init()
	o.updateLen()
	o.putIdx = o._putIdx
}

func (o *objectGoArrayReflect) updateLen() {
	o.lengthProp.value = intToValue(int64(o.fieldsValue.Len()))
}

func (o *objectGoArrayReflect) _hasIdx(idx valueInt) bool {
	if idx := int64(idx); idx >= 0 && idx < int64(o.fieldsValue.Len()) {
		return true
	}
	return false
}

func (o *objectGoArrayReflect) _hasStr(name unistring.String) bool {
	if idx := strToIdx64(name); idx >= 0 && idx < int64(o.fieldsValue.Len()) {
		return true
	}
	return false
}

func (o *objectGoArrayReflect) _getIdx(idx int) Value {
	if v := o.valueCache.get(idx); v != nil {
		return v.esValue()
	}

	v := o.fieldsValue.Index(idx)

	res, w := o.elemToValue(v)
	if w != nil {
		o.valueCache.put(idx, w)
	}

	return res
}

func (o *objectGoArrayReflect) getIdx(idx valueInt, receiver Value) Value {
	if idx := toIntStrict(int64(idx)); idx >= 0 && idx < o.fieldsValue.Len() {
		return o._getIdx(idx)
	}
	return o.objectGoReflect.getStr(idx.string(), receiver)
}

func (o *objectGoArrayReflect) getStr(name unistring.String, receiver Value) Value {
	var ownProp Value
	if idx := strToGoIdx(name); idx >= 0 && idx < o.fieldsValue.Len() {
		ownProp = o._getIdx(idx)
	} else if name == "length" {
		if o.fieldsValue.Kind() == reflect.Slice {
			o.updateLen()
		}
		ownProp = &o.lengthProp
	} else {
		ownProp = o.objectGoReflect.getOwnPropStr(name)
	}
	return o.getStrWithOwnProp(ownProp, name, receiver)
}

func (o *objectGoArrayReflect) getOwnPropStr(name unistring.String) Value {
	if idx := strToGoIdx(name); idx >= 0 {
		if idx < o.fieldsValue.Len() {
			return &valueProperty{
				value:      o._getIdx(idx),
				writable:   true,
				enumerable: true,
			}
		}
		return nil
	}
	if name == "length" {
		if o.fieldsValue.Kind() == reflect.Slice {
			o.updateLen()
		}
		return &o.lengthProp
	}
	return o.objectGoReflect.getOwnPropStr(name)
}

func (o *objectGoArrayReflect) getOwnPropIdx(idx valueInt) Value {
	if idx := toIntStrict(int64(idx)); idx >= 0 && idx < o.fieldsValue.Len() {
		return &valueProperty{
			value:      o._getIdx(idx),
			writable:   true,
			enumerable: true,
		}
	}
	return nil
}

func (o *objectGoArrayReflect) _putIdx(idx int, v Value, throw bool) bool {
	cached := o.valueCache.get(idx)
	if cached != nil {
		copyReflectValueWrapper(cached)
	}

	rv := o.fieldsValue.Index(idx)
	err := o.val.runtime.toReflectValue(v, rv, &objectExportCtx{})
	if err != nil {
		if cached != nil {
			cached.setReflectValue(rv)
		}
		o.val.runtime.typeErrorResult(throw, "Go type conversion error: %v", err)
		return false
	}
	if cached != nil {
		o.valueCache[idx] = nil
	}
	return true
}

func (o *objectGoArrayReflect) setOwnIdx(idx valueInt, val Value, throw bool) bool {
	if i := toIntStrict(int64(idx)); i >= 0 {
		if i >= o.fieldsValue.Len() {
			if res, ok := o._setForeignIdx(idx, nil, val, o.val, throw); ok {
				return res
			}
		}
		return o.putIdx(i, val, throw)
	} else {
		name := idx.string()
		if res, ok := o._setForeignStr(name, nil, val, o.val, throw); !ok {
			o.val.runtime.typeErrorResult(throw, "Can't set property '%s' on Go slice", name)
			return false
		} else {
			return res
		}
	}
}

func (o *objectGoArrayReflect) setOwnStr(name unistring.String, val Value, throw bool) bool {
	if idx := strToGoIdx(name); idx >= 0 {
		if idx >= o.fieldsValue.Len() {
			if res, ok := o._setForeignStr(name, nil, val, o.val, throw); ok {
				return res
			}
		}
		return o.putIdx(idx, val, throw)
	} else {
		if res, ok := o._setForeignStr(name, nil, val, o.val, throw); !ok {
			o.val.runtime.typeErrorResult(throw, "Can't set property '%s' on Go slice", name)
			return false
		} else {
			return res
		}
	}
}

func (o *objectGoArrayReflect) setForeignIdx(idx valueInt, val, receiver Value, throw bool) (bool, bool) {
	return o._setForeignIdx(idx, trueValIfPresent(o._hasIdx(idx)), val, receiver, throw)
}

func (o *objectGoArrayReflect) setForeignStr(name unistring.String, val, receiver Value, throw bool) (bool, bool) {
	return o._setForeignStr(name, trueValIfPresent(o.hasOwnPropertyStr(name)), val, receiver, throw)
}

func (o *objectGoArrayReflect) hasOwnPropertyIdx(idx valueInt) bool {
	return o._hasIdx(idx)
}

func (o *objectGoArrayReflect) hasOwnPropertyStr(name unistring.String) bool {
	if o._hasStr(name) || name == "length" {
		return true
	}
	return o.objectGoReflect.hasOwnPropertyStr(name)
}

func (o *objectGoArrayReflect) defineOwnPropertyIdx(idx valueInt, descr PropertyDescriptor, throw bool) bool {
	if i := toIntStrict(int64(idx)); i >= 0 {
		if !o.val.runtime.checkHostObjectPropertyDescr(idx.string(), descr, throw) {
			return false
		}
		val := descr.Value
		if val == nil {
			val = _undefined
		}
		return o.putIdx(i, val, throw)
	}
	o.val.runtime.typeErrorResult(throw, "Cannot define property '%d' on a Go slice", idx)
	return false
}

func (o *objectGoArrayReflect) defineOwnPropertyStr(name unistring.String, descr PropertyDescriptor, throw bool) bool {
	if idx := strToGoIdx(name); idx >= 0 {
		if !o.val.runtime.checkHostObjectPropertyDescr(name, descr, throw) {
			return false
		}
		val := descr.Value
		if val == nil {
			val = _undefined
		}
		return o.putIdx(idx, val, throw)
	}
	o.val.runtime.typeErrorResult(throw, "Cannot define property '%s' on a Go slice", name)
	return false
}

func (o *objectGoArrayReflect) _deleteIdx(idx int) {
	if idx < o.fieldsValue.Len() {
		if cv := o.valueCache.get(idx); cv != nil {
			copyReflectValueWrapper(cv)
			o.valueCache[idx] = nil
		}

		o.fieldsValue.Index(idx).Set(reflect.Zero(o.fieldsValue.Type().Elem()))
	}
}

func (o *objectGoArrayReflect) deleteStr(name unistring.String, throw bool) bool {
	if idx := strToGoIdx(name); idx >= 0 {
		o._deleteIdx(idx)
		return true
	}

	return o.objectGoReflect.deleteStr(name, throw)
}

func (o *objectGoArrayReflect) deleteIdx(i valueInt, throw bool) bool {
	idx := toIntStrict(int64(i))
	if idx >= 0 {
		o._deleteIdx(idx)
	}
	return true
}

type goArrayReflectPropIter struct {
	o          *objectGoArrayReflect
	idx, limit int
}

func (i *goArrayReflectPropIter) next() (propIterItem, iterNextFunc) {
	if i.idx < i.limit && i.idx < i.o.fieldsValue.Len() {
		name := strconv.Itoa(i.idx)
		i.idx++
		return propIterItem{name: asciiString(name), enumerable: _ENUM_TRUE}, i.next
	}

	return i.o.objectGoReflect.iterateStringKeys()()
}

func (o *objectGoArrayReflect) stringKeys(all bool, accum []Value) []Value {
	for i := 0; i < o.fieldsValue.Len(); i++ {
		accum = append(accum, asciiString(strconv.Itoa(i)))
	}

	return o.objectGoReflect.stringKeys(all, accum)
}

func (o *objectGoArrayReflect) iterateStringKeys() iterNextFunc {
	return (&goArrayReflectPropIter{
		o:     o,
		limit: o.fieldsValue.Len(),
	}).next
}

func (o *objectGoArrayReflect) sortLen() int {
	return o.fieldsValue.Len()
}

func (o *objectGoArrayReflect) sortGet(i int) Value {
	return o.getIdx(valueInt(i), nil)
}

func (o *objectGoArrayReflect) swap(i int, j int) {
	vi := o.fieldsValue.Index(i)
	vj := o.fieldsValue.Index(j)
	tmp := reflect.New(o.fieldsValue.Type().Elem()).Elem()
	tmp.Set(vi)
	vi.Set(vj)
	vj.Set(tmp)

	cachedI := o.valueCache.get(i)
	cachedJ := o.valueCache.get(j)
	if cachedI != nil {
		cachedI.setReflectValue(vj)
		o.valueCache.put(j, cachedI)
	} else {
		if j < len(o.valueCache) {
			o.valueCache[j] = nil
		}
	}

	if cachedJ != nil {
		cachedJ.setReflectValue(vi)
		o.valueCache.put(i, cachedJ)
	} else {
		if i < len(o.valueCache) {
			o.valueCache[i] = nil
		}
	}
}
