////////////////////////////////////////////////////////////////////////////////
// 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 wasm

import (
	"syscall/js"

	"gitlab.com/elixxir/client/v4/bindings"
	"gitlab.com/elixxir/wasm-utils/exception"
	"gitlab.com/elixxir/wasm-utils/utils"
)

////////////////////////////////////////////////////////////////////////////////
// RemoteKV Methods                                                           //
////////////////////////////////////////////////////////////////////////////////

// RemoteKV wraps the [bindings.RemoteKV] object so its methods can be wrapped
// to be Javascript compatible.
type RemoteKV struct {
	api *bindings.RemoteKV
}

// newRemoteKvJS creates a new Javascript compatible object (map[string]any)
// that matches the [RemoteKV] structure.
func newRemoteKvJS(api *bindings.RemoteKV) map[string]any {
	rkv := RemoteKV{api}
	rkvMap := map[string]any{
		"Get":               js.FuncOf(rkv.Get),
		"Delete":            js.FuncOf(rkv.Delete),
		"Set":               js.FuncOf(rkv.Set),
		"GetPrefix":         js.FuncOf(rkv.GetPrefix),
		"HasPrefix":         js.FuncOf(rkv.HasPrefix),
		"Prefix":            js.FuncOf(rkv.Prefix),
		"Root":              js.FuncOf(rkv.Root),
		"IsMemStore":        js.FuncOf(rkv.IsMemStore),
		"GetFullKey":        js.FuncOf(rkv.GetFullKey),
		"StoreMapElement":   js.FuncOf(rkv.StoreMapElement),
		"StoreMap":          js.FuncOf(rkv.StoreMap),
		"DeleteMapElement":  js.FuncOf(rkv.DeleteMapElement),
		"GetMap":            js.FuncOf(rkv.GetMap),
		"GetMapElement":     js.FuncOf(rkv.GetMapElement),
		"ListenOnRemoteKey": js.FuncOf(rkv.ListenOnRemoteKey),
		"ListenOnRemoteMap": js.FuncOf(rkv.ListenOnRemoteMap),
	}

	return rkvMap
}

// Get returns the object stored at the specified version.
// returns a json of [versioned.Object].
//
// Parameters:
//   - args[0] - key to access, a string
//   - args[1] - version, an integer
//
// Returns a promise:
//   - Resolves to JSON of a [versioned.Object], e.g.:
//     {"Version":1,"Timestamp":"2023-05-13T00:50:03.889192694Z","Data":"bm90IHVwZ3JhZGVk"}
//   - Rejected with an access error. Note: File does not exist errors
//     are returned whent key is not set.
func (r *RemoteKV) Get(_ js.Value, args []js.Value) any {
	key := args[0].String()
	version := int64(args[1].Int())

	promiseFn := func(resolve, reject func(args ...any) js.Value) {
		value, err := r.api.Get(key, version)
		if err != nil {
			reject(exception.NewTrace(err))
		} else {
			resolve(utils.CopyBytesToJS(value))
		}
	}

	return utils.CreatePromise(promiseFn)
}

// Delete removes a given key from the data store.
//
// Parameters:
//   - args[0] - key to access, a string
//   - args[1] - version, an integer
//
// Returns a promise:
//   - Rejected with an access error. Note: File does not exist errors
//     are returned whent key is not set.
func (r *RemoteKV) Delete(_ js.Value, args []js.Value) any {
	key := args[0].String()
	version := int64(args[1].Int())

	promiseFn := func(resolve, reject func(args ...any) js.Value) {
		err := r.api.Delete(key, version)
		if err != nil {
			reject(exception.NewTrace(err))
		} else {
			resolve()
		}
	}

	return utils.CreatePromise(promiseFn)
}

