/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 common

import (
	"errors"
	"fmt"
	"htrace/conf"
	"log"
	"os"
	"path/filepath"
	"sort"
	"strings"
	"sync"
	"time"
)

// A logSink is a place logs can be written to.
type logSink struct {
	path     logPath
	file     *os.File
	lock     sync.Mutex
	refCount int // protected by logFilesLock
}

// Write to the logSink.
func (sink *logSink) write(str string) {
	sink.lock.Lock()
	defer sink.lock.Unlock()
	_, err := sink.file.Write([]byte(str))
	if err != nil {
		fmt.Fprintf(os.Stderr, "Error logging to '%s': %s\n", sink.path, err.Error())
	}
}

// Unreference the logSink.  If there are no more references, and the logSink is
// closeable, then we will close it here.
func (sink *logSink) Unref() {
	logFilesLock.Lock()
	defer logFilesLock.Unlock()
	sink.refCount--
	if sink.refCount <= 0 {
		if sink.path.IsCloseable() {
			err := sink.file.Close()
			if err != nil {
				fmt.Fprintf(os.Stderr, "Error closing log file %s: %s\n",
					sink.path, err.Error())
			}
		}
		logSinks[sink.path] = nil
	}
}

type logPath string

// An empty LogPath represents "stdout."
const STDOUT_LOG_PATH = ""

// Convert a path to a logPath.
func logPathFromString(path string) logPath {
	if path == STDOUT_LOG_PATH {
		return logPath("")
	}
	absPath, err := filepath.Abs(path)
	if err != nil {
		fmt.Fprintf(os.Stderr, "Failed to get absolute path of %s: %s\n",
			path, err.Error())
		return logPath(path)
	}
	return logPath(absPath)
}

// Convert the path to a human-readable string.
func (path logPath) String() string {
	if path == "" {
		return "(stdout)"
	} else {
		return string(path)
	}
}

// Return true if the path is closeable.  stdout is not closeable.
func (path logPath) IsCloseable() bool {
	return path != STDOUT_LOG_PATH
}

