package starbind

import (
	"errors"
	"fmt"
	"math"
	"reflect"
	"strconv"

	"go.starlark.net/starlark"

	"github.com/aarzilli/gdlv/internal/dlvclient/service/api"
	"github.com/aarzilli/gdlv/internal/prettyprint"
)

// autoLoadConfig is the load configuration used to automatically load more from a variable
var autoLoadConfig = api.LoadConfig{MaxVariableRecurse: 1, MaxStringLen: 1024, MaxArrayValues: 64, MaxStructFields: -1}

type WrappedVariable interface {
	UnwrapVariable() *api.Variable
}

var _ WrappedVariable = structVariableAsStarlarkValue{}
var _ WrappedVariable = sliceVariableAsStarlarkValue{}
var _ WrappedVariable = ptrVariableAsStarlarkValue{}

// interfaceToStarlarkValue converts an interface{} variable (produced by
// decoding JSON) into a starlark.Value.
func (env *Env) interfaceToStarlarkValue(v any) starlark.Value {
	switch v := v.(type) {
	case bool:
		return starlark.Bool(v)
	case uint8:
		return starlark.MakeUint64(uint64(v))
	case uint16:
		return starlark.MakeUint64(uint64(v))
	case uint32:
		return starlark.MakeUint64(uint64(v))
	case uint64:
		return starlark.MakeUint64(v)
	case uintptr:
		return starlark.MakeUint64(uint64(v))
	case uint:
		return starlark.MakeUint64(uint64(v))
	case int8:
		return starlark.MakeInt64(int64(v))
	case int16:
		return starlark.MakeInt64(int64(v))
	case int32:
		return starlark.MakeInt64(int64(v))
	case int64:
		return starlark.MakeInt64(v)
	case int:
		return starlark.MakeInt64(int64(v))
	case string:
		return starlark.String(v)
	case map[string]uint64:
		// this is the only map type that we use in the api, if we ever want to
		// add more maps to the api a more general approach will be necessary.
		var r starlark.Dict
		for k, v := range v {
			r.SetKey(starlark.String(k), starlark.MakeUint64(v))
		}
		return &r
	case nil:
		return starlark.None
	case error:
		return starlark.String(v.Error())
	case reflect.Value:
		if v.Type().Kind() == reflect.Struct {
			return structAsStarlarkValue{v, env}
		}
		return env.interfaceToStarlarkValue(v.Interface())
	default:
		vval := reflect.ValueOf(v)
		switch vval.Type().Kind() {
		case reflect.Ptr:
			if vval.IsNil() {
				return starlark.None
			}
			vval = vval.Elem()
			if vval.Type().Kind() == reflect.Struct {
				return structAsStarlarkValue{vval, env}
			}
		case reflect.Struct:
			return structAsStarlarkValue{vval, env}
		case reflect.Slice:
			return sliceAsStarlarkValue{vval, env}
		}
		return starlark.String(fmt.Sprintf("%v", v))
	}
}

// sliceAsStarlarkValue converts a reflect.Value containing a slice
// into a starlark value.
// The public methods of sliceAsStarlarkValue implement the Indexable and
// Sequence starlark interfaces.
type sliceAsStarlarkValue struct {
	v   reflect.Value
	env *Env
}

var _ starlark.Indexable = sliceAsStarlarkValue{}
var _ starlark.Sequence = sliceAsStarlarkValue{}

func (v sliceAsStarlarkValue) Freeze() {
}

func (v sliceAsStarlarkValue) Hash() (uint32, error) {
	return 0, errors.New("not hashable")
}

func (v sliceAsStarlarkValue) String() string {
	if x, ok := v.v.Interface().([]byte); ok {
		return string(x)
	}
	return fmt.Sprintf("%#v", v.v)
}

func (v sliceAsStarlarkValue) Truth() starlark.Bool {
	return v.v.Len() != 0
}

func (v sliceAsStarlarkValue) Type() string {
	return v.v.Type().String()
}

func (v sliceAsStarlarkValue) Index(i int) starlark.Value {
	if i >= v.v.Len() {
		return nil
	}
	return v.env.interfaceToStarlarkValue(v.v.Index(i).Interface())
}

