package log

import (
	"fmt"
	log "github.com/sirupsen/logrus"
	"os"
	"path/filepath"
	"runtime"
	"strings"
	"time"
)

func GetLog(path string, showCodeLine bool) *log.Logger {

	hook := MyHook{
		Field: "line",
		Skip:  5,
		Path:  path, ShowCodeLine: showCodeLine,
	}

	loger := log.New()
	loger.SetFormatter(&log.TextFormatter{})
	loger.AddHook(&hook)
	return loger
}

// MyHook ...
type MyHook struct {
	Path         string //存储日志的位置
	ShowCodeLine bool   //输出代码文件名称和日志行
	Field        string
	Skip         int
	levels       []log.Level
}

// Levels 只定义 error 和 panic 等级的日志,其他日志等级不会触发 hook
func (that *MyHook) Levels() []log.Level {
	return log.AllLevels
}

// Fire 将异常日志写入到指定日志文件中
func (that *MyHook) Fire(entry *log.Entry) error {
	if that.ShowCodeLine {
		entry.Data[that.Field] = findCaller(that.Skip)
	}
	//不需要存储到文件
	if that.Path == "" {
		return nil
	}
	//存储到文件
	logFilePath := time.Now().Format(that.Path)

	err := os.MkdirAll(filepath.Dir(logFilePath), os.ModeAppend)
	if err != nil {
		return err
	}
	//os.Create(logFilePath)
	f, err := os.OpenFile(logFilePath, os.O_APPEND|os.O_CREATE, 0644)
	if err != nil {
		return err
	}
	bte, _ := entry.Bytes()
	_, err = f.Write(bte)
	if err != nil {
		return err
	}

	err = f.Close()
	if err != nil {
		return err
	}

	return nil
}

// 对caller进行递归查询, 直到找到非logrus包产生的第一个调用.
// 因为filename我获取到了上层目录名, 因此所有logrus包的调用的文件名都是 logrus/...
// 因此通过排除logrus开头的文件名, 就可以排除所有logrus包的自己的函数调用
func findCaller(skip int) string {
	file := ""
	line := 0
	for i := 0; i < 10; i++ {
		file, line = getCaller(skip + i)
		if !strings.HasPrefix(file, "logrus") {
			j := 0
			for true {
				j++
				if file == "common/error.go" {
					file, line = getCaller(skip + i + j)
				}
				if file == "db/hotimedb.go" {
					file, line = getCaller(skip + i + j)
				}
				if file == "code/makecode.go" {
					file, line = getCaller(skip + i + j)
				}
				if strings.Index(file, "common/") == 0 {
					file, line = getCaller(skip + i + j)
				}
				if strings.Contains(file, "application.go") {
					file, line = getCaller(skip + i + j)
				}
				if j == 5 {
					break
				}
			}

			break
		}
	}
	return fmt.Sprintf("%s:%d", file, line)
}

// 这里其实可以获取函数名称的: fnName := runtime.FuncForPC(pc).Name()
// 但是我觉得有 文件名和行号就够定位问题, 因此忽略了caller返回的第一个值:pc
// 在标准库log里面我们可以选择记录文件的全路径或者文件名, 但是在使用过程成并发最合适的,
// 因为文件的全路径往往很长, 而文件名在多个包中往往有重复, 因此这里选择多取一层, 取到文件所在的上层目录那层.
func getCaller(skip int) (string, int) {
	_, file, line, ok := runtime.Caller(skip)
	//fmt.Println(file)
	//fmt.Println(line)
	if !ok {
		return "", 0
	}
	n := 0
	for i := len(file) - 1; i > 0; i-- {
		if file[i] == '/' {
			n++
			if n >= 2 {
				file = file[i+1:]
				break
			}
		}
	}
	return file, line
}
