/**
 * 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 logcfg

import (
	"bytes"
	"fmt"
	"io"
	"io/ioutil"
	"os"
	"path/filepath"
	"sort"
	"strings"

	log "github.com/sirupsen/logrus"
	"github.com/spf13/cast"

	"mynewt.apache.org/newt/newt/newtutil"
	"mynewt.apache.org/newt/newt/pkg"
	"mynewt.apache.org/newt/newt/syscfg"
	"mynewt.apache.org/newt/newt/val"
	"mynewt.apache.org/newt/util"
)

const HEADER_PATH = "logcfg/logcfg.h"

type Log struct {
	// Log name; equal to the name of the YAML map that defines the log.
	Name string

	// The package that defines the log.
	Source *pkg.LocalPackage

	// The log's numeric module ID.
	Module val.ValSetting

	// The level assigned to this log.
	Level val.ValSetting

	// The log's string name
	NameStr string
}

// Map of: [log-name] => log
type LogMap map[string]Log

// The log configuration of the target.
type LCfg struct {
	// [log-name] => log
	Logs LogMap

	// Strings describing errors encountered while parsing the log config.
	InvalidSettings []string

	// Contains sets of logs with conflicting module IDs.
	//     [module-ID] => <slice-of-logs-with-module-id>
	ModuleConflicts map[int][]Log
}

// Maps numeric log levels to their string representations.  Used when
// generating the C log macros.
var logLevelNames = []string{
	0:  "DEBUG",
	1:  "INFO",
	2:  "WARN",
	3:  "ERROR",
	4:  "CRITICAL",
	15: "DISABLED",
}

func LogLevelString(level int) string {
	if level < 0 || level >= len(logLevelNames) {
		return "???"
	}

	return logLevelNames[level]
}

func NewLCfg() LCfg {
	return LCfg{
		Logs:            map[string]Log{},
		ModuleConflicts: map[int][]Log{},
	}
}

// Parses a single log definition from a YAML map.  The `logMapItf` parameter
// should be a map with the following elements:
//     "module": <module-string>
//     "level": <level-string>
func parseOneLog(name string, lpkg *pkg.LocalPackage, logMapItf interface{},
	cfg *syscfg.Cfg) (Log, error) {

	cl := Log{
		Name:   name,
		Source: lpkg,
	}

	logMap := cast.ToStringMapString(logMapItf)
	if logMap == nil {
		return cl, util.FmtNewtError(
			"\"%s\" missing required field \"module\"", name)
	}

	modStr := logMap["module"]
	if modStr == "" {
		return cl, util.FmtNewtError(
			"\"%s\" missing required field \"module\"", name)
	}
	mod, err := val.ResolveValSetting(modStr, cfg)
	if err != nil {
		return cl, util.FmtNewtError(
			"\"%s\" contains invalid \"module\": %s",
			name, err.Error())
	}
	if _, err := mod.IntVal(); err != nil {
		return cl, util.FmtNewtError(
			"\"%s\" contains invalid \"module\": %s", name, err.Error())
	}

	levelStr := logMap["level"]
	if levelStr == "" {
		return cl, util.FmtNewtError(
			"\"%s\" missing required field \"level\"", name)
	}
	level, err := val.ResolveValSetting(levelStr, cfg)
	if err != nil {
		return cl, util.FmtNewtError(
			"\"%s\" contains invalid \"level\": %s",
			name, err.Error())
	}
	if _, err := level.IntVal(); err != nil {
		return cl, util.FmtNewtError(
			"\"%s\" contains invalid \"level\": %s", name, err.Error())
	}

	nameStr := logMap["name"]
	if nameStr == "" {
		// If there is no "name:" node, use log id without _LOG suffix
		nameStr = "\"" + strings.Replace(name, "_LOG", "", 1) + "\""
	}

	cl.Module = mod
	cl.Level = level
	cl.NameStr = nameStr

	return cl, nil
}

// Reads all the logs defined by the specified package.  The log definitions
// are read from the `syscfg.logs` map in the package's `syscfg.yml` file.
func (lcfg *LCfg) readOnePkg(lpkg *pkg.LocalPackage, cfg *syscfg.Cfg) {
	lsettings := cfg.AllSettingsForLpkg(lpkg)
	logMaps, err := lpkg.SyscfgY.GetValStringMap("syscfg.logs", lsettings)
	util.OneTimeWarningError(err)

	for name, logMapItf := range logMaps {
		cl, err := parseOneLog(name, lpkg, logMapItf, cfg)
		if err != nil {
			lcfg.InvalidSettings =
				append(lcfg.InvalidSettings, strings.TrimSpace(err.Error()))
		} else {
			lcfg.Logs[cl.Name] = cl
		}
	}
}

// Searches the log configuration for logs with identical module IDs.  The log
// configuration object is populated with the results.
func (lcfg *LCfg) detectModuleConflicts() {
	m := map[int][]Log{}

	for _, l := range lcfg.Logs {
		intMod, _ := l.Module.IntVal()
		m[intMod] = append(m[intMod], l)
	}

	for mod, logs := range m {
		if len(logs) > 1 {
			for _, l := range logs {
				lcfg.ModuleConflicts[mod] =
					append(lcfg.ModuleConflicts[mod], l)
			}
		}
	}
}

// Reads all log definitions for each of the specified packages.  The
// returned LCfg object is populated with the result of this operation.
func Read(lpkgs []*pkg.LocalPackage, cfg *syscfg.Cfg) LCfg {
	lcfg := NewLCfg()

	for _, lpkg := range lpkgs {
		lcfg.readOnePkg(lpkg, cfg)
	}

	lcfg.detectModuleConflicts()

	return lcfg
}

// If any errors were encountered while parsing log definitions, this function
// returns a string indicating the errors.  If no errors were encountered, ""
// is returned.
func (lcfg *LCfg) ErrorText() string {
	str := ""

	if len(lcfg.InvalidSettings) > 0 {
		str += "Invalid log definitions detected:"
		for _, e := range lcfg.InvalidSettings {
			str += "\n    " + e
		}
	}

	if len(lcfg.ModuleConflicts) > 0 {
		str += "Log module conflicts detected:\n"
		for mod, logs := range lcfg.ModuleConflicts {
			for _, l := range logs {
				str += fmt.Sprintf("    Module=%d Log=%s Package=%s\n",
					mod, l.Name, l.Source.FullName())
			}
		}

		str +=
			"\nResolve the problem by assigning unique module IDs to each log."
	}

	return str
}

// Retrieves a sorted slice of logs from the receiving log configuration.
func (lcfg *LCfg) sortedLogs() []Log {
	names := make([]string, 0, len(lcfg.Logs))

	for n, _ := range lcfg.Logs {
		names = append(names, n)
	}
	sort.Strings(names)

	logs := make([]Log, 0, len(names))
	for _, n := range names {
		logs = append(logs, lcfg.Logs[n])
	}

	return logs
}

// Writes a no-op stub log C macro definition.
func writeLogStub(logName string, levelStr string, w io.Writer) {
	fmt.Fprintf(w, "#define %s_%s(...) IGNORE(__VA_ARGS__)\n",
		logName, levelStr)
}

// Writes a log C macro definition.
func writeLogMacro(logName string, module int, levelStr string, w io.Writer) {
	fmt.Fprintf(w,
		"#define %s_%s(...) MODLOG_%s(%d, __VA_ARGS__)\n",
		logName, levelStr, levelStr, module)
}

// Write log C macro definitions for each log in the log configuration.
func (lcfg *LCfg) writeLogMacros(w io.Writer) {
	logs := lcfg.sortedLogs()
	for _, l := range logs {
		fmt.Fprintf(w, "\n")

		levelInt, _ := util.AtoiNoOct(l.Level.Value)
		for i, levelStr := range logLevelNames {
			if levelStr != "" {
				if i < levelInt {
					writeLogStub(l.Name, levelStr, w)
				} else {
					modInt, _ := l.Module.IntVal()
					writeLogMacro(l.Name, modInt, levelStr, w)
				}
			}
		}
	}
}

// Write log C macro definitions for each log in the log configuration.
func (lcfg *LCfg) writeLogModuleNames(w io.Writer) {
	logs := lcfg.sortedLogs()
	fmt.Fprintf(w, "\n")
	for _, l := range logs {
		fmt.Fprintf(w,
			"#define %s_NAME %s\n",
			l.Name, l.NameStr)
	}
}

func (lcfg *LCfg) writeLogModuleCases(w io.Writer) {
	logs := lcfg.sortedLogs()
	for _, l := range logs {
		fmt.Fprintf(w,
			"    case %s: return %s_NAME;\n",
			l.Module.Value, l.Name)
	}
}

// Writes a logcfg header file to the specified writer.
func (lcfg *LCfg) writeHeader(w io.Writer) {
	fmt.Fprintf(w, newtutil.GeneratedPreamble())

	fmt.Fprintf(w, "#ifndef H_MYNEWT_LOGCFG_\n")
	fmt.Fprintf(w, "#define H_MYNEWT_LOGCFG_\n\n")

	if len(lcfg.Logs) > 0 {
		fmt.Fprintf(w, "#include \"modlog/modlog.h\"\n")
		fmt.Fprintf(w, "#include \"log_common/log_common.h\"\n")

		lcfg.writeLogMacros(w)
		lcfg.writeLogModuleNames(w)
		fmt.Fprintf(w, "\n")
	}
	fmt.Fprintf(w, "const char *logcfg_log_module_name(uint8_t id);\n\n")

	fmt.Fprintf(w, "#endif\n")
}

// Writes a logcfg src file to the specified writer.
func (lcfg *LCfg) writeSource(w io.Writer) {
	fmt.Fprintf(w, newtutil.GeneratedPreamble())

	fmt.Fprintf(w, "#include <stdint.h>\n")
	fmt.Fprintf(w, "#include <stdlib.h>\n")
	fmt.Fprintf(w, "#include <logcfg/logcfg.h>\n\n")
	fmt.Fprintf(w, "const char *\nlogcfg_log_module_name(uint8_t id)\n{\n")
	fmt.Fprintf(w, "    switch (id) {\n")
	lcfg.writeLogModuleCases(w)
	fmt.Fprintf(w, "    default: return NULL;\n    }\n}\n")
}

// Ensures an up-to-date logcfg header is written for the target.
func (lcfg *LCfg) EnsureWritten(includeDir string, srcDir string, targetName string) error {
	buf := bytes.Buffer{}
	srcBuf := bytes.Buffer{}
	lcfg.writeHeader(&buf)
	lcfg.writeSource(&srcBuf)

	path := includeDir + "/" + HEADER_PATH

	writeReqd, err := util.FileContentsChanged(path, buf.Bytes())
	if err != nil {
		return err
	}
	if writeReqd {
		log.Debugf("logcfg changed; writing header file (%s).", path)

		if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
			return util.NewNewtError(err.Error())
		}

		if err := ioutil.WriteFile(path, buf.Bytes(), 0644); err != nil {
			return util.NewNewtError(err.Error())
		}
	} else {
		log.Debugf("logcfg unchanged; not writing header file (%s).", path)
	}

	path = fmt.Sprintf("%s/%s-logcfg.c", srcDir, targetName)
	writeReqd, err = util.FileContentsChanged(path, srcBuf.Bytes())

	if writeReqd {
		log.Debugf("logcfg changed; writing source file (%s).", path)
		if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
			return util.NewNewtError(err.Error())
		}
		if err := os.WriteFile(path, srcBuf.Bytes(), 0644); err != nil {
			return util.NewNewtError(err.Error())
		}
	}
	return nil
}
