package goja

import (
	"fmt"
	"go/ast"
	"reflect"
	"strings"

	"github.com/dop251/goja/parser"
	"github.com/dop251/goja/unistring"
)

// JsonEncodable allows custom JSON encoding by JSON.stringify()
// Note that if the returned value itself also implements JsonEncodable, it won't have any effect.
type JsonEncodable interface {
	JsonEncodable() interface{}
}

// FieldNameMapper provides custom mapping between Go and JavaScript property names.
type FieldNameMapper interface {
	// FieldName returns a JavaScript name for the given struct field in the given type.
	// If this method returns "" the field becomes hidden.
	FieldName(t reflect.Type, f reflect.StructField) string

	// MethodName returns a JavaScript name for the given method in the given type.
	// If this method returns "" the method becomes hidden.
	MethodName(t reflect.Type, m reflect.Method) string
}

type tagFieldNameMapper struct {
	tagName      string
	uncapMethods bool
}

func (tfm tagFieldNameMapper) FieldName(_ reflect.Type, f reflect.StructField) string {
	tag := f.Tag.Get(tfm.tagName)
	if idx := strings.IndexByte(tag, ','); idx != -1 {
		tag = tag[:idx]
	}
	if parser.IsIdentifier(tag) {
		return tag
	}
	return ""
}

func uncapitalize(s string) string {
	return strings.ToLower(s[0:1]) + s[1:]
}

func (tfm tagFieldNameMapper) MethodName(_ reflect.Type, m reflect.Method) string {
	if tfm.uncapMethods {
		return uncapitalize(m.Name)
	}
	return m.Name
}

type uncapFieldNameMapper struct {
}

func (u uncapFieldNameMapper) FieldName(_ reflect.Type, f reflect.StructField) string {
	return uncapitalize(f.Name)
}

func (u uncapFieldNameMapper) MethodName(_ reflect.Type, m reflect.Method) string {
	return uncapitalize(m.Name)
}

type reflectFieldInfo struct {
	Index     []int
	Anonymous bool
}

type reflectFieldsInfo struct {
	Fields map[string]reflectFieldInfo
	Names  []string
}

type reflectMethodsInfo struct {
	Methods map[string]int
	Names   []string
}

type reflectValueWrapper interface {
	esValue() Value
	reflectValue() reflect.Value
	setReflectValue(reflect.Value)
}

func isContainer(k reflect.Kind) bool {
	switch k {
	case reflect.Struct, reflect.Slice, reflect.Array:
		return true
	}
	return false
}

func copyReflectValueWrapper(w reflectValueWrapper) {
	v := w.reflectValue()
	c := reflect.New(v.Type()).Elem()
	c.Set(v)
	w.setReflectValue(c)
}

type objectGoReflect struct {
	baseObject
	origValue, fieldsValue reflect.Value

	fieldsInfo  *reflectFieldsInfo
	methodsInfo *reflectMethodsInfo

	methodsValue reflect.Value

	valueCache map[string]reflectValueWrapper

	toString, valueOf func() Value

	toJson func() interface{}
}

