////////////////////////////////////////////////////////////////////////////////
// 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 (
	"fmt"
	"github.com/armon/circbuf"
	"github.com/pkg/errors"
	jww "github.com/spf13/jwalterweatherman"
	"gitlab.com/elixxir/client/v4/bindings"
	"gitlab.com/elixxir/xxdk-wasm/utils"
	"io"
	"log"
	"syscall/js"
)

// logListeners is a list of all registered log listeners. This is used to add
// additional log listener without overwriting previously registered listeners.
var logListeners []jww.LogListener

// LogLevel sets level of logging. All logs at the set level and below will be
// displayed (e.g., when log level is ERROR, only ERROR, CRITICAL, and FATAL
// messages will be printed).
//
// Log level options:
//
//	TRACE    - 0
//	DEBUG    - 1
//	INFO     - 2
//	WARN     - 3
//	ERROR    - 4
//	CRITICAL - 5
//	FATAL    - 6
//
// The default log level without updates is INFO.
//
// Parameters:
//   - args[0] - Log level (int).
//
// Returns:
//   - Throws TypeError if the log level is invalid.
func LogLevel(_ js.Value, args []js.Value) any {
	threshold := jww.Threshold(args[0].Int())
	if threshold < jww.LevelTrace || threshold > jww.LevelFatal {
		err := errors.Errorf("log level is not valid: log level: %d", threshold)
		utils.Throw(utils.TypeError, err)
		return nil
	}

	jww.SetLogThreshold(threshold)
	jww.SetFlags(log.LstdFlags | log.Lmicroseconds)

	ll := NewJsConsoleLogListener(threshold)
	logListeners = append(logListeners, ll.Listen)
	jww.SetLogListeners(logListeners...)
	jww.SetStdoutThreshold(jww.LevelFatal + 1)

	msg := fmt.Sprintf("Log level set to: %s", threshold)
	switch threshold {
	case jww.LevelTrace:
		fallthrough
	case jww.LevelDebug:
		fallthrough
	case jww.LevelInfo:
		jww.INFO.Print(msg)
	case jww.LevelWarn:
		jww.WARN.Print(msg)
	case jww.LevelError:
		jww.ERROR.Print(msg)
	case jww.LevelCritical:
		jww.CRITICAL.Print(msg)
	case jww.LevelFatal:
		jww.FATAL.Print(msg)
	}

	return nil
}

// LogToFile enables logging to a file that can be downloaded.
//
// Parameters:
//   - args[0] - Log level (int).
//   - args[1] - Log file name (string).
//   - args[2] - Max log file size, in bytes (int).
//
// Returns:
//   - A Javascript representation of the [LogFile] object, which allows
//     accessing the contents of the log file and other metadata.
func LogToFile(_ js.Value, args []js.Value) any {
	threshold := jww.Threshold(args[0].Int())
	if threshold < jww.LevelTrace || threshold > jww.LevelFatal {
		err := errors.Errorf("log level is not valid: log level: %d", threshold)
		utils.Throw(utils.TypeError, err)
		return nil
	}

	// Create new log file output
	ll, err := NewLogFile(args[1].String(), threshold, args[2].Int())
	if err != nil {
		utils.Throw(utils.TypeError, err)
		return nil
	}

	logListeners = append(logListeners, ll.Listen)
	jww.SetLogListeners(logListeners...)

	msg := fmt.Sprintf("Outputting log to file %s of max size %d with level %s",
		ll.name, ll.b.Size(), threshold)
	switch threshold {
	case jww.LevelTrace:
		fallthrough
	case jww.LevelDebug:
		fallthrough
	case jww.LevelInfo:
		jww.INFO.Print(msg)
	case jww.LevelWarn:
		jww.WARN.Print(msg)
	case jww.LevelError:
		jww.ERROR.Print(msg)
	case jww.LevelCritical:
		jww.CRITICAL.Print(msg)
	case jww.LevelFatal:
		jww.FATAL.Print(msg)
	}

	return newLogFileJS(ll)
}

// logWriter wraps Javascript callbacks to adhere to the [bindings.LogWriter]
// interface.
type logWriter struct {
	log func(args ...any) js.Value
}

// Log returns a log message to pass to the log writer.
//
// Parameters:
//   - s - Log message (string).
func (lw *logWriter) Log(s string) { lw.log(s) }

// RegisterLogWriter registers a callback on which logs are written.
//
// Parameters:
//   - args[0] - a function that accepts a string and writes to a log. It must
//     be of the form func(string).
func RegisterLogWriter(_ js.Value, args []js.Value) any {
	bindings.RegisterLogWriter(&logWriter{args[0].Invoke})
	return nil
}

// EnableGrpcLogs sets GRPC trace logging.
//
// Parameters:
//   - args[0] - a function that accepts a string and writes to a log. It must
//     be of the form func(string).
func EnableGrpcLogs(_ js.Value, args []js.Value) any {
	bindings.EnableGrpcLogs(&logWriter{args[0].Invoke})
	return nil
}

