// Copyright (c) Acrosync LLC. All rights reserved.
// Free for personal use and commercial trial
// Commercial use requires per-user licenses available from https://duplicacy.com

package duplicacy

import (
	"fmt"
	"os"
	"log"
	"runtime/debug"
	"sync"
	"testing"
	"time"
	"regexp"
)

const (
	DEBUG  = -2
	TRACE  = -1
	INFO   = 0
	WARN   = 1
	ERROR  = 2
	FATAL  = 3
	ASSERT = 4
)

var LogFunction func(level int, logID string, message string)

var printLogHeader = false

func EnableLogHeader() {
	printLogHeader = true
}

var printStackTrace = false

func EnableStackTrace() {
	printStackTrace = true
}

var testingT *testing.T

func setTestingT(t *testing.T) {
	testingT = t
}

// Contains the ids of logs that won't be displayed
var suppressedLogs map[string]bool = map[string]bool{}

func SuppressLog(id string) {
	suppressedLogs[id] = true
}

func getLevelName(level int) string {
	switch level {
	case DEBUG:
		return "DEBUG"
	case TRACE:
		return "TRACE"
	case INFO:
		return "INFO"
	case WARN:
		return "WARN"
	case ERROR:
		return "ERROR"
	case FATAL:
		return "FATAL"
	case ASSERT:
		return "ASSERT"
	default:
		return fmt.Sprintf("[%d]", level)
	}
}

var loggingLevel int

func IsDebugging() bool {
	return loggingLevel <= DEBUG
}

func IsTracing() bool {
	return loggingLevel <= TRACE
}

func SetLoggingLevel(level int) {
	loggingLevel = level
}

func LOG_DEBUG(logID string, format string, v ...interface{}) {
	logf(DEBUG, logID, format, v...)
}

func LOG_TRACE(logID string, format string, v ...interface{}) {
	logf(TRACE, logID, format, v...)
}

func LOG_INFO(logID string, format string, v ...interface{}) {
	logf(INFO, logID, format, v...)
}

func LOG_WARN(logID string, format string, v ...interface{}) {
	logf(WARN, logID, format, v...)
}

func LOG_ERROR(logID string, format string, v ...interface{}) {
	logf(ERROR, logID, format, v...)
}

func LOG_WERROR(isWarning bool, logID string, format string, v ...interface{}) {
	if isWarning {
		logf(WARN, logID, format, v...)
	} else {
		logf(ERROR, logID, format, v...)
	}
}


func LOG_FATAL(logID string, format string, v ...interface{}) {
	logf(FATAL, logID, format, v...)
}

func LOG_ASSERT(logID string, format string, v ...interface{}) {
	logf(ASSERT, logID, format, v...)
}

type Exception struct {
	Level   int
	LogID   string
	Message string
}

var logMutex sync.Mutex

func logf(level int, logID string, format string, v ...interface{}) {

	message := fmt.Sprintf(format, v...)

	if LogFunction != nil {
		LogFunction(level, logID, message)
		return
	}

	now := time.Now()

	// Uncomment this line to enable unbufferred logging for tests
	// fmt.Printf("%s %s %s %s\n", now.Format("2006-01-02 15:04:05.000"), getLevelName(level), logID, message)

	if testingT != nil {
		if level <= WARN {
			if level >= loggingLevel {
				testingT.Logf("%s %s %s %s\n",
					now.Format("2006-01-02 15:04:05.000"), getLevelName(level), logID, message)
			}
		} else {
			testingT.Errorf("%s %s %s %s\n",
				now.Format("2006-01-02 15:04:05.000"), getLevelName(level), logID, message)
		}
	} else {
		logMutex.Lock()
		defer logMutex.Unlock()

		if level >= loggingLevel {
			if level <= ERROR && len(suppressedLogs) > 0 {
				if _, found := suppressedLogs[logID]; found {
					return
				}
			}

			if printLogHeader {
				fmt.Printf("%s %s %s %s\n",
					now.Format("2006-01-02 15:04:05.000"), getLevelName(level), logID, message)
			} else {
				fmt.Printf("%s\n", message)
			}
		}
	}

	if level > WARN {
		panic(Exception{
			Level:   level,
			LogID:   logID,
			Message: message,
		})
	}
}

// Set up logging for libraries that Duplicacy depends on.  They can call 'log.Printf("[ID] message")'
// to produce logs in Duplicacy's format
type Logger struct {
	formatRegex *regexp.Regexp
}

func (logger *Logger) Write(line []byte) (n int, err error) {
	n = len(line)
	for len(line) > 0 && line[len(line) - 1] == '\n' {
		line = line[:len(line) - 1]
	}
	matched := logger.formatRegex.FindStringSubmatch(string(line))
	if matched != nil {
		LOG_INFO(matched[1], "%s", matched[2])
	} else {
		LOG_INFO("LOG_DEFAULT", "%s", line)
	}

    return
}

func init() {
	log.SetFlags(0)
	log.SetOutput(&Logger{ formatRegex: regexp.MustCompile(`^\[(.+)\]\s*(.+)`) })
}

const (
	duplicacyExitCode = 100
	otherExitCode     = 101
)

// This is the function to be called before exiting when an error occurs.
var RunAtError func() = func() {}

func CatchLogException() {
	if r := recover(); r != nil {
		switch e := r.(type) {
		case Exception:
			if printStackTrace {
				debug.PrintStack()
			}
			RunAtError()
			os.Exit(duplicacyExitCode)
		default:
			fmt.Fprintf(os.Stderr, "%v\n", e)
			debug.PrintStack()
			RunAtError()
			os.Exit(otherExitCode)
		}
	}
}
