////////////////////////////////////////////////////////////////////////////////
// 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/xxdk-wasm/utils"
)

// TODO: add tests

////////////////////////////////////////////////////////////////////////////////
// Remote Storage Interface and Implementation(s)                             //
////////////////////////////////////////////////////////////////////////////////

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

// newRemoteStoreFileSystemJS creates a new Javascript compatible object
// (map[string]any) that matches the [RemoteStoreFileSystem] structure.
func newRemoteStoreFileSystemJS(api *bindings.RemoteStoreFileSystem) map[string]any {
	rsf := RemoteStoreFileSystem{api}
	rsfMap := map[string]any{
		"Read":            js.FuncOf(rsf.Read),
		"Write":           js.FuncOf(rsf.Write),
		"GetLastModified": js.FuncOf(rsf.GetLastModified),
		"GetLastWrite":    js.FuncOf(rsf.GetLastWrite),
	}

	return rsfMap
}

// NewFileSystemRemoteStorage is a constructor for [RemoteStoreFileSystem].
//
// Parameters:
//   - args[0] - The base directory that all file operations will be performed.
//     It must contain a file delimiter (i.e., `/`) (string).
//
// Returns:
//   - A Javascript representation of the [RemoteStoreFileSystem] object.
func NewFileSystemRemoteStorage(_ js.Value, args []js.Value) any {
	baseDir := args[0].String()
	api := bindings.NewFileSystemRemoteStorage(baseDir)

	return newRemoteStoreFileSystemJS(api)
}

// Read reads from the provided file path and returns the data at that path.
// An error is returned if it failed to read the file.
//
// Parameters:
//   - args[0] - The file path to read from (string).
//
// Returns a promise:
//   - Resolves to the file data (Uint8Array)
//   - Rejected with an error if reading from the file fails.
func (rsf *RemoteStoreFileSystem) Read(_ js.Value, args []js.Value) any {
	path := args[0].String()

	promiseFn := func(resolve, reject func(args ...any) js.Value) {
		data, err := rsf.api.Read(path)
		if err != nil {
			reject(utils.JsTrace(err))
		} else {
			resolve(utils.CopyBytesToJS(data))
		}
	}

	return utils.CreatePromise(promiseFn)
}

// Write writes to the file path the provided data. An error is returned if it
// fails to write to file.
//
// Parameters:
//   - args[0] - The file path to write to (string).
//   - args[1] - The file data to write (Uint8Array).
//
// Returns a promise:
//   - Resolves on success (void).
//   - Rejected with an error if writing to the file fails.
func (rsf *RemoteStoreFileSystem) Write(_ js.Value, args []js.Value) any {
	path := args[0].String()
	data := utils.CopyBytesToGo(args[1])

	promiseFn := func(resolve, reject func(args ...any) js.Value) {
		err := rsf.api.Write(path, data)
		if err != nil {
			reject(utils.JsTrace(err))
		} else {
			resolve()
		}
	}

	return utils.CreatePromise(promiseFn)
}

// GetLastModified returns when the file at the given file path was last
// modified. If the implementation that adheres to this interface does not
// support this, [Write] or [Read] should be implemented to either write a
// separate timestamp file or add a prefix.
//
// Parameters:
//   - args[0] - The file path (string).
//
// Returns a promise:
//   - Resolves to the JSON of [bindings.RemoteStoreReport] (Uint8Array).
//   - Rejected with an error on failure.
func (rsf *RemoteStoreFileSystem) GetLastModified(_ js.Value, args []js.Value) any {
	path := args[0].String()

	promiseFn := func(resolve, reject func(args ...any) js.Value) {
		report, err := rsf.api.GetLastModified(path)
		if err != nil {
			reject(utils.JsTrace(err))
		} else {
			resolve(utils.CopyBytesToJS(report))
		}
	}

	return utils.CreatePromise(promiseFn)
}