func (v sliceAsStarlarkValue) Len() int {
	return v.v.Len()
}

func (v sliceAsStarlarkValue) Iterate() starlark.Iterator {
	return &sliceAsStarlarkValueIterator{0, v.v, v.env}
}

type sliceAsStarlarkValueIterator struct {
	cur int
	v   reflect.Value
	env *Env
}

func (it *sliceAsStarlarkValueIterator) Done() {
}

func (it *sliceAsStarlarkValueIterator) Next(p *starlark.Value) bool {
	if it.cur >= it.v.Len() {
		return false
	}
	*p = it.env.interfaceToStarlarkValue(it.v.Index(it.cur).Interface())
	it.cur++
	return true
}

// structAsStarlarkValue converts any Go struct into a starlark.Value.
// The public methods of structAsStarlarkValue implement the
// starlark.HasAttrs interface.
type structAsStarlarkValue struct {
	v   reflect.Value
	env *Env
}

var _ starlark.HasAttrs = structAsStarlarkValue{}

func (v structAsStarlarkValue) Freeze() {
}

func (v structAsStarlarkValue) Hash() (uint32, error) {
	return 0, errors.New("not hashable")
}

func (v structAsStarlarkValue) String() string {
	if vv, ok := v.v.Interface().(api.Variable); ok {
		return fmt.Sprintf("Variable<%s>", prettyprint.Singleline(&vv, true, false))
	}
	return fmt.Sprintf("%#v", v.v)
}

func (v structAsStarlarkValue) Truth() starlark.Bool {
	return true
}

func (v structAsStarlarkValue) Type() string {
	if vv, ok := v.v.Interface().(api.Variable); ok {
		return fmt.Sprintf("Variable<%s>", vv.Type)
	}
	return v.v.Type().String()
}

func (v structAsStarlarkValue) Attr(name string) (starlark.Value, error) {
	if r, err := v.valueAttr(name); err != nil || r != nil {
		return r, err
	}
	r := v.v.FieldByName(name)
	if !r.IsValid() {
		return starlark.None, starlark.NoSuchAttrError(fmt.Sprintf("no field named %q in %T", name, v.v.Interface()))
	}
	return v.env.interfaceToStarlarkValue(r), nil
}

func (v structAsStarlarkValue) SetField(name string, value starlark.Value) (err error) {
	defer func() {
		// reflect.Value.SetInt, SetFloat, etc panic
		ierr := recover()
		if ierr == nil {
			return
		}
		err, _ = ierr.(error)
		if err == nil {
			panic(ierr)
		}
		err = fmt.Errorf("can not assign to %T.%q: %v", v.v.Interface(), name, err)
	}()
	if r, err := v.valueAttr(name); err != nil || r != nil {
		return starlark.NoSuchAttrError(fmt.Sprintf("no field named %s in %T", name, v.v.Interface()))
	}
	r := v.v.FieldByName(name)
	if !r.IsValid() {
		return starlark.NoSuchAttrError(fmt.Sprintf("no field named %q in %T", name, v.v.Interface()))
	}
	switch value := value.(type) {
	case starlark.Int:
		n, ok := value.Int64()
		if !ok {
			return fmt.Errorf("can not assign big integer to %T.%q", v.v.Interface(), name)
		}
		r.SetInt(n)
	case starlark.Float:
		r.SetFloat(float64(value))
	case starlark.String:
		r.SetString(value.GoString())
	case starlark.Bool:
		r.SetBool(bool(value))
	default:
		return fmt.Errorf("can not assign value of type %T to %T.%q", value, v.v.Interface(), name)
	}
	return nil
}

func (v structAsStarlarkValue) valueAttr(name string) (starlark.Value, error) {
	if v.v.Type().Name() != "Variable" || (name != "Value" && name != "Expr") {
		return nil, nil
	}
	v2 := v.v.Interface().(api.Variable)

	if name == "Expr" {
		return starlark.String(varAddrExpr(&v2)), nil
	}

	return v.env.variableValueToStarlarkValue(&v2, true)
}

func varAddrExpr(v *api.Variable) string {
	return fmt.Sprintf("(*(*%q)(%#x))", v.Type, v.Addr)
}

