////////////////////////////////////////////////////////////////////////////////
// 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"
)

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

// newChannelsFileTransferJS creates a new Javascript compatible object
// (map[string]any) that matches the [ChannelsFileTransfer] structure.
func newChannelsFileTransferJS(api *bindings.ChannelsFileTransfer) map[string]any {
	cft := ChannelsFileTransfer{api}
	channelsFileTransferMap := map[string]any{
		"GetExtensionBuilderID": js.FuncOf(cft.GetExtensionBuilderID),
		"MaxFileNameLen":        js.FuncOf(cft.MaxFileNameLen),
		"MaxFileTypeLen":        js.FuncOf(cft.MaxFileTypeLen),
		"MaxFileSize":           js.FuncOf(cft.MaxFileSize),
		"MaxPreviewSize":        js.FuncOf(cft.MaxPreviewSize),

		// Uploading/Sending
		"Upload":                       js.FuncOf(cft.Upload),
		"Send":                         js.FuncOf(cft.Send),
		"RegisterSentProgressCallback": js.FuncOf(cft.RegisterSentProgressCallback),
		"RetryUpload":                  js.FuncOf(cft.RetryUpload),
		"CloseSend":                    js.FuncOf(cft.CloseSend),

		// Downloading
		"Download":                         js.FuncOf(cft.Download),
		"RegisterReceivedProgressCallback": js.FuncOf(cft.RegisterReceivedProgressCallback),
	}

	return channelsFileTransferMap
}

// InitChannelsFileTransfer creates a file transfer manager for channels.
//
// Parameters:
//   - args[0] - ID of [E2e] object in tracker (int).
//   - args[1] - JSON of [channelsFileTransfer.Params] (Uint8Array).
//
// Returns:
//   - New [ChannelsFileTransfer] object.
//
// Returns a promise:
//   - Resolves to a Javascript representation of the [ChannelsFileTransfer]
//     object.
//   - Rejected with an error if creating the file transfer object fails.
func InitChannelsFileTransfer(_ js.Value, args []js.Value) any {
	e2eID := args[0].Int()
	paramsJson := utils.CopyBytesToGo(args[1])

	promiseFn := func(resolve, reject func(args ...any) js.Value) {
		cft, err := bindings.InitChannelsFileTransfer(e2eID, paramsJson)
		if err != nil {
			reject(exception.NewTrace(err))
		} else {
			resolve(newChannelsFileTransferJS(cft))
		}
	}

	return utils.CreatePromise(promiseFn)
}

// GetExtensionBuilderID returns the ID of the extension builder in the tracker.
// Pass this ID into the channel manager creator to use file transfer manager in
// conjunction with channels.
//
// Returns:
//   - Extension builder ID (int).
func (cft *ChannelsFileTransfer) GetExtensionBuilderID(js.Value, []js.Value) any {
	return cft.api.GetExtensionBuilderID()
}

// MaxFileNameLen returns the max number of bytes allowed for a file name.
//
// Returns:
//   - Max number of bytes (int).
func (cft *ChannelsFileTransfer) MaxFileNameLen(js.Value, []js.Value) any {
	return cft.api.MaxFileNameLen()
}

// MaxFileTypeLen returns the max number of bytes allowed for a file type.
//
// Returns:
//   - Max number of bytes (int).
func (cft *ChannelsFileTransfer) MaxFileTypeLen(js.Value, []js.Value) any {
	return cft.api.MaxFileNameLen()
}

// MaxFileSize returns the max number of bytes allowed for a file.
//
// Returns:
//   - Max number of bytes (int).
func (cft *ChannelsFileTransfer) MaxFileSize(js.Value, []js.Value) any {
	return cft.api.MaxFileSize()
}

// MaxPreviewSize returns the max number of bytes allowed for a file preview.
//
// Returns:
//   - Max number of bytes (int).
func (cft *ChannelsFileTransfer) MaxPreviewSize(js.Value, []js.Value) any {
	return cft.api.MaxFileSize()
}

////////////////////////////////////////////////////////////////////////////////
// Uploading/Sending                                                          //
////////////////////////////////////////////////////////////////////////////////

