package luar

import (
	"fmt"
	"reflect"

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

var wrapperMetatable map[string]map[string]lua.LGFunction

func init() {
	wrapperMetatable = map[string]map[string]lua.LGFunction{
		"chan": {
			"__index":    chanIndex,
			"__len":      chanLen,
			"__tostring": allTostring,
			"__eq":       chanEq,
		},
		"map": {
			"__index":    mapIndex,
			"__newindex": mapNewIndex,
			"__len":      mapLen,
			"__call":     mapCall,
			"__tostring": allTostring,
			"__eq":       mapEq,
		},
		"ptr": {
			"__index":    ptrIndex,
			"__newindex": ptrNewIndex,
			"__pow":      ptrPow,
			"__call":     ptrCall,
			"__tostring": allTostring,
			"__unm":      ptrUnm,
			"__eq":       ptrEq,
		},
		"slice": {
			"__index":    sliceIndex,
			"__newindex": sliceNewIndex,
			"__len":      sliceLen,
			"__tostring": allTostring,
			"__eq":       sliceEq,
		},
		"struct": {
			"__index":    structIndex,
			"__newindex": structNewIndex,
			"__call":     structCall,
			"__tostring": allTostring,
		},
		"type": {
			"__call":     typeCall,
			"__tostring": allTostring,
			"__eq":       typeEq,
		},
	}
}

func ensureMetatable(L *lua.LState) *lua.LTable {
	const metatableKey = lua.LString("github.com/layeh/gopher-luar")
	v := L.G.Registry.RawGetH(metatableKey)
	if v != lua.LNil {
		return v.(*lua.LTable)
	}
	newTable := L.NewTable()

	for typeName, meta := range wrapperMetatable {
		typeTable := L.NewTable()
		typeTable.RawSetH(lua.LString("__metatable"), lua.LTrue)
		for methodName, methodFunc := range meta {
			typeTable.RawSetH(lua.LString(methodName), L.NewFunction(methodFunc))
		}
		newTable.RawSetH(lua.LString(typeName), typeTable)
	}

	L.G.Registry.RawSetH(metatableKey, newTable)
	return newTable
}

func allTostring(L *lua.LState) int {
	ud := L.CheckUserData(1)
	value := ud.Value
	if stringer, ok := value.(fmt.Stringer); ok {
		L.Push(lua.LString(stringer.String()))
	} else {
		L.Push(lua.LString(fmt.Sprintf("userdata (luar): %p", ud)))
	}
	return 1
}

// New creates and returns a new lua.LValue for the given value.
//
// The following types are supported:
//  Kind            gopher-lua Type
//  -------------------------------
//  nil             LNil
//  Bool            LBool
//  Int             LNumber
//  Int8            LNumber
//  Int16           LNumber
//  Int32           LNumber
//  Int64           LNumber
//  Uint            LNumber
//  Uint8           LNumber
//  Uint32          LNumber
//  Uint64          LNumber
//  Float32         LNumber
//  Float64         LNumber
//  Complex64       *LUserData
//  Complex128      *LUserData
//  Array           *LUserData
//  Chan            *LUserData
//  Interface       *LUserData
//  Func            *lua.LFunction
//  Map             *LUserData
//  Ptr             *LUserData
//  Slice           *LUserData
//  String          LString
//  Struct          *LUserData
//  UnsafePointer   *LUserData
func New(L *lua.LState, value interface{}) lua.LValue {
	if value == nil {
		return lua.LNil
	}
	if lval, ok := value.(lua.LValue); ok {
		return lval
	}

	val := reflect.ValueOf(value)
	switch val.Kind() {
	case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice:
		if val.IsNil() {
			return lua.LNil
		}
	}

	table := ensureMetatable(L)

	switch val.Kind() {
	case reflect.Bool:
		return lua.LBool(val.Bool())
	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
		return lua.LNumber(float64(val.Int()))
	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
		return lua.LNumber(float64(val.Uint()))
	case reflect.Float32, reflect.Float64:
		return lua.LNumber(val.Float())
	case reflect.Chan:
		ud := L.NewUserData()
		ud.Value = val.Interface()
		ud.Metatable = table.RawGetH(lua.LString("chan"))
		return ud
	case reflect.Func:
		return funcWrapper(L, val)
	case reflect.Interface:
		ud := L.NewUserData()
		ud.Value = val.Interface()
		return ud
	case reflect.Map:
		ud := L.NewUserData()
		ud.Value = val.Interface()
		ud.Metatable = table.RawGetH(lua.LString("map"))
		return ud
	case reflect.Ptr:
		ud := L.NewUserData()
		ud.Value = val.Interface()
		ud.Metatable = table.RawGetH(lua.LString("ptr"))
		return ud
	case reflect.Slice:
		ud := L.NewUserData()
		ud.Value = val.Interface()
		ud.Metatable = table.RawGetH(lua.LString("slice"))
		return ud
	case reflect.String:
		return lua.LString(val.String())
	case reflect.Struct:
		ud := L.NewUserData()
		ud.Value = val.Interface()
		ud.Metatable = table.RawGetH(lua.LString("struct"))
		return ud
	default:
		ud := L.NewUserData()
		ud.Value = val.Interface()
		return ud
	}
}

// NewType returns a new type creator for the given value's type.
//
// When the lua.LValue is called, a new value will be created that is the
// same type as value's type.
func NewType(L *lua.LState, value interface{}) lua.LValue {
	table := ensureMetatable(L)

	val := reflect.TypeOf(value)
	ud := L.NewUserData()
	ud.Value = val
	ud.Metatable = table.RawGetH(lua.LString("type"))
	return ud
}

func lValueToReflect(v lua.LValue, hint reflect.Type) reflect.Value {
	if hint == refTypeLuaLValue {
		return reflect.ValueOf(v)
	}
	switch converted := v.(type) {
	case lua.LBool:
		return reflect.ValueOf(bool(converted))
	case lua.LChannel:
		return reflect.ValueOf(converted)
	case lua.LNumber:
		return reflect.ValueOf(converted).Convert(hint)
	case *lua.LFunction:
		return reflect.ValueOf(converted)
	case *lua.LNilType:
		return reflect.Zero(hint)
	case *lua.LState:
		return reflect.ValueOf(converted)
	case lua.LString:
		return reflect.ValueOf(string(converted))
	case *lua.LTable:
		return reflect.ValueOf(converted)
	case *lua.LUserData:
		return reflect.ValueOf(converted.Value)
	}
	panic("fatal lValueToReflect error")
}