func (env *Env) variableValueToStarlarkValue(v *api.Variable, top bool) (starlark.Value, error) {
	if !top && v.Addr == 0 && v.Value == "" {
		return starlark.None, nil
	}

	switch v.Kind {
	case reflect.Struct:
		if v.Len != 0 && len(v.Children) == 0 {
			return starlark.None, errors.New("value not loaded")
		}
		return structVariableAsStarlarkValue{v, env}, nil
	case reflect.Slice, reflect.Array:
		if v.Len != 0 && len(v.Children) == 0 {
			return starlark.None, errors.New("value not loaded")
		}
		return sliceVariableAsStarlarkValue{v, env}, nil
	case reflect.Map:
		if v.Len != 0 && len(v.Children) == 0 {
			return starlark.None, errors.New("value not loaded")
		}
		return mapVariableAsStarlarkValue{v, env}, nil
	case reflect.String:
		return starlark.String(v.Value), nil
	case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int:
		n, _ := strconv.ParseInt(prettyprint.ExtractIntValue(v.Value), 0, 64)
		return starlark.MakeInt64(n), nil
	case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint, reflect.Uintptr:
		n, _ := strconv.ParseUint(prettyprint.ExtractIntValue(v.Value), 0, 64)
		return starlark.MakeUint64(n), nil
	case reflect.Bool:
		n, _ := strconv.ParseBool(v.Value)
		return starlark.Bool(n), nil
	case reflect.Float32, reflect.Float64:
		switch v.Value {
		case "+Inf":
			return starlark.Float(math.Inf(+1)), nil
		case "-Inf":
			return starlark.Float(math.Inf(-1)), nil
		case "NaN":
			return starlark.Float(math.NaN()), nil
		default:
			n, _ := strconv.ParseFloat(v.Value, 64)
			return starlark.Float(n), nil
		}
	case reflect.Ptr, reflect.Interface:
		if len(v.Children) > 0 {
			v.Children[0] = *env.autoLoad(varAddrExpr(&v.Children[0]))
		}
		return ptrVariableAsStarlarkValue{v, env}, nil
	}
	return nil, nil
}

func (env *Env) autoLoad(expr string) *api.Variable {
	v, err := env.ctx.Client().EvalVariable(api.EvalScope{GoroutineID: -1}, expr, autoLoadConfig)
	if err != nil {
		return &api.Variable{Unreadable: err.Error()}
	}
	return v
}

func (v structAsStarlarkValue) AttrNames() []string {
	typ := v.v.Type()
	r := make([]string, 0, typ.NumField()+1)
	for i := 0; i < typ.NumField(); i++ {
		r = append(r, typ.Field(i).Name)
	}
	return r
}

// structVariableAsStarlarkValue converts an api.Variable representing a
// struct variable (in the target process) into a starlark.Value.
// The public methods of structVariableAsStarlarkValue implement the
// starlark.HasAttrs and starlark.Mapping interfaces.
type structVariableAsStarlarkValue struct {
	v   *api.Variable
	env *Env
}

var _ starlark.HasAttrs = structVariableAsStarlarkValue{}
var _ starlark.Mapping = structVariableAsStarlarkValue{}

func (v structVariableAsStarlarkValue) Freeze() {
}

func (v structVariableAsStarlarkValue) Hash() (uint32, error) {
	return 0, errors.New("not hashable")
}

func (v structVariableAsStarlarkValue) String() string {
	return prettyprint.Singleline(v.v, true, false)
}

func (v structVariableAsStarlarkValue) Truth() starlark.Bool {
	return true
}

func (v structVariableAsStarlarkValue) Type() string {
	return v.v.Type
}

func (v structVariableAsStarlarkValue) Attr(name string) (starlark.Value, error) {
	for i := range v.v.Children {
		if v.v.Children[i].Name == name {
			v2 := v.env.autoLoad(varAddrExpr(&v.v.Children[i]))
			return v.env.variableValueToStarlarkValue(v2, false)
		}
	}
	return nil, nil // no such field or method
}

func (v structVariableAsStarlarkValue) AttrNames() []string {
	r := make([]string, len(v.v.Children))
	for i := range v.v.Children {
		r[i] = v.v.Children[i].Name
	}
	return r
}