func (o *objectGoReflect) init() {
	o.baseObject.init()
	switch o.fieldsValue.Kind() {
	case reflect.Bool:
		o.class = classBoolean
		o.prototype = o.val.runtime.getBooleanPrototype()
		o.toString = o._toStringBool
		o.valueOf = o._valueOfBool
	case reflect.String:
		o.class = classString
		o.prototype = o.val.runtime.getStringPrototype()
		o.toString = o._toStringString
	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
		o.class = classNumber
		o.prototype = o.val.runtime.getNumberPrototype()
		o.valueOf = o._valueOfInt
	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
		o.class = classNumber
		o.prototype = o.val.runtime.getNumberPrototype()
		o.valueOf = o._valueOfUint
	case reflect.Float32, reflect.Float64:
		o.class = classNumber
		o.prototype = o.val.runtime.getNumberPrototype()
		o.valueOf = o._valueOfFloat
	default:
		o.class = classObject
		o.prototype = o.val.runtime.global.ObjectPrototype
	}

	if o.fieldsValue.Kind() == reflect.Struct {
		o.fieldsInfo = o.val.runtime.fieldsInfo(o.fieldsValue.Type())
	}

	var methodsType reflect.Type
	// Always use pointer type for non-interface values to be able to access both methods defined on
	// the literal type and on the pointer.
	if o.fieldsValue.Kind() != reflect.Interface {
		methodsType = reflect.PtrTo(o.fieldsValue.Type())
	} else {
		methodsType = o.fieldsValue.Type()
	}

	o.methodsInfo = o.val.runtime.methodsInfo(methodsType)

	// Container values and values that have at least one method defined on the pointer type
	// need to be addressable.
	if !o.origValue.CanAddr() && (isContainer(o.origValue.Kind()) || len(o.methodsInfo.Names) > 0) {
		value := reflect.New(o.origValue.Type()).Elem()
		value.Set(o.origValue)
		o.origValue = value
		if value.Kind() != reflect.Ptr {
			o.fieldsValue = value
		}
	}

	o.extensible = true

	switch o.origValue.Interface().(type) {
	case fmt.Stringer:
		o.toString = o._toStringStringer
	case error:
		o.toString = o._toStringError
	}

	if len(o.methodsInfo.Names) > 0 && o.fieldsValue.Kind() != reflect.Interface {
		o.methodsValue = o.fieldsValue.Addr()
	} else {
		o.methodsValue = o.fieldsValue
	}

	if j, ok := o.origValue.Interface().(JsonEncodable); ok {
		o.toJson = j.JsonEncodable
	}
}

func (o *objectGoReflect) getStr(name unistring.String, receiver Value) Value {
	if v := o._get(name.String()); v != nil {
		return v
	}
	return o.baseObject.getStr(name, receiver)
}

func (o *objectGoReflect) _getField(jsName string) reflect.Value {
	if o.fieldsInfo != nil {
		if info, exists := o.fieldsInfo.Fields[jsName]; exists {
			return o.fieldsValue.FieldByIndex(info.Index)
		}
	}

	return reflect.Value{}
}

func (o *objectGoReflect) _getMethod(jsName string) reflect.Value {
	if o.methodsInfo != nil {
		if idx, exists := o.methodsInfo.Methods[jsName]; exists {
			return o.methodsValue.Method(idx)
		}
	}

	return reflect.Value{}
}

func (o *objectGoReflect) elemToValue(ev reflect.Value) (Value, reflectValueWrapper) {
	if isContainer(ev.Kind()) {
		ret := o.val.runtime.toValue(ev.Interface(), ev)
		if obj, ok := ret.(*Object); ok {
			if w, ok := obj.self.(reflectValueWrapper); ok {
				return ret, w
			}
		}
		return ret, nil
	}

	if ev.Kind() == reflect.Interface {
		ev = ev.Elem()
	}

	if ev.Kind() == reflect.Invalid {
		return _null, nil
	}

	return o.val.runtime.toValue(ev.Interface(), ev), nil
}

func (o *objectGoReflect) _getFieldValue(name string) Value {
	if v := o.valueCache[name]; v != nil {
		return v.esValue()
	}
	if v := o._getField(name); v.IsValid() {
		res, w := o.elemToValue(v)
		if w != nil {
			if o.valueCache == nil {
				o.valueCache = make(map[string]reflectValueWrapper)
			}
			o.valueCache[name] = w
		}
		return res
	}
	return nil
}

func (o *objectGoReflect) _get(name string) Value {
	if o.fieldsValue.Kind() == reflect.Struct {
		if ret := o._getFieldValue(name); ret != nil {
			return ret
		}
	}

	if v := o._getMethod(name); v.IsValid() {
		return o.val.runtime.toValue(v.Interface(), v)
	}

	return nil
}