// Set upserts new data into the storage
// When calling this, you are responsible for prefixing the
// key with the correct type optionally unique id! Call
// GetFullKey() to do so.
// The [Object] should contain the versioning if you are
// maintaining such a functionality.
//
// Parameters:
//   - args[0] - the key string
//   - args[1] - the [versioned.Object] JSON value, e.g.:
//     {"Version":1,"Timestamp":"2023-05-13T00:50:03.889192694Z",
//     "Data":"bm90IHVwZ3JhZGVk"}
//
// Returns a promise:
//   - Rejected with an access error.
func (r *RemoteKV) Set(_ js.Value, args []js.Value) any {
	key := args[0].String()
	value := utils.CopyBytesToGo(args[1])

	promiseFn := func(resolve, reject func(args ...any) js.Value) {
		err := r.api.Set(key, value)
		if err != nil {
			reject(exception.NewTrace(err))
		} else {
			resolve()
		}
	}

	return utils.CreatePromise(promiseFn)
}

// GetPrefix returns the full Prefix of the KV
// Returns a string via a Promise
func (r *RemoteKV) GetPrefix(_ js.Value, args []js.Value) any {
	promiseFn := func(resolve, reject func(args ...any) js.Value) {
		prefix := r.api.GetPrefix()
		resolve(prefix)
	}

	return utils.CreatePromise(promiseFn)
}

// HasPrefix returns whether this prefix exists in the KV
//
// Parameters:
//   - args[0] - the prefix string to check for.
//
// Returns a bool via a promise.
func (r *RemoteKV) HasPrefix(_ js.Value, args []js.Value) any {
	prefix := args[0].String()
	promiseFn := func(resolve, reject func(args ...any) js.Value) {
		resolve(r.api.HasPrefix(prefix))
	}

	return utils.CreatePromise(promiseFn)
}

// Prefix returns a new KV with the new prefix appending
//
// Parameters:
//   - args[0] - the prefix to append to the list of prefixes
//
// Returns a promise:
//   - Resolves to a new RemoteKV
//   - Rejected with an error.
func (r *RemoteKV) Prefix(_ js.Value, args []js.Value) any {
	prefix := args[0].String()

	promiseFn := func(resolve, reject func(args ...any) js.Value) {
		newAPI, err := r.api.Prefix(prefix)

		if err != nil {
			reject(exception.NewTrace(err))
		} else {
			resolve(newRemoteKvJS(newAPI))
		}
	}

	return utils.CreatePromise(promiseFn)
}

// Root returns the KV with no prefixes
func (r *RemoteKV) Root(_ js.Value, args []js.Value) any {
	promiseFn := func(resolve, reject func(args ...any) js.Value) {
		newAPI, err := r.api.Root()

		if err != nil {
			reject(exception.NewTrace(err))
		} else {
			resolve(newRemoteKvJS(newAPI))
		}
	}

	return utils.CreatePromise(promiseFn)
}

// IsMemStore returns true if the underlying KV is memory based
func (r *RemoteKV) IsMemStore(_ js.Value, args []js.Value) any {
	promiseFn := func(resolve, reject func(args ...any) js.Value) {
		resolve(r.api.IsMemStore())
	}

	return utils.CreatePromise(promiseFn)
}

// GetFullKey returns the key with all prefixes appended
func (r *RemoteKV) GetFullKey(_ js.Value, args []js.Value) any {
	key := args[0].String()
	version := int64(args[1].Int())

	promiseFn := func(resolve, reject func(args ...any) js.Value) {
		fullKey := r.api.GetFullKey(key, version)
		resolve(fullKey)
	}

	return utils.CreatePromise(promiseFn)
}

// StoreMapElement stores a versioned map element into the KV. This relies
// on the underlying remote [KV.StoreMapElement] function to lock and control
// updates, but it uses [versioned.Object] values.
// All Map storage functions update the remote.
// valueJSON is a json of a versioned.Object
//
// Parameters:
//   - args[0] - the mapName string
//   - args[1] - the elementKey string
//   - args[2] - the [versioned.Object] JSON value, e.g.:
//     {"Version":1,"Timestamp":"2023-05-13T00:50:03.889192694Z",
//     "Data":"bm90IHVwZ3JhZGVk"}
//   - args[3] - the version int
//
// Returns a promise with an error if any
func (r *RemoteKV) StoreMapElement(_ js.Value, args []js.Value) any {
	mapName := args[0].String()
	elementKey := args[1].String()
	val := utils.CopyBytesToGo(args[2])
	version := int64(args[3].Int())

	promiseFn := func(resolve, reject func(args ...any) js.Value) {
		err := r.api.StoreMapElement(mapName, elementKey, val, version)
		if err != nil {
			reject(exception.NewTrace(err))
		} else {
			resolve()
		}
	}

	return utils.CreatePromise(promiseFn)
}

