// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT

package logger

import (
	"bytes"
	"context"
	"fmt"
	"io"
	"log/slog"
	"os"
	"runtime"
	"strings"
	"time"
)

type logger struct {
	levels   map[Level]int
	writer   io.Writer
	logger   *slog.Logger
	level    Level
	levelVar *slog.LevelVar
}

var levels = map[Level]int{
	Trace: int(slog.LevelDebug) - 1,
	Debug: int(slog.LevelDebug),
	Info:  int(slog.LevelInfo),
	Warn:  int(slog.LevelWarn),
	Error: int(slog.LevelError),
	Fatal: int(slog.LevelError + 1),
}

var levelList = []Level{
	Trace,
	Debug,
	Info,
	Warn,
	Error,
	Fatal,
}

func MoreVerbose(level Level) *Level {
	position := levelToPosition(level) - 1
	if position > 0 {
		return &levelList[position]
	}
	return nil
}

func LessVerbose(level Level) *Level {
	position := levelToPosition(level) + 1
	if position < len(levelList) {
		return &levelList[position]
	}
	return nil
}

func levelToPosition(level Level) int {
	var i int
	for i = 0; i < len(levelList); i++ {
		if levelList[i] == level {
			break
		}
	}
	if i >= len(levelList) {
		panic(fmt.Errorf("unknown verbosity %v", level))
	}
	return i
}

func NewLogger() Interface {
	l := &logger{}
	l.Init()
	return l
}

type captureLogger struct {
	logger
	buf *bytes.Buffer
}

func NewCaptureLogger() CaptureInterface {
	l := &captureLogger{}
	l.buf = new(bytes.Buffer)
	l.writer = l.buf
	l.Init()
	return l
}

func (o *captureLogger) String() string {
	return o.buf.String()
}

func (o *captureLogger) GetBuffer() *bytes.Buffer {
	return o.buf
}

func (o *captureLogger) Reset() {
	o.buf.Reset()
}

var filenamePrefix string

func init() {
	_, filename, _, _ := runtime.Caller(0)
	filenamePrefix = strings.TrimSuffix(filename, "logger.go")
	if filenamePrefix == filename {
		// in case the source code file is moved, we can not trim the suffix, the code above should also be updated.
		panic("unable to detect correct package prefix, please update file: " + filename)
	}
}

func (o *logger) Log(skip int, level Level, format string, args ...any) {
	slogLevel := levels[level]
	logger := o.logger
	if !logger.Handler().Enabled(context.Background(), slog.Level(slogLevel)) {
		return
	}
	var pcs [1]uintptr
	runtime.Callers(2+skip, pcs[:]) // 2 is to skip [Callers(), Log()]
	r := slog.NewRecord(time.Now(), slog.Level(slogLevel), fmt.Sprintf(format, args...), pcs[0])
	_ = logger.Handler().Handle(context.Background(), r)
}

func (o *logger) Init() {
	replace := func(groups []string, a slog.Attr) slog.Attr {
		if a.Key == slog.TimeKey && len(groups) == 0 {
			return slog.Attr{}
		}
		if a.Key == slog.SourceKey {
			source := a.Value.Any().(*slog.Source)
			var function string
			dot := strings.LastIndex(source.Function, ".")
			if dot >= 0 {
				function = ":" + source.Function[dot+1:]
			}
			source.File = strings.TrimPrefix(source.File, projectPackagePrefix) + function
		}
		return a
	}
	o.levelVar = new(slog.LevelVar)
	if o.writer == nil {
		o.writer = os.Stdout
	}
	o.logger = slog.New(slog.NewTextHandler(o.writer, &slog.HandlerOptions{
		Level:       o.levelVar,
		AddSource:   true,
		ReplaceAttr: replace,
	}))
}

func (o *logger) SetLevel(level Level) {
	o.level = level
	o.levelVar.Set(slog.Level(levels[o.level]))
}

func (o *logger) GetLevel() Level {
	return o.level
}

func (o *logger) SetWriter(out io.Writer) {
	o.writer = out
	o.Init()
}

func (o *logger) Message(message string, args ...any) { o.Log(1, Info, message, args...) }
func (o *logger) Trace(message string, args ...any)   { o.Log(1, Trace, message, args...) }
func (o *logger) Debug(message string, args ...any)   { o.Log(1, Debug, message, args...) }
func (o *logger) Info(message string, args ...any)    { o.Log(1, Info, message, args...) }
func (o *logger) Warn(message string, args ...any)    { o.Log(1, Warn, message, args...) }
func (o *logger) Error(message string, args ...any)   { o.Log(1, Error, message, args...) }
func (o *logger) Fatal(message string, args ...any)   { o.Log(1, Fatal, message, args...) }

type Logger struct {
	logger Interface
}

func (o *Logger) GetLogger() Interface { return o.logger }

func (o *Logger) SetLogger(logger Interface) { o.logger = logger }

func (o *Logger) SetLevel(level Level) { o.logger.SetLevel(level) }

func (o *Logger) GetLevel() Level { return o.logger.GetLevel() }

func (o *Logger) Message(message string, args ...any) {
	o.logger.Log(1, Message, message, args...)
}

func (o *Logger) Trace(message string, args ...any) {
	o.logger.Log(1, Trace, message, args...)
}

func (o *Logger) Debug(message string, args ...any) {
	o.logger.Log(1, Debug, message, args...)
}

func (o *Logger) Info(message string, args ...any) {
	o.logger.Log(1, Info, message, args...)
}

func (o *Logger) Warn(message string, args ...any) {
	o.logger.Log(1, Warn, message, args...)
}

func (o *Logger) Error(message string, args ...any) {
	o.logger.Log(1, Error, message, args...)
}

func (o *Logger) Fatal(message string, args ...any) {
	o.logger.Log(1, Fatal, message, args...)
}

func (o *Logger) Log(skip int, level Level, message string, args ...any) {
	o.logger.Log(skip+1, level, message, args...)
}

var projectPackagePrefix string

func init() {
	_, filename, _, _ := runtime.Caller(0)
	projectPackagePrefix = strings.TrimSuffix(filename, "logger/logger.go")
	if projectPackagePrefix == filename {
		// in case the source code file is moved, we can not trim the suffix, the code above should also be updated.
		panic("unable to detect correct package prefix, please update file: " + filename)
	}
}
