/*
Copyright 2020 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

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, _ 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 = 5

		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) {
	ok := false
	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
}
