// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package log

import (
	"bytes"
	"fmt"
	"strings"
	"time"
)

type Event struct {
	Time time.Time

	Caller   string
	Filename string
	Line     int

	Level Level

	MsgSimpleText string

	msgFormat string // the format and args is only valid in the caller's goroutine
	msgArgs   []any  // they are discarded before the event is passed to the writer's channel

	Stacktrace string
}

type EventFormatted struct {
	Origin *Event
	Msg    any // the message formatted by the writer's formatter, the writer knows its type
}

type EventFormatter func(mode *WriterMode, event *Event, msgFormat string, msgArgs ...any) []byte

type logStringFormatter struct {
	v LogStringer
}

var _ fmt.Formatter = logStringFormatter{}

func (l logStringFormatter) Format(f fmt.State, verb rune) {
	if f.Flag('#') && verb == 'v' {
		_, _ = fmt.Fprintf(f, "%#v", l.v)
		return
	}
	_, _ = f.Write([]byte(l.v.LogString()))
}

// Copy of cheap integer to fixed-width decimal to ascii from logger.
// TODO: legacy bugs: doesn't support negative number, overflow if wid it too large.
func itoa(buf []byte, i, wid int) []byte {
	var s [20]byte
	bp := len(s) - 1
	for i >= 10 || wid > 1 {
		wid--
		q := i / 10
		s[bp] = byte('0' + i - q*10)
		bp--
		i = q
	}
	// i < 10
	s[bp] = byte('0' + i)
	return append(buf, s[bp:]...)
}

func colorSprintf(colorize bool, format string, args ...any) string {
	hasColorValue := false
	for _, v := range args {
		if _, hasColorValue = v.(*ColoredValue); hasColorValue {
			break
		}
	}
	if colorize || !hasColorValue {
		return fmt.Sprintf(format, args...)
	}

	noColors := make([]any, len(args))
	copy(noColors, args)
	for i, v := range args {
		if cv, ok := v.(*ColoredValue); ok {
			noColors[i] = cv.v
		}
	}
	return fmt.Sprintf(format, noColors...)
}

