package luar

import (
	"reflect"

	"github.com/yuin/gopher-lua"
)

// LState is an wrapper for gopher-lua's LState. It should be used when you
// wish to have a function/method with the standard "func(*lua.LState) int"
// signature.
type LState struct {
	*lua.LState
}

var (
	refTypeLStatePtr      reflect.Type
	refTypeLuaLValueSlice reflect.Type
	refTypeLuaLValue      reflect.Type
	refTypeInt            reflect.Type
)

func init() {
	refTypeLStatePtr = reflect.TypeOf(&LState{})
	refTypeLuaLValueSlice = reflect.TypeOf([]lua.LValue{})
	refTypeLuaLValue = reflect.TypeOf((*lua.LValue)(nil)).Elem()
	refTypeInt = reflect.TypeOf(int(0))
}

func funcIsBypass(t reflect.Type) bool {
	if t.NumIn() == 1 && t.NumOut() == 1 && t.In(0) == refTypeLStatePtr && t.Out(0) == refTypeInt {
		return true
	}
	if t.NumIn() == 2 && t.NumOut() == 1 && t.In(1) == refTypeLStatePtr && t.Out(0) == refTypeInt {
		return true
	}
	return false
}

func funcEvaluate(L *lua.LState, fn reflect.Value) int {
	fnType := fn.Type()
	if funcIsBypass(fnType) {
		luarState := LState{L}
		args := make([]reflect.Value, 0, 2)
		if fnType.NumIn() == 2 {
			receiverHint := fnType.In(0)
			receiver := lValueToReflect(L.Get(1), receiverHint)
			if receiver.Type() != receiverHint {
				L.RaiseError("incorrect receiver type")
			}
			args = append(args, receiver)
			L.Remove(1)
		}
		args = append(args, reflect.ValueOf(&luarState))
		return fn.Call(args)[0].Interface().(int)
	}

	top := L.GetTop()
	expected := fnType.NumIn()
	variadic := fnType.IsVariadic()
	if !variadic && top != expected {
		L.RaiseError("invalid number of function arguments (%d expected, got %d)", expected, top)
	}
	if variadic && top < expected-1 {
		L.RaiseError("invalid number of function arguments (%d or more expected, got %d)", expected-1, top)
	}
	args := make([]reflect.Value, top)
	for i := 0; i < L.GetTop(); i++ {
		var hint reflect.Type
		if variadic && i >= expected-1 {
			hint = fnType.In(expected - 1).Elem()
		} else {
			hint = fnType.In(i)
		}
		args[i] = lValueToReflect(L.Get(i+1), hint)
	}
	ret := fn.Call(args)
	if len(ret) == 1 && ret[0].Type() == refTypeLuaLValueSlice {
		values := ret[0].Interface().([]lua.LValue)
		for _, value := range values {
			L.Push(value)
		}
		return len(values)
	}
	for _, val := range ret {
		L.Push(New(L, val.Interface()))
	}
	return len(ret)
}

func funcWrapper(L *lua.LState, fn reflect.Value) *lua.LFunction {
	wrapper := func(L *lua.LState) int {
		return funcEvaluate(L, fn)
	}
	return L.NewFunction(wrapper)
}