// GetLastWrite retrieves the most recent successful write operation that was
// received by [RemoteStoreFileSystem].
//
// Returns a promise:
//   - Resolves to the JSON of [bindings.RemoteStoreReport] (Uint8Array).
//   - Rejected with an error on failure.
func (rsf *RemoteStoreFileSystem) GetLastWrite(js.Value, []js.Value) any {
	promiseFn := func(resolve, reject func(args ...any) js.Value) {
		report, err := rsf.api.GetLastWrite()
		if err != nil {
			reject(utils.JsTrace(err))
		} else {
			resolve(utils.CopyBytesToJS(report))
		}
	}

	return utils.CreatePromise(promiseFn)
}

////////////////////////////////////////////////////////////////////////////////
// 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{
		"Write":   js.FuncOf(rkv.Write),
		"Read":    js.FuncOf(rkv.Read),
		"GetList": js.FuncOf(rkv.GetList),
	}

	return rkvMap
}

// NewOrLoadSyncRemoteKV constructs a [RemoteKV].
//
// Parameters:
//   - args[0] - ID of [E2e] object in tracker (int).
//   - args[1] - A Javascript object that implements the functions on
//     [RemoteKVCallbacks]. These will be the callbacks that are called for
//     [bindings.RemoteStore] operations.
//   - args[2] - A [RemoteStoreCallbacks]. This will be a structure the consumer
//     implements. This acts as a wrapper around the remote storage API
//     (e.g., Google Drive's API, DropBox's API, etc.).
//
// Returns a promise:
//   - Resolves to a Javascript representation of the [RemoteKV] object.
//   - Rejected with an error if initialising the remote KV fails.
func NewOrLoadSyncRemoteKV(_ js.Value, args []js.Value) any {
	e2eID := args[0].Int()
	remoteKvCallbacks := newRemoteKVCallbacks(args[1])
	remote := newRemoteStoreCallbacks(args[2])

	promiseFn := func(resolve, reject func(args ...any) js.Value) {
		api, err :=
			bindings.NewOrLoadSyncRemoteKV(e2eID, remoteKvCallbacks, remote)
		if err != nil {
			reject(utils.JsTrace(err))
		} else {
			resolve(newRemoteKvJS(api))
		}
	}

	return utils.CreatePromise(promiseFn)
}

// Write writes a transaction to the remote and local store.
//
// Parameters:
//   - args[0] - The key that this data will be written to (i.e., the device
//     name, the channel name, etc.). Certain keys should follow a pattern and
//     contain special characters (see [RemoteKV.GetList] for details) (string).
//   - args[1] - The data that will be stored (i.e., state data) (Uint8Array).
//   - args[2] - A Javascript object that implements the functions on
//     [RemoteKVCallbacks]. This may be nil if you do not care about the network
//     report.
//
// Returns a promise:
//   - Resolves on success (void).
//   - Rejected with an error if writing to the file fails.
func (rkv *RemoteKV) Write(_ js.Value, args []js.Value) any {
	path := args[0].String()
	data := utils.CopyBytesToGo(args[1])
	cb := newRemoteKVCallbacks(args[1])

	promiseFn := func(resolve, reject func(args ...any) js.Value) {
		err := rkv.api.Write(path, data, cb)
		if err != nil {
			reject(utils.JsTrace(err))
		} else {
			resolve()
		}
	}

	return utils.CreatePromise(promiseFn)
}

// Read retrieves the data stored in the underlying KV. Returns an error if the
// data at this key cannot be retrieved.
//
// Parameters:
//   - args[0] - The path that this data will be written to (i.e., the device
//     name) (string).
//
// Returns a promise:
//   - Resolves to the file data (Uint8Array)
//   - Rejected with an error if reading from the file fails.
func (rkv *RemoteKV) Read(_ js.Value, args []js.Value) any {
	path := args[0].String()

	promiseFn := func(resolve, reject func(args ...any) js.Value) {
		data, err := rkv.api.Read(path)
		if err != nil {
			reject(utils.JsTrace(err))
		} else {
			resolve(utils.CopyBytesToJS(data))
		}
	}

	return utils.CreatePromise(promiseFn)
}