// EventFormatTextMessage makes the log message for a writer with its mode. This function is a copy of the original package
func EventFormatTextMessage(mode *WriterMode, event *Event, msgFormat string, msgArgs ...any) []byte {
	buf := make([]byte, 0, 1024)
	buf = append(buf, mode.Prefix...)
	t := event.Time
	flags := mode.Flags.Bits()
	if flags&(Ldate|Ltime|Lmicroseconds) != 0 {
		if mode.Colorize {
			buf = append(buf, fgCyanBytes...)
		}
		if flags&LUTC != 0 {
			t = t.UTC()
		}
		if flags&Ldate != 0 {
			year, month, day := t.Date()
			buf = itoa(buf, year, 4)
			buf = append(buf, '/')
			buf = itoa(buf, int(month), 2)
			buf = append(buf, '/')
			buf = itoa(buf, day, 2)
			buf = append(buf, ' ')
		}
		if flags&(Ltime|Lmicroseconds) != 0 {
			hour, minNum, sec := t.Clock()
			buf = itoa(buf, hour, 2)
			buf = append(buf, ':')
			buf = itoa(buf, minNum, 2)
			buf = append(buf, ':')
			buf = itoa(buf, sec, 2)
			if flags&Lmicroseconds != 0 {
				buf = append(buf, '.')
				buf = itoa(buf, t.Nanosecond()/1e3, 6)
			}
			buf = append(buf, ' ')
		}
		if mode.Colorize {
			buf = append(buf, resetBytes...)
		}
	}
	if flags&(Lshortfile|Llongfile) != 0 && event.Filename != "" {
		if mode.Colorize {
			buf = append(buf, fgGreenBytes...)
		}
		file := event.Filename
		if flags&Lmedfile == Lmedfile {
			fileLen := len(file)
			const softLimit = 20
			if fileLen > softLimit {
				slashIndex := strings.LastIndexByte(file[:fileLen-softLimit], '/')
				if slashIndex != -1 {
					file = ".../" + file[slashIndex+1:]
				}
			}
		} else if flags&Lshortfile != 0 {
			startIndex := strings.LastIndexByte(file, '/')
			if startIndex > 0 && startIndex < len(file) {
				file = file[startIndex+1:]
			}
		}
		buf = append(buf, file...)
		buf = append(buf, ':')
		buf = itoa(buf, event.Line, -1)
		if flags&(Lfuncname|Lshortfuncname) != 0 {
			buf = append(buf, ':')
		} else {
			if mode.Colorize {
				buf = append(buf, resetBytes...)
			}
			buf = append(buf, ' ')
		}
	}
	if flags&(Lfuncname|Lshortfuncname) != 0 {
		if mode.Colorize {
			buf = append(buf, fgGreenBytes...)
		}
		funcName := event.Caller
		shortFuncName := funcName
		if flags&Lshortfuncname != 0 {
			// funcName = "code.gitea.io/gitea/modules/foo/bar.MyFunc.func1.2()"
			slashPos := strings.LastIndexByte(funcName, '/')
			dotPos := strings.IndexByte(funcName[slashPos+1:], '.')
			if dotPos > 0 {
				// shortFuncName = "MyFunc.func1.2()"
				shortFuncName = funcName[slashPos+1+dotPos+1:]
				if strings.Contains(shortFuncName, ".") {
					shortFuncName = strings.ReplaceAll(shortFuncName, ".func", ".")
				}
			}
			funcName = shortFuncName
		}
		buf = append(buf, funcName...)
		if mode.Colorize {
			buf = append(buf, resetBytes...)
		}
		buf = append(buf, ' ')
	}

	if flags&(Llevel|Llevelinitial) != 0 {
		level := strings.ToUpper(event.Level.String())
		if mode.Colorize {
			buf = append(buf, ColorBytes(levelToColor[event.Level]...)...)
		}
		buf = append(buf, '[')
		if flags&Llevelinitial != 0 {
			buf = append(buf, level[0])
		} else {
			buf = append(buf, level...)
		}
		buf = append(buf, ']')
		if mode.Colorize {
			buf = append(buf, resetBytes...)
		}
		buf = append(buf, ' ')
	}

	var msg []byte

	// if the log needs colorizing, do it
	if mode.Colorize && len(msgArgs) > 0 {
		hasColorValue := false
		for _, v := range msgArgs {
			if _, hasColorValue = v.(*ColoredValue); hasColorValue {
				break
			}
		}
		if hasColorValue {
			msg = fmt.Appendf(nil, msgFormat, msgArgs...)
		}
	}
	// try to re-use the pre-formatted simple text message
	if len(msg) == 0 {
		msg = []byte(event.MsgSimpleText)
	}
	// if still no message, do the normal Sprintf for the message
	if len(msg) == 0 {
		msg = []byte(colorSprintf(mode.Colorize, msgFormat, msgArgs...))
	}
	// remove at most one trailing new line
	if len(msg) > 0 && msg[len(msg)-1] == '\n' {
		msg = msg[:len(msg)-1]
	}

	if flags&Lgopid == Lgopid {
		deprecatedGoroutinePid := "no-gopid" // use a dummy value to avoid breaking the log format
		buf = append(buf, '[')
		if mode.Colorize {
			buf = append(buf, ColorBytes(FgHiYellow)...)
		}
		buf = append(buf, deprecatedGoroutinePid...)
		if mode.Colorize {
			buf = append(buf, resetBytes...)
		}
		buf = append(buf, ']', ' ')
	}
	buf = append(buf, msg...)

	if event.Stacktrace != "" && mode.StacktraceLevel <= event.Level {
		lines := bytes.SplitSeq([]byte(event.Stacktrace), []byte("\n"))
		for line := range lines {
			buf = append(buf, "\n\t"...)
			buf = append(buf, line...)
		}
		buf = append(buf, '\n')
	}
	buf = append(buf, '\n')
	return buf
}