func (v structVariableAsStarlarkValue) Get(key starlark.Value) (starlark.Value, bool, error) {
	skey, ok := key.(starlark.String)
	if !ok {
		return starlark.None, false, nil
	}
	r, err := v.Attr(string(skey))
	if r == nil && err == nil {
		return starlark.None, false, nil
	}
	if err != nil {
		return starlark.None, false, err
	}
	return r, true, nil
}

func (v structVariableAsStarlarkValue) UnwrapVariable() *api.Variable {
	return v.v
}

type sliceVariableAsStarlarkValue struct {
	v   *api.Variable
	env *Env
}

var _ starlark.Indexable = sliceVariableAsStarlarkValue{}
var _ starlark.Sequence = sliceVariableAsStarlarkValue{}

func (v sliceVariableAsStarlarkValue) Freeze() {
}

func (v sliceVariableAsStarlarkValue) Hash() (uint32, error) {
	return 0, errors.New("not hashable")
}

func (v sliceVariableAsStarlarkValue) String() string {
	return prettyprint.Singleline(v.v, true, false)
}

func (v sliceVariableAsStarlarkValue) Truth() starlark.Bool {
	return v.v.Len != 0
}

func (v sliceVariableAsStarlarkValue) Type() string {
	return v.v.Type
}

func (v sliceVariableAsStarlarkValue) Index(i int) starlark.Value {
	if i >= v.Len() {
		return nil
	}
	v2 := v.env.autoLoad(fmt.Sprintf("%s[%d]", varAddrExpr(v.v), i))
	r, err := v.env.variableValueToStarlarkValue(v2, false)
	if err != nil {
		return starlark.String(err.Error())
	}
	return r
}

func (v sliceVariableAsStarlarkValue) Len() int {
	return int(v.v.Len)
}

func (v sliceVariableAsStarlarkValue) Iterate() starlark.Iterator {
	return &sliceVariableAsStarlarkValueIterator{0, v.v, v.env}
}

func (v sliceVariableAsStarlarkValue) UnwrapVariable() *api.Variable {
	return v.v
}

type sliceVariableAsStarlarkValueIterator struct {
	cur int64
	v   *api.Variable
	env *Env
}

func (it *sliceVariableAsStarlarkValueIterator) Done() {
}

func (it *sliceVariableAsStarlarkValueIterator) Next(p *starlark.Value) bool {
	if it.cur >= it.v.Len {
		return false
	}
	s := sliceVariableAsStarlarkValue{it.v, it.env}
	*p = s.Index(int(it.cur))
	it.cur++
	return true
}

type ptrVariableAsStarlarkValue struct {
	v   *api.Variable
	env *Env
}

var _ starlark.HasAttrs = ptrVariableAsStarlarkValue{}
var _ starlark.Mapping = ptrVariableAsStarlarkValue{}

func (v ptrVariableAsStarlarkValue) Freeze() {
}

func (v ptrVariableAsStarlarkValue) Hash() (uint32, error) {
	return 0, errors.New("not hashable")
}

func (v ptrVariableAsStarlarkValue) String() string {
	return prettyprint.Singleline(v.v, true, false)
}

func (v ptrVariableAsStarlarkValue) Truth() starlark.Bool {
	return true
}

func (v ptrVariableAsStarlarkValue) Type() string {
	return v.v.Type
}

func (v ptrVariableAsStarlarkValue) Attr(name string) (starlark.Value, error) {
	if len(v.v.Children) == 0 {
		return nil, nil // no such field or method
	}
	if v.v.Children[0].Kind == reflect.Struct {
		// autodereference pointers to structs
		x := structVariableAsStarlarkValue{&v.v.Children[0], v.env}
		return x.Attr(name)
	} else if v.v.Kind == reflect.Interface && v.v.Children[0].Kind == reflect.Ptr {
		// allow double-autodereference for iface to ptr to struct
		vchild := &v.v.Children[0]
		if len(vchild.Children) > 0 {
			vchild.Children[0] = *v.env.autoLoad(varAddrExpr(&vchild.Children[0]))
		}
		v2 := ptrVariableAsStarlarkValue{vchild, v.env}
		return v2.Attr(name)
	}

	return nil, nil
}