// StoreMap saves a versioned map element into the KV. This relies
// on the underlying remote [KV.StoreMap] function to lock and control
// updates, but it uses [versioned.Object] values.
// All Map storage functions update the remote.
// valueJSON is a json of map[string]*versioned.Object
//
// Parameters:
//   - args[0] - the mapName string
//   - args[1] - the [map[string]versioned.Object] JSON value, e.g.:
//     {"elementKey": {"Version":1,"Timestamp":"2023-05-13T00:50:03.889192694Z",
//     "Data":"bm90IHVwZ3JhZGVk"}}
//   - args[2] - the version int
//
// Returns a promise with an error if any
func (r *RemoteKV) StoreMap(_ js.Value, args []js.Value) any {
	mapName := args[0].String()
	val := utils.CopyBytesToGo(args[1])
	version := int64(args[2].Int())

	promiseFn := func(resolve, reject func(args ...any) js.Value) {
		err := r.api.StoreMap(mapName, val, version)
		if err != nil {
			reject(exception.NewTrace(err))
		} else {
			resolve()
		}
	}

	return utils.CreatePromise(promiseFn)
}

// DeleteMapElement removes a versioned map element from the KV.
//
// Parameters:
//   - args[0] - the mapName string
//   - args[1] - the elementKey string
//   - args[2] - the version int
//
// Returns a promise with an error if any or the json of the deleted
// [versioned.Object], e.g.:
//
//	{"Version":1,"Timestamp":"2023-05-13T00:50:03.889192694Z",
//	"Data":"bm90IHVwZ3JhZGVk"}
func (r *RemoteKV) DeleteMapElement(_ js.Value, args []js.Value) any {
	mapName := args[0].String()
	elementKey := args[1].String()
	version := int64(args[2].Int())

	promiseFn := func(resolve, reject func(args ...any) js.Value) {
		deleted, err := r.api.DeleteMapElement(mapName, elementKey,
			version)
		if err != nil {
			reject(exception.NewTrace(err))
		} else {
			resolve(utils.CopyBytesToJS(deleted))
		}
	}

	return utils.CreatePromise(promiseFn)
}

// GetMap loads a versioned map from the KV. This relies
// on the underlying remote [KV.GetMap] function to lock and control
// updates, but it uses [versioned.Object] values.
//
// Parameters:
//   - args[0] - the mapName string
//   - args[1] - the version int
//
// Returns a promise with an error if any or the
// the [map[string]versioned.Object] JSON value, e.g.:
//
//	{"elementKey": {"Version":1,"Timestamp":"2023-05-13T00:50:03.889192694Z",
//	"Data":"bm90IHVwZ3JhZGVk"}}
func (r *RemoteKV) GetMap(_ js.Value, args []js.Value) any {
	mapName := args[0].String()
	version := int64(args[1].Int())

	promiseFn := func(resolve, reject func(args ...any) js.Value) {
		mapJSON, err := r.api.GetMap(mapName, version)
		if err != nil {
			reject(exception.NewTrace(err))
		} else {
			resolve(utils.CopyBytesToJS(mapJSON))
		}
	}

	return utils.CreatePromise(promiseFn)
}