// Upload starts uploading the file to a new ID that can be sent to the
// specified channel when complete. To get progress information about the
// upload, a [bindings.FtSentProgressCallback] must be registered. All errors
// returned on the callback are fatal and the user must take action to either
// [ChannelsFileTransfer.RetryUpload] or [ChannelsFileTransfer.CloseSend].
//
// The file is added to the event model at the returned file ID with the status
// [channelsFileTransfer.Uploading]. Once the upload is complete, the file link
// is added to the event model with the status [channelsFileTransfer.Complete].
//
// The [bindings.FtSentProgressCallback] only indicates the progress of the file
// upload, not the status of the file in the event model. You must rely on
// updates from the event model to know when it can be retrieved.
//
// Parameters:
//   - args[0] - File contents. Max size defined by
//     [ChannelsFileTransfer.MaxFileSize] (Uint8Array).
//   - args[1] - The number of sending retries allowed on send failure (e.g. a
//     retry of 2.0 with 6 parts means 12 total possible sends) (float).
//   - args[2] - The progress callback, which is a callback that reports the
//     progress of the file upload. The callback is called once on
//     initialization, on every progress update (or less if restricted by the
//     period), or on fatal error. It must be a Javascript object that
//     implements the [bindings.FtSentProgressCallback] interface.
//   - args[3] - Progress callback period. A progress callback will be limited
//     from triggering only once per period, in milliseconds (int).
//
// Returns a promise:
//   - Resolves to the marshalled bytes of [fileTransfer.ID] that uniquely
//     identifies the file (Uint8Array).
//   - Rejected with an error if initiating the upload fails.
func (cft *ChannelsFileTransfer) Upload(_ js.Value, args []js.Value) any {
	var (
		fileData   = utils.CopyBytesToGo(args[0])
		retry      = float32(args[1].Float())
		progressCB = &ftSentCallback{utils.WrapCB(args[2], "Callback")}
		period     = args[3].Int()
	)

	promiseFn := func(resolve, reject func(args ...any) js.Value) {
		fileID, err := cft.api.Upload(fileData, retry, progressCB, period)
		if err != nil {
			reject(exception.NewTrace(err))
		} else {
			resolve(utils.CopyBytesToJS(fileID))
		}
	}

	return utils.CreatePromise(promiseFn)
}

// Send sends the specified file info to the channel. Once a file is uploaded
// via [ChannelsFileTransfer.Upload], its file info (found in the event model)
// can be sent to any channel.
//
// Parameters:
//   - args[0] - Marshalled bytes of the channel's [id.ID] to send the file to
//     (Uint8Array).
//   - args[1] - JSON of [channelsFileTransfer.FileLink] stored in the event
//     model (Uint8Array).
//   - args[2] - Human-readable file name. Max length defined by
//     [ChannelsFileTransfer.MaxFileNameLen] (string).
//   - args[3] - Shorthand that identifies the type of file. Max length defined
//     by [ChannelsFileTransfer.MaxFileTypeLen] (string).
//   - args[4] - A preview of the file data (e.g. a thumbnail). Max size defined
//     by [ChannelsFileTransfer.MaxPreviewSize] (Uint8Array).
//   - args[5] - The duration, in milliseconds, that the file is available in
//     the channel (int). For the maximum amount of time, use [ValidForever].
//   - args[6] - JSON of [xxdk.CMIXParams] (Uint8Array). If left empty,
//     [GetDefaultCMixParams] will be used internally.
//   - args[7] - JSON of a slice of public keys of users that should receive
//     mobile notifications for the message.
//
// Returns a promise:
//   - Resolves to the JSON of [bindings.ChannelSendReport] (Uint8Array).
//   - Rejected with an error if sending fails.
func (cft *ChannelsFileTransfer) Send(_ js.Value, args []js.Value) any {
	var (
		channelIdBytes = utils.CopyBytesToGo(args[0])
		fileLinkJSON   = utils.CopyBytesToGo(args[1])
		fileName       = args[2].String()
		fileType       = args[3].String()
		preview        = utils.CopyBytesToGo(args[4])
		validUntilMS   = args[5].Int()
		cmixParamsJSON = utils.CopyBytesToGo(args[6])
		pingsJSON      = utils.CopyBytesToGo(args[7])
	)

	promiseFn := func(resolve, reject func(args ...any) js.Value) {
		fileID, err := cft.api.Send(channelIdBytes, fileLinkJSON,
			fileName, fileType, preview, validUntilMS,
			cmixParamsJSON, pingsJSON)
		if err != nil {
			reject(exception.NewTrace(err))
		} else {
			resolve(utils.CopyBytesToJS(fileID))
		}
	}

	return utils.CreatePromise(promiseFn)
}