////////////////////////////////////////////////////////////////////////////////
// Javascript Console Log Listener                                            //
////////////////////////////////////////////////////////////////////////////////

// console contains the Javascript console object, which provides access to the
// browser's debugging console. This structure detects logging types and prints
// it using the correct logging method.
type console struct {
	call string
	js.Value
}

// Write writes the data to the Javascript console at the level specified by the
// call.
func (c *console) Write(p []byte) (n int, err error) {
	c.Call(c.call, string(p))
	return len(p), nil
}

// JsConsoleLogListener redirects log output to the Javascript console.
type JsConsoleLogListener struct {
	jww.Threshold
	js.Value

	trace    *console
	debug    *console
	info     *console
	error    *console
	warn     *console
	critical *console
	fatal    *console
	def      *console
}

// NewJsConsoleLogListener initialises a new log listener that listener for the
// specific threshold and prints the logs to the Javascript console.
func NewJsConsoleLogListener(threshold jww.Threshold) *JsConsoleLogListener {
	consoleObj := js.Global().Get("console")
	return &JsConsoleLogListener{
		Threshold: threshold,
		Value:     consoleObj,
		trace:     &console{"debug", consoleObj},
		debug:     &console{"log", consoleObj},
		info:      &console{"info", consoleObj},
		warn:      &console{"warn", consoleObj},
		error:     &console{"error", consoleObj},
		critical:  &console{"error", consoleObj},
		fatal:     &console{"error", consoleObj},
		def:       &console{"log", consoleObj},
	}
}

// Listen is called for every logging event. This function adheres to the
// [jwalterweatherman.LogListener] type.
func (ll *JsConsoleLogListener) Listen(t jww.Threshold) io.Writer {
	if t < ll.Threshold {
		return nil
	}

	switch t {
	case jww.LevelTrace:
		return ll.trace
	case jww.LevelDebug:
		return ll.debug
	case jww.LevelInfo:
		return ll.info
	case jww.LevelWarn:
		return ll.warn
	case jww.LevelError:
		return ll.error
	case jww.LevelCritical:
		return ll.critical
	case jww.LevelFatal:
		return ll.fatal
	default:
		return ll.def
	}
}

////////////////////////////////////////////////////////////////////////////////
// Log File Log Listener                                                      //
////////////////////////////////////////////////////////////////////////////////

// LogFile represents a virtual log file in memory. It contains a circular
// buffer that limits the log file, overwriting the oldest logs.
type LogFile struct {
	name      string
	threshold jww.Threshold
	b         *circbuf.Buffer
}

// NewLogFile initialises a new [LogFile] for log writing.
func NewLogFile(
	name string, threshold jww.Threshold, maxSize int) (*LogFile, error) {
	// Create new buffer of the specified size
	b, err := circbuf.NewBuffer(int64(maxSize))
	if err != nil {
		return nil, err
	}

	return &LogFile{
		name:      name,
		threshold: threshold,
		b:         b,
	}, nil
}

// newLogFileJS creates a new Javascript compatible object (map[string]any) that
// matches the [LogFile] structure.
func newLogFileJS(lf *LogFile) map[string]any {
	logFile := map[string]any{
		"Name":      js.FuncOf(lf.Name),
		"Threshold": js.FuncOf(lf.Threshold),
		"GetFile":   js.FuncOf(lf.GetFile),
		"MaxSize":   js.FuncOf(lf.MaxSize),
		"Size":      js.FuncOf(lf.Size),
	}

	return logFile
}

// Listen is called for every logging event. This function adheres to the
// [jwalterweatherman.LogListener] type.
func (lf *LogFile) Listen(t jww.Threshold) io.Writer {
	if t < lf.threshold {
		return nil
	}

	return lf.b
}

// Name returns the name of the log file.
//
// Returns:
//   - File name (string).
func (lf *LogFile) Name(js.Value, []js.Value) any {
	return lf.name
}

// Threshold returns the log level threshold used in the file.
//
// Returns:
//   - Log level (string).
func (lf *LogFile) Threshold(js.Value, []js.Value) any {
	return lf.threshold.String()
}

// GetFile returns the entire log file.
//
// Returns:
//   - Log file contents (string).
func (lf *LogFile) GetFile(js.Value, []js.Value) any {
	return string(lf.b.Bytes())
}

// MaxSize returns the max size, in bytes, that the log file is allowed to be.
//
// Returns:
//   - Max file size (int).
func (lf *LogFile) MaxSize(js.Value, []js.Value) any {
	return lf.b.Size()
}

// Size returns the current size, in bytes, written to the log file.
//
// Returns:
//   - Current file size (int).
func (lf *LogFile) Size(js.Value, []js.Value) any {
	return lf.b.TotalWritten()
}