// GetMapElement loads a versioned map element from the KV. This relies
// on the underlying remote [KV.GetMapElement] function to lock and control
// updates, but it uses [versioned.Object] values.
// Parameters:
//   - args[0] - the mapName string
//   - args[1] - the elementKey string
//   - args[2] - the version int
//
// Returns a promise with an error if any or the json of the
// [versioned.Object], e.g.:
//
//	{"Version":1,"Timestamp":"2023-05-13T00:50:03.889192694Z",
//	"Data":"bm90IHVwZ3JhZGVk"}
func (r *RemoteKV) GetMapElement(_ js.Value, args []js.Value) any {
	mapName := args[0].String()
	elementKey := args[1].String()
	version := int64(args[2].Int())

	promiseFn := func(resolve, reject func(args ...any) js.Value) {
		deleted, err := r.api.GetMapElement(mapName, elementKey,
			version)
		if err != nil {
			reject(exception.NewTrace(err))
		} else {
			resolve(utils.CopyBytesToJS(deleted))
		}
	}

	return utils.CreatePromise(promiseFn)
}

// ListenOnRemoteKey sets up a callback listener for the object specified
// by the key and version. It returns the current [versioned.Object] JSON
// of the value.
// Parameters:
//   - args[0] - the key string
//   - args[1] - the version int
//   - args[2] - the [KeyChangedByRemoteCallback] javascript callback
//
// Returns a promise with an error if any or the json of the existing
// [versioned.Object], e.g.:
//
//	{"Version":1,"Timestamp":"2023-05-13T00:50:03.889192694Z",
//	"Data":"bm90IHVwZ3JhZGVk"}
func (r *RemoteKV) ListenOnRemoteKey(_ js.Value, args []js.Value) any {
	key := args[0].String()
	version := int64(args[1].Int())
	cb := newKeyChangedByRemoteCallback(args[2])

	promiseFn := func(resolve, reject func(args ...any) js.Value) {
		deleted, err := r.api.ListenOnRemoteKey(key, version, cb)
		if err != nil {
			reject(exception.NewTrace(err))
		} else {
			resolve(utils.CopyBytesToJS(deleted))
		}
	}

	return utils.CreatePromise(promiseFn)
}

// ListenOnRemoteMap allows the caller to receive updates when
// the map or map elements are updated. Returns a JSON of
// map[string]versioned.Object of the current map value.
// Parameters:
//   - args[0] - the mapName string
//   - args[1] - the version int
//   - args[2] - the [MapChangedByRemoteCallback] javascript callback
//
// Returns a promise with an error if any or the json of the existing
// the [map[string]versioned.Object] JSON value, e.g.:
//
//	{"elementKey": {"Version":1,"Timestamp":"2023-05-13T00:50:03.889192694Z",
//	"Data":"bm90IHVwZ3JhZGVk"}}
func (r *RemoteKV) ListenOnRemoteMap(_ js.Value, args []js.Value) any {
	mapName := args[0].String()
	version := int64(args[1].Int())
	cb := newMapChangedByRemoteCallback(args[2])

	promiseFn := func(resolve, reject func(args ...any) js.Value) {
		deleted, err := r.api.ListenOnRemoteMap(mapName, version, cb)
		if err != nil {
			reject(exception.NewTrace(err))
		} else {
			resolve(utils.CopyBytesToJS(deleted))
		}
	}

	return utils.CreatePromise(promiseFn)
}

////////////////////////////////////////////////////////////////////////////////
// RemoteStore                                                                //
////////////////////////////////////////////////////////////////////////////////

// RemoteStore wraps Javascript callbacks to adhere to the
// [bindings.RemoteStore] interface.
type RemoteStore struct {
	read            func(args ...any) js.Value
	write           func(args ...any) js.Value
	getLastModified func(args ...any) js.Value
	getLastWrite    func(args ...any) js.Value
	readDir         func(args ...any) js.Value
}

// newRemoteStoreCallbacks maps the functions of the Javascript object matching
// [bindings.RemoteStore] to a RemoteStoreCallbacks.
func newRemoteStore(arg js.Value) *RemoteStore {
	return &RemoteStore{
		read:            utils.WrapCB(arg, "Read"),
		write:           utils.WrapCB(arg, "Write"),
		getLastModified: utils.WrapCB(arg, "GetLastModified"),
		getLastWrite:    utils.WrapCB(arg, "GetLastWrite"),
		readDir:         utils.WrapCB(arg, "ReadDir"),
	}
}