// RegisterSentProgressCallback allows for the registration of a callback to
// track the progress of an individual file upload. A
// [bindings.FtSentProgressCallback] is auto-registered on
// [ChannelsFileTransfer.Send]; this function should be called when resuming
// clients or registering extra callbacks.
//
// The callback will be called immediately when added to report the current
// progress of the transfer. It will then call every time a file part arrives,
// the transfer completes, or a fatal error occurs. It is called at most once
// every period regardless of the number of progress updates.
//
// In the event that the client is closed and resumed, this function must be
// used to re-register any callbacks previously registered with this function or
// [ChannelsFileTransfer.Send].
//
// The [bindings.FtSentProgressCallback] only indicates the progress of the file
// upload, not the status of the file in the event model. You must rely on
// updates from the event model to know when it can be retrieved.
//
// Parameters:
//   - args[0] - Marshalled bytes of the file's [fileTransfer.ID] (Uint8Array).
//   - args[1] - The progress callback, which is a callback that reports the
//     progress of the file upload. The callback is called once on
//     initialization, on every progress update (or less if restricted by the
//     period), or on fatal error. It must be a Javascript object that
//     implements the [bindings.FtSentProgressCallback] interface.
//   - args[2] - Progress callback period. A progress callback will be limited
//     from triggering only once per period, in milliseconds (int).
//
// Returns a promise:
//   - Resolves on success (void).
//   - Rejected with an error if registering the callback fails.
func (cft *ChannelsFileTransfer) RegisterSentProgressCallback(
	_ js.Value, args []js.Value) any {
	var (
		fileIDBytes = utils.CopyBytesToGo(args[0])
		progressCB  = &ftSentCallback{utils.WrapCB(args[1], "Callback")}
		periodMS    = args[2].Int()
	)

	promiseFn := func(resolve, reject func(args ...any) js.Value) {
		err := cft.api.RegisterSentProgressCallback(
			fileIDBytes, progressCB, periodMS)
		if err != nil {
			reject(exception.NewTrace(err))
		} else {
			resolve()
		}
	}

	return utils.CreatePromise(promiseFn)
}

// RetryUpload retries uploading a failed file upload. Returns an error if the
// transfer has not failed.
//
// This function should be called once a transfer errors out (as reported by the
// progress callback).
//
// A new progress callback must be registered on retry. Any previously
// registered callbacks are defunct when the upload fails.
//
// Parameters:
//   - args[0] - Marshalled bytes of the file's [fileTransfer.ID] (Uint8Array).
//   - args[1] - The progress callback, which is a callback that reports the
//     progress of the file upload. The callback is called once on
//     initialization, on every progress update (or less if restricted by the
//     period), or on fatal error. It must be a Javascript object that
//     implements the [bindings.FtSentProgressCallback] interface.
//   - args[2] - Progress callback period. A progress callback will be limited
//     from triggering only once per period, in milliseconds (int).
//
// Returns a promise:
//   - Resolves on success (void).
//   - Rejected with an error if registering retrying the upload fails.
func (cft *ChannelsFileTransfer) RetryUpload(_ js.Value, args []js.Value) any {
	var (
		fileIDBytes = utils.CopyBytesToGo(args[0])
		progressCB  = &ftSentCallback{utils.WrapCB(args[1], "Callback")}
		periodMS    = args[2].Int()
	)

	promiseFn := func(resolve, reject func(args ...any) js.Value) {
		err := cft.api.RetryUpload(fileIDBytes, progressCB, periodMS)
		if err != nil {
			reject(exception.NewTrace(err))
		} else {
			resolve()
		}
	}

	return utils.CreatePromise(promiseFn)
}

// CloseSend deletes a file from the internal storage once a transfer has
// completed or reached the retry limit. If neither of those condition are met,
// an error is returned.
//
// This function should be called once a transfer completes or errors out (as
// reported by the progress callback).
//
// Parameters:
//   - args[0] - Marshalled bytes of the file's [fileTransfer.ID] (Uint8Array).
//
// Returns a promise:
//   - Resolves on success (void).
//   - Rejected with an error if the file has not failed or completed or if
//     closing failed.
func (cft *ChannelsFileTransfer) CloseSend(_ js.Value, args []js.Value) any {
	fileIDBytes := utils.CopyBytesToGo(args[0])

	promiseFn := func(resolve, reject func(args ...any) js.Value) {
		err := cft.api.CloseSend(fileIDBytes)
		if err != nil {
			reject(exception.NewTrace(err))
		} else {
			resolve()
		}
	}

	return utils.CreatePromise(promiseFn)
}

