// Copyright 2018 The GopherWasm Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// +build !wasm

package js

import (
	"reflect"
	"unsafe"

	"github.com/gopherjs/gopherjs/js"
)

type Type int

const (
	TypeUndefined Type = iota
	TypeNull
	TypeBoolean
	TypeNumber
	TypeString
	TypeSymbol
	TypeObject
	TypeFunction
)

func (t Type) String() string {
	switch t {
	case TypeUndefined:
		return "undefined"
	case TypeNull:
		return "null"
	case TypeBoolean:
		return "boolean"
	case TypeNumber:
		return "number"
	case TypeString:
		return "string"
	case TypeSymbol:
		return "symbol"
	case TypeObject:
		return "object"
	case TypeFunction:
		return "function"
	default:
		panic("bad type")
	}
}

func Global() Value {
	return Value{v: js.Global}
}

func Null() Value {
	return Value{v: nil}
}

func Undefined() Value {
	return Value{v: js.Undefined}
}

type Func struct {
	Value
}

func (f Func) Release() {
	f.Value = Null()
}

func FuncOf(fn func(this Value, args []Value) interface{}) Func {
	return Func{
		Value: Value{
			v: js.MakeFunc(func(this *js.Object, args []*js.Object) interface{} {
				vargs := make([]Value, len(args))
				for i, a := range args {
					vargs[i] = Value{a}
				}
				return fn(Value{this}, vargs)
			}),
		},
	}
}

type Error struct {
	Value
}

func (e Error) Error() string {
	return "JavaScript error: " + e.Get("message").String()
}

type Value struct {
	v *js.Object
}

var (
	id           *js.Object
	instanceOf   *js.Object
	getValueType *js.Object
)

func init() {
	if js.Global != nil {
		id = js.Global.Call("eval", "(function(x) { return x; })")
		instanceOf = js.Global.Call("eval", "(function(x, y) { return x instanceof y; })")
		getValueType = js.Global.Call("eval", `(function(x) {
  if (typeof(x) === "undefined") {
    return 0; // TypeUndefined
  }
  if (x === null) {
    return 1; // TypeNull
  }
  if (typeof(x) === "boolean") {
    return 2; // TypeBoolean
  }
  if (typeof(x) === "number") {
    return 3; // TypeNumber
  }
  if (typeof(x) === "string") {
    return 4; // TypeString
  }
  if (typeof(x) === "symbol") {
    return 5; // TypeSymbol
  }
  if (typeof(x) === "function") {
    return 7; // TypeFunction
  }
  return 6; // TypeObject
})`)
	}
}

func ValueOf(x interface{}) Value {
	switch x := x.(type) {
	case Value:
		return x
	case Func:
		return x.Value
	case TypedArray:
		return x.Value
	case nil:
		return Null()
	case bool, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64, unsafe.Pointer, string, map[string]interface{}, []interface{}:
		return Value{v: id.Invoke(x)}
	default:
		panic(`invalid arg: ` + reflect.TypeOf(x).String())
	}
}

func (v Value) Bool() bool {
	if vType := v.Type(); vType != TypeBoolean {
		panic(&ValueError{"Value.Bool", vType})
	}
	return v.v.Bool()
}

func convertArgs(args []interface{}) []interface{} {
	newArgs := []interface{}{}
	for _, arg := range args {
		v := ValueOf(arg)
		newArgs = append(newArgs, v.v)
	}
	return newArgs
}

func (v Value) Call(m string, args ...interface{}) Value {
	if vType := v.Type(); vType != TypeObject && vType != TypeFunction {
		panic(&ValueError{"Value.Call", vType})
	}
	if propType := v.Get(m).Type(); propType != TypeFunction {
		panic("js: Value.Call: property " + m + " is not a function, got " + propType.String())
	}
	return Value{v: v.v.Call(m, convertArgs(args)...)}
}

func (v Value) Float() float64 {
	if vType := v.Type(); vType != TypeNumber {
		panic(&ValueError{"Value.Float", vType})
	}
	return v.v.Float()
}

func (v Value) Get(p string) Value {
	return Value{v: v.v.Get(p)}
}

func (v Value) Index(i int) Value {
	return Value{v: v.v.Index(i)}
}

func (v Value) Int() int {
	if vType := v.Type(); vType != TypeNumber {
		panic(&ValueError{"Value.Int", vType})
	}
	return v.v.Int()
}

func (v Value) Invoke(args ...interface{}) Value {
	if vType := v.Type(); vType != TypeFunction {
		panic(&ValueError{"Value.Invoke", vType})
	}
	return Value{v: v.v.Invoke(convertArgs(args)...)}
}

func (v Value) Length() int {
	return v.v.Length()
}

func (v Value) New(args ...interface{}) Value {
	return Value{v: v.v.New(convertArgs(args)...)}
}

func (v Value) Set(p string, x interface{}) {
	v.v.Set(p, x)
}

func (v Value) SetIndex(i int, x interface{}) {
	v.v.SetIndex(i, x)
}

func (v Value) String() string {
	return v.v.String()
}

func (v Value) InstanceOf(t Value) bool {
	return instanceOf.Invoke(v, t).Bool()
}

func (v Value) Type() Type {
	return Type(getValueType.Invoke(v).Int())
}

type TypedArray struct {
	Value
}

func TypedArrayOf(slice interface{}) TypedArray {
	switch slice := slice.(type) {
	case []int8, []int16, []int32, []uint8, []uint16, []uint32, []float32, []float64:
		return TypedArray{Value{v: id.Invoke(slice)}}
	default:
		panic("TypedArrayOf: not a supported slice")
	}
}

func (t *TypedArray) Release() {
	t.Value = Null()
}

func GetInternalObject(v Value) interface{} {
	return v.v
}

type ValueError struct {
	Method string
	Type   Type
}

func (e *ValueError) Error() string {
	return "syscall/js: call of " + e.Method + " on " + e.Type.String()
}
