package log

import (
	"fmt"
	"runtime"
	"strings"

	"github.com/sirupsen/logrus"
)

type FileNameHook struct {
	field      string
	skipPrefix []string
	formatter  logrus.Formatter
	Formatter  func(file, function string, line int) string
}

type wrapper struct {
	old  logrus.Formatter
	hook *FileNameHook
}

// NewFilenameHook creates a new default FileNameHook.
func NewFilenameHook() *FileNameHook {
	return &FileNameHook{
		field:      "file",
		skipPrefix: []string{"log/", "logrus/", "logrus@"},
		Formatter: func(file, function string, line int) string {
			return fmt.Sprintf("%s:%d", file, line)
		},
	}
}

// Levels returns the levels for which the hook is activated. This contains
// currently only the DebugLevel.
func (f *FileNameHook) Levels() []logrus.Level {
	return []logrus.Level{logrus.DebugLevel}
}

// Fire executes the hook for every logrus entry.
func (f *FileNameHook) Fire(entry *logrus.Entry) error {
	if f.formatter != entry.Logger.Formatter {
		f.formatter = &wrapper{entry.Logger.Formatter, f}
	}

	entry.Logger.Formatter = f.formatter

	return nil
}

// Format returns the log format including the caller as field.
func (w *wrapper) Format(entry *logrus.Entry) ([]byte, error) {
	field := entry.WithField(
		w.hook.field,
		w.hook.Formatter(w.hook.findCaller()),
	)
	field.Level = entry.Level
	field.Message = entry.Message

	return w.old.Format(field)
}

// findCaller returns the file, function and line number for the current call.
func (f *FileNameHook) findCaller() (file, function string, line int) {
	var pc uintptr
	// The maximum amount of frames to be iterated
	const maxFrames = 10
	for i := range maxFrames {
		// The amount of frames to be skipped to land at the actual caller
		const skipFrames = 6

		pc, file, line = caller(skipFrames + i)
		if !f.shouldSkipPrefix(file) {
			break
		}
	}

	if pc != 0 {
		frames := runtime.CallersFrames([]uintptr{pc})
		frame, _ := frames.Next()
		function = frame.Function
	}

	return file, function, line
}

// caller reports file and line number information about function invocations
// on the calling goroutine's stack. The argument skip is the number of stack
// frames to ascend, with 0 identifying the caller of Caller.
func caller(skip int) (pc uintptr, file string, line int) {
	pc, file, line, ok := runtime.Caller(skip)
	if !ok {
		return 0, "", 0
	}

	n := 0

	for i := len(file) - 1; i > 0; i-- {
		if file[i] == '/' {
			n++
			if n >= 2 {
				file = file[i+1:]

				break
			}
		}
	}

	return pc, file, line
}

// shouldSkipPrefix returns true if the hook should be skipped, otherwise false.
func (f *FileNameHook) shouldSkipPrefix(file string) bool {
	for i := range f.skipPrefix {
		if strings.HasPrefix(file, f.skipPrefix[i]) {
			return true
		}
	}

	return false
}