////////////////////////////////////////////////////////////////////////////////
// Download                                                                   //
////////////////////////////////////////////////////////////////////////////////

// Download begins the download of the file described in the marshalled
// [channelsFileTransfer.FileInfo]. The progress of the download is reported on
// the [bindings.FtReceivedProgressCallback].
//
// Once the download completes, the file will be stored in the event model with
// the given file ID and with the status [channels.ReceptionProcessingComplete].
//
// The [bindings.FtReceivedProgressCallback] only indicates the progress of the
// file download, not the status of the file in the event model. You must rely
// on updates from the event model to know when it can be retrieved.
//
// Parameters:
//   - args[0] - The JSON of [channelsFileTransfer.FileInfo] received on a
//     channel (Uint8Array).
//   - args[1] - The progress callback, which is a callback that reports the
//     progress of the file download. The callback is called once on
//     initialization, on every progress update (or less if restricted by the
//     period), or on fatal error. It must be a Javascript object that
//     implements the [bindings.FtReceivedProgressCallback] interface.
//   - args[2] - Progress callback period. A progress callback will be limited
//     from triggering only once per period, in milliseconds (int).
//
// Returns:
//   - Marshalled bytes of [fileTransfer.ID] that uniquely identifies the file.
//
// Returns a promise:
//   - Resolves to the marshalled bytes of [fileTransfer.ID] that uniquely
//     identifies the file. (Uint8Array).
//   - Rejected with an error if downloading fails.
func (cft *ChannelsFileTransfer) Download(_ js.Value, args []js.Value) any {
	var (
		fileInfoJSON = utils.CopyBytesToGo(args[0])
		progressCB   = &ftReceivedCallback{utils.WrapCB(args[1], "Callback")}
		periodMS     = args[2].Int()
	)

	promiseFn := func(resolve, reject func(args ...any) js.Value) {
		fileID, err := cft.api.Download(fileInfoJSON, progressCB, periodMS)
		if err != nil {
			reject(exception.NewTrace(err))
		} else {
			resolve(utils.CopyBytesToJS(fileID))
		}
	}

	return utils.CreatePromise(promiseFn)
}

// RegisterReceivedProgressCallback allows for the registration of a callback to
// track the progress of an individual file download.
//
// The callback will be called immediately when added to report the current
// progress of the transfer. It will then call every time a file part is
// received, the transfer completes, or a fatal error occurs. It is called at
// most once every period regardless of the number of progress updates.
//
// In the event that the client is closed and resumed, this function must be
// used to re-register any callbacks previously registered.
//
// Once the download completes, the file will be stored in the event model with
// the given file ID and with the status [channelsFileTransfer.Complete].
//
// The [bindings.FtReceivedProgressCallback] only indicates the progress of the
// file download, not the status of the file in the event model. You must rely
// on updates from the event model to know when it can be retrieved.
//
// Parameters:
//   - args[0] - Marshalled bytes of the file's [fileTransfer.ID] (Uint8Array).
//   - args[1] - The progress callback, which is a callback that reports the
//     progress of the file download. The callback is called once on
//     initialization, on every progress update (or less if restricted by the
//     period), or on fatal error. It must be a Javascript object that
//     implements the [bindings.FtReceivedProgressCallback] interface.
//   - args[2] - Progress callback period. A progress callback will be limited
//     from triggering only once per period, in milliseconds (int).
//
// Returns a promise:
//   - Resolves on success (void).
//   - Rejected with an error if registering the callback fails.
func (cft *ChannelsFileTransfer) RegisterReceivedProgressCallback(
	_ js.Value, args []js.Value) any {
	var (
		fileIDBytes = utils.CopyBytesToGo(args[0])
		progressCB  = &ftReceivedCallback{utils.WrapCB(args[1], "Callback")}
		periodMS    = args[2].Int()
	)

	promiseFn := func(resolve, reject func(args ...any) js.Value) {
		err := cft.api.RegisterReceivedProgressCallback(
			fileIDBytes, progressCB, periodMS)
		if err != nil {
			reject(exception.NewTrace(err))
		} else {
			resolve()
		}
	}

	return utils.CreatePromise(promiseFn)
}

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