// GetList returns all entries for a path (or key) that contain the name
// parameter from the local store.
//
// For example, assuming the usage of the [sync.LocalStoreKeyDelimiter], if both
// "channels-123" and "channels-abc" are written to [RemoteKV], then
// GetList("channels") will retrieve the data for both channels. All data that
// contains no [sync.LocalStoreKeyDelimiter] can be retrieved using GetList("").
//
// Parameters:
//   - args[0] - Some prefix to a Write operation. If no prefix applies, simply
//     use the empty string. (string).
//
// Returns:
//   - The file data (Uint8Array)
//   - Throws a TypeError if getting the list fails.
func (rkv *RemoteKV) GetList(_ js.Value, args []js.Value) any {
	name := args[0].String()

	data, err := rkv.api.GetList(name)
	if err != nil {
		utils.Throw(utils.TypeError, err)
		return nil
	}

	return utils.CopyBytesToJS(data)
}

// RemoteStoreCallbacks wraps Javascript callbacks to adhere to the
// [bindings.RemoteStore] interface.
type RemoteStoreCallbacks 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
}

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

// Read reads from the provided file path and returns the data at that path.
// An error is returned if it failed to read the file.
//
// 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 *RemoteStoreCallbacks) Read(path string) ([]byte, error) {

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

// Write writes to the file path the provided data. An error is returned if it
// fails to write to file.
//
// 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 *RemoteStoreCallbacks) Write(path string, data []byte) error {
	fn := func() js.Value { return rsCB.write(path, utils.CopyBytesToJS(data)) }
	_, err := utils.RunAndCatch(fn)
	return err
}

// GetLastModified returns when the file at the given file path was last
// modified. If the implementation that adheres to this interface does not
// support this, [Write] or [Read] should be implemented to either write a
// separate timestamp file or add a prefix.
//
// 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 *RemoteStoreCallbacks) GetLastModified(path string) ([]byte, error) {
	fn := func() js.Value { return rsCB.getLastModified(path) }
	v, err := utils.RunAndCatch(fn)
	if err != nil {
		return nil, err
	}
	return utils.CopyBytesToGo(v), err
}

// GetLastWrite retrieves the most recent successful write operation that was
// received by [RemoteStoreFileSystem].
//
// Returns:
//   - JSON of [bindings.RemoteStoreReport] (Uint8Array).
//   - Catches any thrown errors (of type Error) and returns it as an error.
func (rsCB *RemoteStoreCallbacks) GetLastWrite() ([]byte, error) {
	fn := func() js.Value { return rsCB.getLastWrite() }
	v, err := utils.RunAndCatch(fn)
	if err != nil {
		return nil, err
	}
	return utils.CopyBytesToGo(v), err
}

// RemoteKVCallbacks wraps Javascript callbacks to adhere to the
// [bindings.RemoteKVCallbacks] interface.
type RemoteKVCallbacks struct {
	keyUpdated        func(args ...any) js.Value
	remoteStoreResult func(args ...any) js.Value
}

// newRemoteKVCallbacks maps the functions of the Javascript object matching
// [bindings.RemoteKVCallbacks] to a RemoteKVCallbacks.
func newRemoteKVCallbacks(arg js.Value) *RemoteKVCallbacks {
	return &RemoteKVCallbacks{
		keyUpdated:        utils.WrapCB(arg, "KeyUpdated"),
		remoteStoreResult: utils.WrapCB(arg, "RemoteStoreResult"),
	}
}

// KeyUpdated is the callback to be called any time a key is updated by another
// device tracked by the [RemoteKV] store.
//
// Parameters:
//   - key - (string).
//   - oldVal - (Uint8Array).
//   - newVal - (Uint8Array).
//   - updated - (Boolean)
func (rkvCB *RemoteKVCallbacks) KeyUpdated(
	key string, oldVal, newVal []byte, updated bool) {
	rkvCB.keyUpdated(
		key, utils.CopyBytesToJS(oldVal), utils.CopyBytesToJS(newVal), updated)
}

// RemoteStoreResult is called to report network save results after the key has
// been updated locally.
//
// NOTE: Errors originate from the authentication and writing code in regard to
// remote which is handled by the user of this API. As a result, this callback
// provides no information in simple implementations.
//
// Parameters:
//   - remoteStoreReport - JSON of [bindings.RemoteStoreReport] (Uint8Array).
func (rkvCB *RemoteKVCallbacks) RemoteStoreResult(remoteStoreReport []byte) {
	rkvCB.remoteStoreResult(utils.CopyBytesToJS(remoteStoreReport))
}