// Copyright (c) 2018, Sylabs Inc. All rights reserved.
// This software is licensed under a 3-clause BSD license. Please consult the
// LICENSE.md file distributed with the sources of this project regarding your
// rights to use or distribute this software.

// +build sylog

package sylog

import (
	"fmt"
	"io"
	"io/ioutil"
	"os"
	"runtime"
	"strconv"
	"strings"

	apexlog "github.com/apex/log"
)

type messageLevel int

const (
	fatal    messageLevel = iota - 4 // fatal    : -4
	error                            // error    : -3
	warn                             // warn     : -2
	log                              // log      : -1
	_                                // SKIP     : 0
	info                             // info     : 1
	verbose                          // verbose  : 2
	verbose2                         // verbose2 : 3
	verbose3                         // verbose3 : 4
	debug                            // debug    : 5
)

func (l messageLevel) String() string {
	str, ok := messageLabels[l]

	if !ok {
		str = "????"
	}

	return str
}

var messageLabels = map[messageLevel]string{
	fatal:    "FATAL",
	error:    "ERROR",
	warn:     "WARNING",
	log:      "LOG",
	info:     "INFO",
	verbose:  "VERBOSE",
	verbose2: "VERBOSE",
	verbose3: "VERBOSE",
	debug:    "DEBUG",
}

var messageColors = map[messageLevel]string{
	fatal: "\x1b[31m",
	error: "\x1b[31m",
	warn:  "\x1b[33m",
	info:  "\x1b[34m",
}

var colorReset = "\x1b[0m"

var loggerLevel messageLevel

func init() {
	_levelint := int(messageLevel(info))
	_levelstr, ok := os.LookupEnv("SINGULARITY_MESSAGELEVEL")
	if ok {
		_leveli, err := strconv.Atoi(_levelstr)
		if err == nil {
			_levelint = _leveli
		}
	}
	SetLevel(_levelint)
}

func prefix(level messageLevel) string {
	messageColor, ok := messageColors[level]
	if !ok {
		messageColor = "\x1b[0m"
	}

	// This section builds and returns the prefix for levels < debug
	if loggerLevel < debug {
		return fmt.Sprintf("%s%-8s%s ", messageColor, level.String()+":", colorReset)
	}

	pc, _, _, ok := runtime.Caller(3)
	details := runtime.FuncForPC(pc)

	var funcName string
	if ok && details == nil {
		fmt.Printf("Unable to get details of calling function\n")
		funcName = "UNKNOWN CALLING FUNC"
	} else {
		funcNameSplit := strings.Split(details.Name(), ".")
		funcName = funcNameSplit[len(funcNameSplit)-1] + "()"
	}

	uid := os.Geteuid()
	pid := os.Getpid()
	uidStr := fmt.Sprintf("[U=%d,P=%d]", uid, pid)

	return fmt.Sprintf("%s%-8s%s%-19s%-30s", messageColor, level, colorReset, uidStr, funcName)
}

func writef(w io.Writer, level messageLevel, format string, a ...interface{}) {
	if loggerLevel < level {
		return
	}

	message := fmt.Sprintf(format, a...)
	message = strings.TrimSuffix(message, "\n")

	fmt.Fprintf(w, "%s%s\n", prefix(level), message)
}

// Fatalf is equivalent to a call to Errorf followed by os.Exit(255). Code that
// may be imported by other projects should NOT use Fatalf.
func Fatalf(format string, a ...interface{}) {
	writef(os.Stderr, fatal, format, a...)
	os.Exit(255)
}

// Errorf writes an ERROR level message to the log but does not exit. This
// should be called when an error is being returned to the calling thread
func Errorf(format string, a ...interface{}) {
	writef(os.Stderr, error, format, a...)
}

// Warningf writes a WARNING level message to the log.
func Warningf(format string, a ...interface{}) {
	writef(os.Stderr, warn, format, a...)
}

// Infof writes an INFO level message to the log. By default, INFO level messages
// will always be output (unless running in silent)
func Infof(format string, a ...interface{}) {
	writef(os.Stderr, info, format, a...)
}

// Verbosef writes a VERBOSE level message to the log. This should probably be
// deprecated since the granularity is often too fine to be useful.
func Verbosef(format string, a ...interface{}) {
	writef(os.Stderr, verbose, format, a...)
}

// Debugf writes a DEBUG level message to the log.
func Debugf(format string, a ...interface{}) {
	writef(os.Stderr, debug, format, a...)
}

// SetLevel explicitly sets the loggerLevel
func SetLevel(l int) {
	loggerLevel = messageLevel(l)
	// set the apex log level, for umoci
	if loggerLevel <= error {
		// silent option
		apexlog.SetLevel(apexlog.ErrorLevel)
	} else if loggerLevel <= log {
		// quiet option
		apexlog.SetLevel(apexlog.WarnLevel)
	} else if loggerLevel < debug {
		// verbose option(s) or default
		apexlog.SetLevel(apexlog.InfoLevel)
	} else {
		// debug option
		apexlog.SetLevel(apexlog.DebugLevel)
	}
}

// DisableColor for the logger
func DisableColor() {
	messageColors = map[messageLevel]string{
		fatal: "",
		error: "",
		warn:  "",
		info:  "",
	}
	colorReset = ""
}

// GetLevel returns the current log level as integer
func GetLevel() int {
	return int(loggerLevel)
}

// GetEnvVar returns a formatted environment variable string which
// can later be interpreted by init() in a child proc
func GetEnvVar() string {
	return fmt.Sprintf("SINGULARITY_MESSAGELEVEL=%d", loggerLevel)
}

// Writer returns an io.Writer to pass to an external packages logging utility.
// i.e when --quiet option is set, this function returns ioutil.Discard writer to ignore output
func Writer() io.Writer {
	if loggerLevel <= -1 {
		return ioutil.Discard
	}

	return os.Stderr
}