// ftSentCallback wraps Javascript callbacks to adhere to the
// [bindings.FtSentProgressCallback] interface.
type ftSentCallback struct {
	callback func(args ...any) js.Value
}

// Callback is called when the status of the sent file changes.
//
// Parameters:
//   - payload - Returns the contents of the message. JSON of
//     [bindings.Progress] (Uint8Array).
//   - t - Returns a tracker that allows the lookup of the status of any file
//     part. It is a Javascript object that matches the functions on
//     [FilePartTracker].
//   - err - Returns an error on failure (Error).

// Callback is called when the progress on a sent file changes or an error
// occurs in the transfer.
//
// The [ChFilePartTracker] can be used to look up the status of individual file
// parts. Note, when completed == true, the [ChFilePartTracker] may be nil.
//
// Any error returned is fatal and the file must either be retried with
// [ChannelsFileTransfer.RetryUpload] or canceled with
// [ChannelsFileTransfer.CloseSend].
//
// This callback only indicates the status of the file transfer, not the status
// of the file in the event model. Do NOT use this callback as an indicator of
// when the file is available in the event model.
//
// Parameters:
//   - payload - JSON of [bindings.FtSentProgress], which describes the progress
//     of the current sent transfer.
//   - fpt - File part tracker that allows the lookup of the status of
//     individual file parts.
//   - err - Fatal errors during sending.
func (fsc *ftSentCallback) Callback(
	payload []byte, t *bindings.ChFilePartTracker, err error) {
	fsc.callback(utils.CopyBytesToJS(payload), newChFilePartTrackerJS(t),
		exception.NewTrace(err))
}

// ftReceivedCallback wraps Javascript callbacks to adhere to the
// [bindings.FtReceivedProgressCallback] interface.
type ftReceivedCallback struct {
	callback func(args ...any) js.Value
}

// Callback is called when
// the progress on a received file changes or an error occurs in the transfer.
//
// The [ChFilePartTracker] can be used to look up the status of individual file
// parts. Note, when completed == true, the [ChFilePartTracker] may be nil.
//
// This callback only indicates the status of the file transfer, not the status
// of the file in the event model. Do NOT use this callback as an indicator of
// when the file is available in the event model.
//
// Parameters:
//   - payload - JSON of [bindings.FtReceivedProgress], which describes the
//     progress of the current received transfer.
//   - fpt - File part tracker that allows the lookup of the status of
//     individual file parts.
//   - err - Fatal errors during receiving.
func (frc *ftReceivedCallback) Callback(
	payload []byte, t *bindings.ChFilePartTracker, err error) {
	frc.callback(utils.CopyBytesToJS(payload), newChFilePartTrackerJS(t),
		exception.NewTrace(err))
}

////////////////////////////////////////////////////////////////////////////////
// File Part Tracker                                                          //
////////////////////////////////////////////////////////////////////////////////

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

// newChFilePartTrackerJS creates a new Javascript compatible object
// (map[string]any) that matches the [FilePartTracker] structure.
func newChFilePartTrackerJS(api *bindings.ChFilePartTracker) map[string]any {
	fpt := ChFilePartTracker{api}
	ftMap := map[string]any{
		"GetPartStatus": js.FuncOf(fpt.GetPartStatus),
		"GetNumParts":   js.FuncOf(fpt.GetNumParts),
	}

	return ftMap
}

// GetPartStatus returns the status of the file part with the given part number.
//
// The possible values for the status are:
//   - 0 < Part does not exist
//   - 0 = unsent
//   - 1 = arrived (sender has sent a part, and it has arrived)
//   - 2 = received (receiver has received a part)
//
// Parameters:
//   - args[0] - Index of part (int).
//
// Returns:
//   - Part status (int).
func (fpt *ChFilePartTracker) GetPartStatus(_ js.Value, args []js.Value) any {
	return fpt.api.GetPartStatus(args[0].Int())
}

// GetNumParts returns the total number of file parts in the transfer.
//
// Returns:
//   - Number of parts (int).
func (fpt *ChFilePartTracker) GetNumParts(js.Value, []js.Value) any {
	return fpt.api.GetNumParts()
}