// Read impelements [bindings.RemoteStore.Read]
//
// Parameters:
//   - path - The file path to read from (string).
//
// Returns:
//   - The file data (Uint8Array).
//   - Catches any thrown errors (of type Error) and returns it as an error.
func (rsCB *RemoteStore) Read(path string) ([]byte, error) {

	fn := func() js.Value { return rsCB.read(path) }
	v, err := exception.RunAndCatch(fn)
	if err != nil {
		return nil, err
	}
	return utils.CopyBytesToGo(v), err
}

// Write implements [bindings.RemoteStore.Write]
//
// Parameters:
//   - path - The file path to write to (string).
//   - data - The file data to write (Uint8Array).
//
// Returns:
//   - Catches any thrown errors (of type Error) and returns it as an error.
func (rsCB *RemoteStore) Write(path string, data []byte) error {
	fn := func() js.Value { return rsCB.write(path, utils.CopyBytesToJS(data)) }
	_, err := exception.RunAndCatch(fn)
	return err
}

// GetLastModified implements [bindings.RemoteStore.GetLastModified]
//
// Parameters:
//   - path - The file path (string).
//
// Returns:
//   - JSON of [bindings.RemoteStoreReport] (Uint8Array).
//   - Catches any thrown errors (of type Error) and returns it as an error.
func (rsCB *RemoteStore) GetLastModified(path string) ([]byte, error) {
	fn := func() js.Value { return rsCB.getLastModified(path) }
	v, err := exception.RunAndCatch(fn)
	if err != nil {
		return nil, err
	}
	return utils.CopyBytesToGo(v), err
}

// GetLastWrite implements [bindings.RemoteStore.GetLastWrite()
//
// Returns:
//   - JSON of [bindings.RemoteStoreReport] (Uint8Array).
//   - Catches any thrown errors (of type Error) and returns it as an error.
func (rsCB *RemoteStore) GetLastWrite() ([]byte, error) {
	fn := func() js.Value { return rsCB.getLastWrite() }
	v, err := exception.RunAndCatch(fn)
	if err != nil {
		return nil, err
	}
	return utils.CopyBytesToGo(v), err
}

// ReadDir implements [bindings.RemoteStore.ReadDir]
//
// Parameters:
//   - path - The file path (string).
//
// Returns:
//   - JSON of []string (Uint8Array).
//   - Catches any thrown errors (of type Error) and returns it as an error.
func (rsCB *RemoteStore) ReadDir(path string) ([]byte, error) {
	fn := func() js.Value { return rsCB.readDir(path) }
	v, err := exception.RunAndCatch(fn)
	if err != nil {
		return nil, err
	}
	return utils.CopyBytesToGo(v), err
}

////////////////////////////////////////////////////////////////////////////////
// Callbacks                                                                  //
////////////////////////////////////////////////////////////////////////////////

// KeyChangedByRemoteCallback wraps the passed javascript function and
// implements [bindings.KeyChangedByRemoteCallback]
type KeyChangedByRemoteCallback struct {
	callback func(args ...any) js.Value
}

func (k *KeyChangedByRemoteCallback) Callback(key string, old, new []byte,
	opType int8) {
	k.callback(key, utils.CopyBytesToJS(old), utils.CopyBytesToJS(new),
		opType)
}

func newKeyChangedByRemoteCallback(
	jsFunc js.Value) *KeyChangedByRemoteCallback {
	return &KeyChangedByRemoteCallback{
		callback: utils.WrapCB(jsFunc, "Callback"),
	}
}

// MapChangedByRemoteCallback wraps the passed javascript function and
// implements [bindings.KeyChangedByRemoteCallback]
type MapChangedByRemoteCallback struct {
	callback func(args ...any) js.Value
}

func (m *MapChangedByRemoteCallback) Callback(mapName string,
	editsJSON []byte) {
	m.callback(mapName, utils.CopyBytesToJS(editsJSON))
}

func newMapChangedByRemoteCallback(
	jsFunc js.Value) *MapChangedByRemoteCallback {
	return &MapChangedByRemoteCallback{
		callback: utils.WrapCB(jsFunc, "Callback"),
	}
}