func (path logPath) Open() *logSink {
	if path == STDOUT_LOG_PATH {
		return &logSink{path: path, file: os.Stdout}
	}
	file, err := os.OpenFile(string(path), os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
	if err != nil {
		sink := &logSink{path: STDOUT_LOG_PATH, file: os.Stdout}
		fmt.Fprintf(os.Stderr, "Failed to open log file %s: %s\n",
			path, err.Error())
		return sink
	}
	return &logSink{path: path, file: file}
}

var logFilesLock sync.Mutex

var logSinks map[logPath]*logSink = make(map[logPath]*logSink)

func getOrCreateLogSink(pathStr string) *logSink {
	path := logPathFromString(pathStr)
	logFilesLock.Lock()
	defer logFilesLock.Unlock()
	sink := logSinks[path]
	if sink == nil {
		sink = path.Open()
		logSinks[path] = sink
	}
	sink.refCount++
	return sink
}

type Level int

const (
	TRACE Level = iota
	DEBUG
	INFO
	WARN
	ERROR
)

var levelToString map[Level]string = map[Level]string{
	TRACE: "TRACE",
	DEBUG: "DEBUG",
	INFO:  "INFO",
	WARN:  "WARN",
	ERROR: "ERROR",
}

func (level Level) String() string {
	return levelToString[level]
}

func (level Level) LogString() string {
	return level.String()[0:1]
}

func LevelFromString(str string) (Level, error) {
	for k, v := range levelToString {
		if strings.ToLower(v) == strings.ToLower(str) {
			return k, nil
		}
	}
	var levelNames sort.StringSlice
	levelNames = make([]string, len(levelToString))
	var i int
	for _, v := range levelToString {
		levelNames[i] = v
		i++
	}
	sort.Sort(levelNames)
	return TRACE, errors.New(fmt.Sprintf("No such level as '%s'.  Valid "+
		"levels are '%v'\n", str, levelNames))
}

type Logger struct {
	sink  *logSink
	Level Level
}

func NewLogger(faculty string, cnf *conf.Config) *Logger {
	path, level := parseConf(faculty, cnf)
	sink := getOrCreateLogSink(path)
	return &Logger{sink: sink, Level: level}
}

func parseConf(faculty string, cnf *conf.Config) (string, Level) {
	facultyLogPathKey := faculty + "." + conf.HTRACE_LOG_PATH
	var facultyLogPath string
	if cnf.Contains(facultyLogPathKey) {
		facultyLogPath = cnf.Get(facultyLogPathKey)
	} else {
		facultyLogPath = cnf.Get(conf.HTRACE_LOG_PATH)
	}
	facultyLogLevelKey := faculty + "." + conf.HTRACE_LOG_LEVEL
	var facultyLogLevelStr string
	if cnf.Contains(facultyLogLevelKey) {
		facultyLogLevelStr = cnf.Get(facultyLogLevelKey)
	} else {
		facultyLogLevelStr = cnf.Get(conf.HTRACE_LOG_LEVEL)
	}
	level, err := LevelFromString(facultyLogLevelStr)
	if err != nil {
		fmt.Fprintf(os.Stderr, "Error configuring log level: %s.  Using TRACE.\n")
		level = TRACE
	}
	return facultyLogPath, level
}

func (lg *Logger) Trace(str string) {
	lg.Write(TRACE, str)
}

func (lg *Logger) Tracef(format string, v ...interface{}) {
	lg.Write(TRACE, fmt.Sprintf(format, v...))
}

func (lg *Logger) Debug(str string) {
	lg.Write(DEBUG, str)
}

func (lg *Logger) Debugf(format string, v ...interface{}) {
	lg.Write(DEBUG, fmt.Sprintf(format, v...))
}

func (lg *Logger) Info(str string) {
	lg.Write(INFO, str)
}

func (lg *Logger) Infof(format string, v ...interface{}) {
	lg.Write(INFO, fmt.Sprintf(format, v...))
}

func (lg *Logger) Warn(str string) error {
	lg.Write(WARN, str)
	return errors.New(str)
}

func (lg *Logger) Warnf(format string, v ...interface{}) error {
	str := fmt.Sprintf(format, v...)
	lg.Write(WARN, str)
	return errors.New(str)
}

func (lg *Logger) Error(str string) error {
	lg.Write(ERROR, str)
	return errors.New(str)
}

func (lg *Logger) Errorf(format string, v ...interface{}) error {
	str := fmt.Sprintf(format, v...)
	lg.Write(ERROR, str)
	return errors.New(str)
}

func (lg *Logger) Write(level Level, str string) {
	if level >= lg.Level {
		lg.sink.write(time.Now().UTC().Format(time.RFC3339) + " " +
			level.LogString() + ": " + str)
	}
}

//
// A few functions which can be used to determine if a certain level of tracing
// is enabled.  These are useful in situations when evaluating the parameters
// of a logging function is expensive.  (Note, however, that we don't pay the
// cost of string concatenation and manipulation when a log message doesn't
// trigger.)
//

func (lg *Logger) TraceEnabled() bool {
	return lg.Level <= TRACE
}

func (lg *Logger) DebugEnabled() bool {
	return lg.Level <= DEBUG
}

func (lg *Logger) InfoEnabled() bool {
	return lg.Level <= INFO
}

func (lg *Logger) WarnEnabled() bool {
	return lg.Level <= WARN
}

func (lg *Logger) ErrorEnabled() bool {
	return lg.Level <= ERROR
}

func (lg *Logger) LevelEnabled(level Level) bool {
	return lg.Level <= level
}

func (lg *Logger) Close() {
	lg.sink.Unref()
	lg.sink = nil
}

// Wraps an htrace logger in a golang standard logger.
//
// This is a bit messy because of the difference in interfaces between the
// golang standard logger and the htrace logger.  The golang standard logger
// doesn't support log levels directly, so you must choose up front what htrace
// log level all messages should be treated as.  Golang standard loggers expect
// to be able to write to an io.Writer, but make no guarantees about whether
// they will break messages into multiple Write() calls (although this does
// not seem to be a major problem in practice.)
//
// Despite these limitations, it's still useful to have this method to be able
// to log things that come out of the go HTTP server and other standard library
// systems.
type WrappedLogger struct {
	lg    *Logger
	level Level
}

func (lg *Logger) Wrap(prefix string, level Level) *log.Logger {
	wlg := &WrappedLogger{
		lg:    lg,
		level: level,
	}
	return log.New(wlg, prefix, 0)
}

func (wlg *WrappedLogger) Write(p []byte) (int, error) {
	wlg.lg.Write(wlg.level, string(p))
	return len(p), nil
}