func (v ptrVariableAsStarlarkValue) AttrNames() []string {
	if len(v.v.Children) == 0 {
		// The pointer variable was not loaded; we don't know the field names.
		return nil
	}
	if v.v.Children[0].Kind != reflect.Struct {
		return nil
	}
	// autodereference: present the field names of the pointed-to struct as the
	// fields of this pointer variable.
	x := structVariableAsStarlarkValue{&v.v.Children[0], v.env}
	return x.AttrNames()
}

func (v ptrVariableAsStarlarkValue) Get(key starlark.Value) (starlark.Value, bool, error) {
	if ikey, ok := key.(starlark.Int); ok {
		if len(v.v.Children) == 0 {
			return starlark.None, true, nil
		}
		if idx, _ := ikey.Int64(); idx == 0 {
			r, err := v.env.variableValueToStarlarkValue(&v.v.Children[0], false)
			if err != nil {
				return starlark.String(err.Error()), true, nil
			}
			return r, true, nil
		}
		return starlark.None, false, nil
	}

	if len(v.v.Children) == 0 || v.v.Children[0].Kind != reflect.Struct {
		return starlark.None, false, nil
	}
	// autodereference
	x := structVariableAsStarlarkValue{&v.v.Children[0], v.env}
	return x.Get(key)
}

func (v ptrVariableAsStarlarkValue) UnwrapVariable() *api.Variable {
	return v.v
}

type mapVariableAsStarlarkValue struct {
	v   *api.Variable
	env *Env
}

var _ starlark.IterableMapping = mapVariableAsStarlarkValue{}

func (v mapVariableAsStarlarkValue) Freeze() {
}

func (v mapVariableAsStarlarkValue) Hash() (uint32, error) {
	return 0, errors.New("not hashable")
}

func (v mapVariableAsStarlarkValue) String() string {
	return prettyprint.Singleline(v.v, true, false)
}

func (v mapVariableAsStarlarkValue) Truth() starlark.Bool {
	return true
}

func (v mapVariableAsStarlarkValue) Type() string {
	return v.v.Type
}

func (v mapVariableAsStarlarkValue) Get(key starlark.Value) (starlark.Value, bool, error) {
	var keyExpr string
	switch key := key.(type) {
	case starlark.Int:
		keyExpr = key.String()
	case starlark.Float:
		keyExpr = fmt.Sprintf("%g", float64(key))
	case starlark.String:
		keyExpr = fmt.Sprintf("%q", string(key))
	case starlark.Bool:
		keyExpr = fmt.Sprintf("%v", bool(key))
	case structVariableAsStarlarkValue:
		keyExpr = varAddrExpr(key.v)
	default:
		return starlark.None, false, fmt.Errorf("key type not supported %T", key)
	}

	v2 := v.env.autoLoad(fmt.Sprintf("%s[%s]", varAddrExpr(v.v), keyExpr))
	r, err := v.env.variableValueToStarlarkValue(v2, false)
	if err != nil {
		if err.Error() == "key not found" {
			return starlark.None, false, nil
		}
		return starlark.None, false, err
	}
	return r, true, nil
}

func (v mapVariableAsStarlarkValue) Items() []starlark.Tuple {
	r := make([]starlark.Tuple, 0, len(v.v.Children)/2)
	for i := 0; i < len(v.v.Children); i += 2 {
		r = append(r, mapStarlarkTupleAt(v.v, v.env, i))
	}
	return r
}

func mapStarlarkTupleAt(v *api.Variable, env *Env, i int) starlark.Tuple {
	keyv := env.autoLoad(varAddrExpr(&v.Children[i]))
	key, err := env.variableValueToStarlarkValue(keyv, false)
	if err != nil {
		key = starlark.None
	}
	valv := env.autoLoad(varAddrExpr(&v.Children[i+1]))
	val, err := env.variableValueToStarlarkValue(valv, false)
	if err != nil {
		val = starlark.None
	}
	return starlark.Tuple{key, val}
}

func (v mapVariableAsStarlarkValue) Iterate() starlark.Iterator {
	return &mapVariableAsStarlarkValueIterator{0, v.v, v.env}
}