func (o *objectGoReflect) getOwnPropStr(name unistring.String) Value {
	n := name.String()
	if o.fieldsValue.Kind() == reflect.Struct {
		if v := o._getFieldValue(n); v != nil {
			return &valueProperty{
				value:      v,
				writable:   true,
				enumerable: true,
			}
		}
	}

	if v := o._getMethod(n); v.IsValid() {
		return &valueProperty{
			value:      o.val.runtime.toValue(v.Interface(), v),
			enumerable: true,
		}
	}

	return o.baseObject.getOwnPropStr(name)
}

func (o *objectGoReflect) setOwnStr(name unistring.String, val Value, throw bool) bool {
	has, ok := o._put(name.String(), val, throw)
	if !has {
		if res, ok := o._setForeignStr(name, nil, val, o.val, throw); !ok {
			o.val.runtime.typeErrorResult(throw, "Cannot assign to property %s of a host object", name)
			return false
		} else {
			return res
		}
	}
	return ok
}

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

func (o *objectGoReflect) setForeignIdx(idx valueInt, val, receiver Value, throw bool) (bool, bool) {
	return o._setForeignIdx(idx, nil, val, receiver, throw)
}

func (o *objectGoReflect) _put(name string, val Value, throw bool) (has, ok bool) {
	if o.fieldsValue.Kind() == reflect.Struct {
		if v := o._getField(name); v.IsValid() {
			cached := o.valueCache[name]
			if cached != nil {
				copyReflectValueWrapper(cached)
			}

			err := o.val.runtime.toReflectValue(val, v, &objectExportCtx{})
			if err != nil {
				if cached != nil {
					cached.setReflectValue(v)
				}
				o.val.runtime.typeErrorResult(throw, "Go struct conversion error: %v", err)
				return true, false
			}
			if cached != nil {
				delete(o.valueCache, name)
			}
			return true, true
		}
	}
	return false, false
}

func (o *objectGoReflect) _putProp(name unistring.String, value Value, writable, enumerable, configurable bool) Value {
	if _, ok := o._put(name.String(), value, false); ok {
		return value
	}
	return o.baseObject._putProp(name, value, writable, enumerable, configurable)
}

func (r *Runtime) checkHostObjectPropertyDescr(name unistring.String, descr PropertyDescriptor, throw bool) bool {
	if descr.Getter != nil || descr.Setter != nil {
		r.typeErrorResult(throw, "Host objects do not support accessor properties")
		return false
	}
	if descr.Writable == FLAG_FALSE {
		r.typeErrorResult(throw, "Host object field %s cannot be made read-only", name)
		return false
	}
	if descr.Configurable == FLAG_TRUE {
		r.typeErrorResult(throw, "Host object field %s cannot be made configurable", name)
		return false
	}
	return true
}

func (o *objectGoReflect) defineOwnPropertyStr(name unistring.String, descr PropertyDescriptor, throw bool) bool {
	if o.val.runtime.checkHostObjectPropertyDescr(name, descr, throw) {
		n := name.String()
		if has, ok := o._put(n, descr.Value, throw); !has {
			o.val.runtime.typeErrorResult(throw, "Cannot define property '%s' on a host object", n)
			return false
		} else {
			return ok
		}
	}
	return false
}

func (o *objectGoReflect) _has(name string) bool {
	if o.fieldsValue.Kind() == reflect.Struct {
		if v := o._getField(name); v.IsValid() {
			return true
		}
	}
	if v := o._getMethod(name); v.IsValid() {
		return true
	}
	return false
}

func (o *objectGoReflect) hasOwnPropertyStr(name unistring.String) bool {
	return o._has(name.String()) || o.baseObject.hasOwnPropertyStr(name)
}

func (o *objectGoReflect) _valueOfInt() Value {
	return intToValue(o.fieldsValue.Int())
}

func (o *objectGoReflect) _valueOfUint() Value {
	return intToValue(int64(o.fieldsValue.Uint()))
}

func (o *objectGoReflect) _valueOfBool() Value {
	if o.fieldsValue.Bool() {
		return valueTrue
	} else {
		return valueFalse
	}
}

