////////////////////////////////////////////////////////////////////////////////
// Copyright © 2022 xx foundation                                             //
//                                                                            //
// Use of this source code is governed by a license that can be found in the  //
// LICENSE file.                                                              //
////////////////////////////////////////////////////////////////////////////////

//go:build js && wasm

package utils

import (
	"github.com/pkg/errors"
	jww "github.com/spf13/jwalterweatherman"
	"syscall/js"
)

var (
	// Error is the Javascript Error type. It used to create new Javascript
	// errors.
	Error = js.Global().Get("Error")

	// JSON is the Javascript JSON type. It is used to perform JSON operations
	// on the Javascript layer.
	JSON = js.Global().Get("JSON")

	// Object is the Javascript Object type. It is used to perform Object
	// operations on the Javascript layer.
	Object = js.Global().Get("Object")

	// Promise is the Javascript Promise type. It is used to generate new
	// promises.
	Promise = js.Global().Get("Promise")

	// Uint8Array is the Javascript Uint8Array type. It is used to create new
	// Uint8Array.
	Uint8Array = js.Global().Get("Uint8Array")
)

// WrapCB wraps a Javascript function in an object so that it can be called
// later with only the arguments and without specifying the function name.
//
// Panics if m is not a function.
func WrapCB(parent js.Value, m string) func(args ...any) js.Value {
	if parent.Get(m).Type() != js.TypeFunction {
		// Create the error separate from the print so stack trace is printed
		err := errors.Errorf("Function %q is not of type %s", m, js.TypeFunction)
		jww.FATAL.Panicf("%+v", err)
	}

	return func(args ...any) js.Value { return parent.Call(m, args...) }
}

// PromiseFn converts the Javascript Promise construct into Go.
//
// Call resolve with the return of the function on success. Call reject with an
// error on failure.
type PromiseFn func(resolve, reject func(args ...any) js.Value)

// CreatePromise creates a Javascript promise to return the value of a blocking
// Go function to Javascript.
func CreatePromise(f PromiseFn) any {
	// Create handler for promise (this will be a Javascript function)
	handler := js.FuncOf(func(this js.Value, args []js.Value) any {
		// Spawn a new go routine to perform the blocking function
		go func(resolve, reject js.Value) {
			f(resolve.Invoke, reject.Invoke)
		}(args[0], args[1])

		return nil
	})

	// Create and return the Promise object
	return Promise.New(handler)
}

// Await waits on a Javascript value. It blocks until the awaitable successfully
// resolves to the result or rejects to err.
//
// If there is a result, err will be nil and vice versa.
func Await(awaitable js.Value) (result []js.Value, err []js.Value) {
	then := make(chan []js.Value)
	defer close(then)
	thenFunc := js.FuncOf(func(this js.Value, args []js.Value) any {
		then <- args
		return nil
	})
	defer thenFunc.Release()

	catch := make(chan []js.Value)
	defer close(catch)
	catchFunc := js.FuncOf(func(this js.Value, args []js.Value) any {
		catch <- args
		return nil
	})
	defer catchFunc.Release()

	awaitable.Call("then", thenFunc).Call("catch", catchFunc)

	select {
	case result = <-then:
		return result, nil
	case err = <-catch:
		return nil, err
	}
}