type mapVariableAsStarlarkValueIterator struct {
	cur int
	v   *api.Variable
	env *Env
}

func (it *mapVariableAsStarlarkValueIterator) Done() {
}

func (it *mapVariableAsStarlarkValueIterator) Next(p *starlark.Value) bool {
	if it.cur >= 2*int(it.v.Len) {
		return false
	}
	if it.cur >= len(it.v.Children) {
		v2 := it.env.autoLoad(fmt.Sprintf("%s[%d:]", varAddrExpr(it.v), len(it.v.Children)/2))
		it.v.Children = append(it.v.Children, v2.Children...)
	}
	if it.cur >= len(it.v.Children) {
		return false
	}

	keyv := it.env.autoLoad(varAddrExpr(&it.v.Children[it.cur]))
	key, err := it.env.variableValueToStarlarkValue(keyv, false)
	if err != nil {
		key = starlark.None
	}
	*p = key

	it.cur += 2
	return true
}

// unmarshalStarlarkValue unmarshals a starlark.Value 'val' into a Go variable 'dst'.
// This works similarly to encoding/json.Unmarshal and similar functions,
// but instead of getting its input from a byte buffer, it uses a
// starlark.Value.
func unmarshalStarlarkValue(val starlark.Value, dst any, path string) error {
	return unmarshalStarlarkValueIntl(val, reflect.ValueOf(dst), path)
}

func unmarshalStarlarkValueIntl(val starlark.Value, dst reflect.Value, path string) (err error) {
	defer func() {
		// catches reflect panics
		ierr := recover()
		if ierr != nil {
			err = fmt.Errorf("error setting argument %q to %s: %v", path, val, ierr)
		}
	}()

	converr := func(args ...string) error {
		if len(args) > 0 {
			return fmt.Errorf("error setting argument %q: can not convert %s to %s: %s", path, val, dst.Type().String(), args[0])
		}
		return fmt.Errorf("error setting argument %q: can not convert %s to %s", path, val, dst.Type().String())
	}

	if _, isnone := val.(starlark.NoneType); isnone {
		return nil
	}

	for dst.Kind() == reflect.Ptr {
		if dst.IsNil() {
			dst.Set(reflect.New(dst.Type().Elem()))
		}
		dst = dst.Elem()
	}

	switch val := val.(type) {
	case starlark.Bool:
		dst.SetBool(bool(val))
	case starlark.Int:
		switch dst.Kind() {
		case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
			n, ok := val.Uint64()
			if !ok {
				return converr()
			}
			dst.SetUint(n)
		case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
			n, ok := val.Int64()
			if !ok {
				return converr()
			}
			dst.SetInt(n)
		default:
			return converr()
		}
	case starlark.Float:
		dst.SetFloat(float64(val))
	case starlark.String:
		dst.SetString(string(val))
	case *starlark.List:
		if dst.Kind() != reflect.Slice {
			return converr()
		}
		dst.Set(reflect.MakeSlice(dst.Type(), val.Len(), val.Len()))
		for i := 0; i < val.Len(); i++ {
			cur := dst.Index(i).Addr()
			err := unmarshalStarlarkValueIntl(val.Index(i), cur, path)
			if err != nil {
				return err
			}
		}
	case *starlark.Dict:
		if dst.Kind() != reflect.Struct {
			return converr()
		}
		for _, k := range val.Keys() {
			if _, ok := k.(starlark.String); !ok {
				return converr(fmt.Sprintf("non-string key %q", k.String()))
			}
			fieldName := string(k.(starlark.String))
			dstfield := dst.FieldByName(fieldName)
			if !dstfield.IsValid() {
				return converr(fmt.Sprintf("unknown field %s", fieldName))
			}
			valfield, _, _ := val.Get(starlark.String(fieldName))
			err := unmarshalStarlarkValueIntl(valfield, dstfield, path+"."+fieldName)
			if err != nil {
				return err
			}
		}
	case structAsStarlarkValue:
		rv := val.v
		if rv.Kind() == reflect.Ptr {
			rv = rv.Elem()
		}
		dst.Set(rv)
	default:
		return converr()
	}
	return nil
}