func (o *objectGoReflect) _valueOfFloat() Value {
	return floatToValue(o.fieldsValue.Float())
}

func (o *objectGoReflect) _toStringStringer() Value {
	return newStringValue(o.origValue.Interface().(fmt.Stringer).String())
}

func (o *objectGoReflect) _toStringString() Value {
	return newStringValue(o.fieldsValue.String())
}

func (o *objectGoReflect) _toStringBool() Value {
	if o.fieldsValue.Bool() {
		return stringTrue
	} else {
		return stringFalse
	}
}

func (o *objectGoReflect) _toStringError() Value {
	return newStringValue(o.origValue.Interface().(error).Error())
}

func (o *objectGoReflect) deleteStr(name unistring.String, throw bool) bool {
	n := name.String()
	if o._has(n) {
		o.val.runtime.typeErrorResult(throw, "Cannot delete property %s from a Go type", n)
		return false
	}
	return o.baseObject.deleteStr(name, throw)
}

type goreflectPropIter struct {
	o   *objectGoReflect
	idx int
}

func (i *goreflectPropIter) nextField() (propIterItem, iterNextFunc) {
	names := i.o.fieldsInfo.Names
	if i.idx < len(names) {
		name := names[i.idx]
		i.idx++
		return propIterItem{name: newStringValue(name), enumerable: _ENUM_TRUE}, i.nextField
	}

	i.idx = 0
	return i.nextMethod()
}

func (i *goreflectPropIter) nextMethod() (propIterItem, iterNextFunc) {
	names := i.o.methodsInfo.Names
	if i.idx < len(names) {
		name := names[i.idx]
		i.idx++
		return propIterItem{name: newStringValue(name), enumerable: _ENUM_TRUE}, i.nextMethod
	}

	return propIterItem{}, nil
}

func (o *objectGoReflect) iterateStringKeys() iterNextFunc {
	r := &goreflectPropIter{
		o: o,
	}
	if o.fieldsInfo != nil {
		return r.nextField
	}

	return r.nextMethod
}

func (o *objectGoReflect) stringKeys(_ bool, accum []Value) []Value {
	// all own keys are enumerable
	if o.fieldsInfo != nil {
		for _, name := range o.fieldsInfo.Names {
			accum = append(accum, newStringValue(name))
		}
	}

	for _, name := range o.methodsInfo.Names {
		accum = append(accum, newStringValue(name))
	}

	return accum
}

func (o *objectGoReflect) export(*objectExportCtx) interface{} {
	return o.origValue.Interface()
}

func (o *objectGoReflect) exportType() reflect.Type {
	return o.origValue.Type()
}

func (o *objectGoReflect) equal(other objectImpl) bool {
	if other, ok := other.(*objectGoReflect); ok {
		k1, k2 := o.fieldsValue.Kind(), other.fieldsValue.Kind()
		if k1 == k2 {
			if isContainer(k1) {
				return o.fieldsValue == other.fieldsValue
			}
			return o.fieldsValue.Interface() == other.fieldsValue.Interface()
		}
	}
	return false
}

func (o *objectGoReflect) reflectValue() reflect.Value {
	return o.fieldsValue
}

func (o *objectGoReflect) setReflectValue(v reflect.Value) {
	o.fieldsValue = v
	o.origValue = v
	o.methodsValue = v.Addr()
}

func (o *objectGoReflect) esValue() Value {
	return o.val
}

