//
// Copyright (c) 2017-2018 Intel Corporation
//
// SPDX-License-Identifier: Apache-2.0
//

package main

import (
	"fmt"
	"reflect"
	"strings"
	"time"
)

// TimeDelta is a time duration type used to record the difference between two
// timestamps.
type TimeDelta time.Duration

// NewTimeDelta create a new TimeDelta from the specified duration.
func NewTimeDelta(d time.Duration) TimeDelta {
	return TimeDelta(d)
}

// String implements the Stringer interface to avoid the "clever" default for
// time.Duration which converts the value into a human-readable format (which
// varies on the value). We just want a fixed nanosecond format.
func (t TimeDelta) String() string {
	return fmt.Sprintf("%d", t)
}

// MapSS is a hack for the xml package which is unable to marshal map types
// without help.
//
// See MapSS.MarshalXML()
type MapSS map[string]string

// Version of LogEntry contents (in semver.org format).
// XXX: Update whenever LogEntry changes!
const logEntryFormatVersion = "0.0.2"

// LogEntry is the main type used by the tool. It encapsulates a number of
// fields that all system components are expected to set, but also includes
// additional fields generated by this program.
//
// Notes:
//
//   - An anonymous field is not used to distinguish between the two
//     categories of fields because that extra layer becomes visible when the
//     struct is converted to various formats (specifically CSV, text and YAML).
//
// - XXX: If you change this struct, update logEntryFormatVersion!
type LogEntry struct {
	// Used to store additional (non-standard) fields
	Data MapSS

	Time time.Time

	// Name of the file this entry belongs to
	Filename string

	Level string
	Msg   string

	// System component type and name that generated the log entry
	Source string
	Name   string

	// Container ID. This is set for most, but not all log records.
	//
	// Excluded log records include:
	//
	// - runtime log entries where the specified CLI command does not
	//   operate on a container (or a single container).
	//
	// - proxy log entries which contain kernel boot output from the
	//   guest.
	//
	// - early startup agent log entries.
	Container string

	// Sandbox ID. This is set for most, but not all log records.
	//
	// Excluded log records include:
	//
	// - runtime log entries where the specified CLI command does not
	//   operate on a container (or a single container).
	Sandbox string

	// Line number in Filename this entry refers to
	Line uint64

	// Log entry number (1-indexed)
	Count uint64

	// difference between this record and the previous one
	TimeDelta TimeDelta

	Pid int
}

// Fields lists the names of the fields in a LogEntry.
func (le LogEntry) Fields() []string {
	var fields []string

	v := reflect.ValueOf(le)

	for i := 0; i < v.NumField(); i++ {
		field := v.Type().Field(i).Name
		fields = append(fields, field)
	}

	return fields
}

// Check runs basic checks on the LogEntry to ensure it is valid.
func (le LogEntry) Check(ignoreMissingFields bool) error {
	if le.Filename == "" {
		return fmt.Errorf("missing filename: %+v", le)
	}

	if le.Filename != stdinFile {
		if !strings.HasPrefix(le.Filename, "/") {
			return fmt.Errorf("filename not absolute: %+v", le)
		}
	}

	if le.Line == 0 {
		return fmt.Errorf("missing line number: %+v", le)
	}

	if le.Time == (time.Time{}) {
		return fmt.Errorf("missing timestamp: %+v", le)
	}

	if !ignoreMissingFields {
		if le.Pid == 0 {
			return fmt.Errorf("missing pid: %+v", le)
		}

		if le.Level == "" {
			return fmt.Errorf("missing log level: %+v", le)
		}

		if le.Source == "" {
			return fmt.Errorf("missing component source: %+v", le)
		}

		if le.Name == "" {
			return fmt.Errorf("missing component name: %+v", le)
		}
	}

	if le.Pid < 0 {
		return fmt.Errorf("invalid pid: %+v", le)
	}

	// Note: le.Container and le.Sandbox cannot be checked since they are not
	// present in all entries.

	m := map[string]string{
		"Level":  le.Level,
		"Source": le.Source,
		"Name":   le.Name,
	}

	for k, v := range m {
		fields := strings.Fields(v)
		if len(fields) > 1 {
			return fmt.Errorf("field %q cannot be multi-word: %+v", k, le)
		}
	}

	return nil
}

// LogEntries is a type that encapsulates a list of LogEntry elements.
// Strictly, this type could refer to the slice itself. However, some
// formatting packages (such as those for XML and TOML) require that the
// object to encode is a struct - not an array slice.
type LogEntries struct {
	FormatVersion string
	Entries       []LogEntry
}

// Len is required by sort.Sort.
func (e LogEntries) Len() int {
	return len(e.Entries)
}

// Swap is required by sort.Sort
func (e LogEntries) Swap(i, j int) {
	entries := e.Entries
	entries[i], entries[j] = entries[j], entries[i]
}

// Less is required by sort.Sort. Allows time-based sorting.
func (e LogEntries) Less(i, j int) bool {
	entries := e.Entries

	return entries[i].Time.Before(entries[j].Time)
}