func (r *Runtime) buildFieldInfo(t reflect.Type, index []int, info *reflectFieldsInfo) {
	n := t.NumField()
	for i := 0; i < n; i++ {
		field := t.Field(i)
		name := field.Name
		isExported := ast.IsExported(name)

		if !isExported && !field.Anonymous {
			continue
		}

		if r.fieldNameMapper != nil {
			name = r.fieldNameMapper.FieldName(t, field)
		}

		if name != "" && isExported {
			if inf, exists := info.Fields[name]; !exists {
				info.Names = append(info.Names, name)
			} else {
				if len(inf.Index) <= len(index) {
					continue
				}
			}
		}

		if name != "" || field.Anonymous {
			idx := make([]int, len(index)+1)
			copy(idx, index)
			idx[len(idx)-1] = i

			if name != "" && isExported {
				info.Fields[name] = reflectFieldInfo{
					Index:     idx,
					Anonymous: field.Anonymous,
				}
			}
			if field.Anonymous {
				typ := field.Type
				for typ.Kind() == reflect.Ptr {
					typ = typ.Elem()
				}
				if typ.Kind() == reflect.Struct {
					r.buildFieldInfo(typ, idx, info)
				}
			}
		}
	}
}

var emptyMethodsInfo = reflectMethodsInfo{}

func (r *Runtime) buildMethodsInfo(t reflect.Type) (info *reflectMethodsInfo) {
	n := t.NumMethod()
	if n == 0 {
		return &emptyMethodsInfo
	}
	info = new(reflectMethodsInfo)
	info.Methods = make(map[string]int, n)
	info.Names = make([]string, 0, n)
	for i := 0; i < n; i++ {
		method := t.Method(i)
		name := method.Name
		if !ast.IsExported(name) {
			continue
		}
		if r.fieldNameMapper != nil {
			name = r.fieldNameMapper.MethodName(t, method)
			if name == "" {
				continue
			}
		}

		if _, exists := info.Methods[name]; !exists {
			info.Names = append(info.Names, name)
		}

		info.Methods[name] = i
	}
	return
}

func (r *Runtime) buildFieldsInfo(t reflect.Type) (info *reflectFieldsInfo) {
	info = new(reflectFieldsInfo)
	n := t.NumField()
	info.Fields = make(map[string]reflectFieldInfo, n)
	info.Names = make([]string, 0, n)
	r.buildFieldInfo(t, nil, info)
	return
}

func (r *Runtime) fieldsInfo(t reflect.Type) (info *reflectFieldsInfo) {
	var exists bool
	if info, exists = r.fieldsInfoCache[t]; !exists {
		info = r.buildFieldsInfo(t)
		if r.fieldsInfoCache == nil {
			r.fieldsInfoCache = make(map[reflect.Type]*reflectFieldsInfo)
		}
		r.fieldsInfoCache[t] = info
	}

	return
}

func (r *Runtime) methodsInfo(t reflect.Type) (info *reflectMethodsInfo) {
	var exists bool
	if info, exists = r.methodsInfoCache[t]; !exists {
		info = r.buildMethodsInfo(t)
		if r.methodsInfoCache == nil {
			r.methodsInfoCache = make(map[reflect.Type]*reflectMethodsInfo)
		}
		r.methodsInfoCache[t] = info
	}

	return
}

// SetFieldNameMapper sets a custom field name mapper for Go types. It can be called at any time, however
// the mapping for any given value is fixed at the point of creation.
// Setting this to nil restores the default behaviour which is all exported fields and methods are mapped to their
// original unchanged names.
func (r *Runtime) SetFieldNameMapper(mapper FieldNameMapper) {
	r.fieldNameMapper = mapper
	r.fieldsInfoCache = nil
	r.methodsInfoCache = nil
}

// TagFieldNameMapper returns a FieldNameMapper that uses the given tagName for struct fields and optionally
// uncapitalises (making the first letter lower case) method names.
// The common tag value syntax is supported (name[,options]), however options are ignored.
// Setting name to anything other than a valid ECMAScript identifier makes the field hidden.
func TagFieldNameMapper(tagName string, uncapMethods bool) FieldNameMapper {
	return tagFieldNameMapper{
		tagName:      tagName,
		uncapMethods: uncapMethods,
	}
}

// UncapFieldNameMapper returns a FieldNameMapper that uncapitalises struct field and method names
// making the first letter lower case.
func UncapFieldNameMapper() FieldNameMapper {
	return uncapFieldNameMapper{}
}
