// Copyright 2018 Google Inc. All rights reserved.
//
// Licensed 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 status tracks actions run by various tools, combining the counts
// (total actions, currently running, started, finished), and giving that to
// multiple outputs.
package status

import (
	"sync"
	"time"
)

// Action describes an action taken (or as Ninja calls them, Edges).
type Action struct {
	// Description is a shorter, more readable form of the command, meant
	// for users. It's optional, but one of either Description or Command
	// should be set.
	Description string

	// Outputs is the (optional) list of outputs. Usually these are files,
	// but they can be any string.
	Outputs []string

	// Inputs is the (optional) list of inputs. Usually these are files,
	// but they can be any string.
	Inputs []string

	// Command is the actual command line executed to perform the action.
	// It's optional, but one of either Description or Command should be
	// set.
	Command string

	// ChangedInputs is the (optional) list of inputs that have changed
	// since last time this action was run.
	ChangedInputs []string
}

// ActionResult describes the result of running an Action.
type ActionResult struct {
	// Action is a pointer to the original Action struct.
	*Action

	// Output is the output produced by the command (usually stdout&stderr
	// for Actions that run commands)
	Output string

	// Error is nil if the Action succeeded, or set to an error if it
	// failed.
	Error error

	Stats ActionResultStats
}

type ActionResultStats struct {
	// Number of milliseconds spent executing in user mode
	UserTime uint32

	// Number of milliseconds spent executing in kernel mode
	SystemTime uint32

	// Max resident set size in kB
	MaxRssKB uint64

	// Minor page faults
	MinorPageFaults uint64

	// Major page faults
	MajorPageFaults uint64

	// IO input in kB
	IOInputKB uint64

	// IO output in kB
	IOOutputKB uint64

	// Voluntary context switches
	VoluntaryContextSwitches uint64

	// Involuntary context switches
	InvoluntaryContextSwitches uint64

	Tags string
}

// Counts describes the number of actions in each state
type Counts struct {
	// TotalActions is the total number of expected changes.  This can
	// generally change up or down during a build, but it should never go
	// below the number of StartedActions
	TotalActions int

	// RunningActions are the number of actions that are currently running
	// -- the number that have called StartAction, but not FinishAction.
	RunningActions int

	// StartedActions are the number of actions that have been started with
	// StartAction.
	StartedActions int

	// FinishedActions are the number of actions that have been finished
	// with FinishAction.
	FinishedActions int

	EstimatedTime time.Time
}

// ToolStatus is the interface used by tools to report on their Actions, and to
// present other information through a set of messaging functions.
type ToolStatus interface {
	// SetTotalActions sets the expected total number of actions that will
	// be started by this tool.
	//
	// This call be will ignored if it sets a number that is less than the
	// current number of started actions.
	SetTotalActions(total int)
	SetEstimatedTime(estimatedTime time.Time)

	// StartAction specifies that the associated action has been started by
	// the tool.
	//
	// A specific *Action should not be specified to StartAction more than
	// once, even if the previous action has already been finished, and the
	// contents rewritten.
	//
	// Do not re-use *Actions between different ToolStatus interfaces
	// either.
	StartAction(action *Action)

	// FinishAction specifies the result of a particular Action.
	//
	// The *Action embedded in the ActionResult structure must have already
	// been passed to StartAction (on this interface).
	//
	// Do not call FinishAction twice for the same *Action.
	FinishAction(result ActionResult)

	// Verbose takes a non-important message that is never printed to the
	// screen, but is in the verbose build log, etc
	Verbose(msg string)
	// Status takes a less important message that may be printed to the
	// screen, but overwritten by another status message. The full message
	// will still appear in the verbose build log.
	Status(msg string)
	// Print takes an message and displays it to the screen and other
	// output logs, etc.
	Print(msg string)
	// Error is similar to Print, but treats it similarly to a failed
	// action, showing it in the error logs, etc.
	Error(msg string)

	// Finish marks the end of all Actions being run by this tool.
	//
	// SetTotalEdges, StartAction, and FinishAction should not be called
	// after Finish.
	Finish()
}

// MsgLevel specifies the importance of a particular log message. See the
// descriptions in ToolStatus: Verbose, Status, Print, Error.
type MsgLevel int

const (
	VerboseLvl MsgLevel = iota
	StatusLvl
	PrintLvl
	ErrorLvl
)

func (l MsgLevel) Prefix() string {
	switch l {
	case VerboseLvl:
		return "verbose: "
	case StatusLvl:
		return "status: "
	case PrintLvl:
		return ""
	case ErrorLvl:
		return "error: "
	default:
		panic("Unknown message level")
	}
}

// StatusOutput is the interface used to get status information as a Status
// output.
//
// All of the functions here are guaranteed to be called by Status while
// holding it's internal lock, so it's safe to assume a single caller at any
// time, and that the ordering of calls will be correct. It is not safe to call
// back into the Status, or one of its ToolStatus interfaces.
type StatusOutput interface {
	// StartAction will be called once every time ToolStatus.StartAction is
	// called. counts will include the current counters across all
	// ToolStatus instances, including ones that have been finished.
	StartAction(action *Action, counts Counts)

	// FinishAction will be called once every time ToolStatus.FinishAction
	// is called. counts will include the current counters across all
	// ToolStatus instances, including ones that have been finished.
	FinishAction(result ActionResult, counts Counts)

	// Message is the equivalent of ToolStatus.Verbose/Status/Print/Error,
	// but the level is specified as an argument.
	Message(level MsgLevel, msg string)

	// Flush is called when your outputs should be flushed / closed. No
	// output is expected after this call.
	Flush()

	// Write lets StatusOutput implement io.Writer
	Write(p []byte) (n int, err error)
}

// Status is the multiplexer / accumulator between ToolStatus instances (via
// StartTool) and StatusOutputs (via AddOutput). There's generally one of these
// per build process (though tools like multiproduct_kati may have multiple
// independent versions).
type Status struct {
	counts  Counts
	outputs []StatusOutput

	// Protects counts and outputs, and allows each output to
	// expect only a single caller at a time.
	lock sync.Mutex
}

// AddOutput attaches an output to this object. It's generally expected that an
// output is attached to a single Status instance.
func (s *Status) AddOutput(output StatusOutput) {
	if output == nil {
		return
	}

	s.lock.Lock()
	defer s.lock.Unlock()

	s.outputs = append(s.outputs, output)
}

// StartTool returns a new ToolStatus instance to report the status of a tool.
func (s *Status) StartTool() ToolStatus {
	return &toolStatus{
		status: s,
	}
}

// Finish will call Flush on all the outputs, generally flushing or closing all
// of their outputs. Do not call any other functions on this instance or any
// associated ToolStatus instances after this has been called.
func (s *Status) Finish() {
	s.lock.Lock()
	defer s.lock.Unlock()

	for _, o := range s.outputs {
		o.Flush()
	}
}

func (s *Status) updateTotalActions(diff int) {
	s.lock.Lock()
	defer s.lock.Unlock()

	s.counts.TotalActions += diff
}

func (s *Status) SetEstimatedTime(estimatedTime time.Time) {
	s.lock.Lock()
	defer s.lock.Unlock()

	s.counts.EstimatedTime = estimatedTime
}

func (s *Status) startAction(action *Action) {
	s.lock.Lock()
	defer s.lock.Unlock()

	s.counts.RunningActions += 1
	s.counts.StartedActions += 1

	for _, o := range s.outputs {
		o.StartAction(action, s.counts)
	}
}

func (s *Status) finishAction(result ActionResult) {
	s.lock.Lock()
	defer s.lock.Unlock()

	s.counts.RunningActions -= 1
	s.counts.FinishedActions += 1

	for _, o := range s.outputs {
		o.FinishAction(result, s.counts)
	}
}

func (s *Status) message(level MsgLevel, msg string) {
	s.lock.Lock()
	defer s.lock.Unlock()

	for _, o := range s.outputs {
		o.Message(level, msg)
	}
}

func (s *Status) Status(msg string) {
	s.message(StatusLvl, msg)
}

type toolStatus struct {
	status *Status

	counts Counts
	// Protects counts
	lock sync.Mutex
}

var _ ToolStatus = (*toolStatus)(nil)

func (d *toolStatus) SetTotalActions(total int) {
	diff := 0

	d.lock.Lock()
	if total >= d.counts.StartedActions && total != d.counts.TotalActions {
		diff = total - d.counts.TotalActions
		d.counts.TotalActions = total
	}
	d.lock.Unlock()

	if diff != 0 {
		d.status.updateTotalActions(diff)
	}
}

func (d *toolStatus) SetEstimatedTime(estimatedTime time.Time) {
	d.status.SetEstimatedTime(estimatedTime)
}

func (d *toolStatus) StartAction(action *Action) {
	totalDiff := 0

	d.lock.Lock()
	d.counts.RunningActions += 1
	d.counts.StartedActions += 1

	if d.counts.StartedActions > d.counts.TotalActions {
		totalDiff = d.counts.StartedActions - d.counts.TotalActions
		d.counts.TotalActions = d.counts.StartedActions
	}
	d.lock.Unlock()

	if totalDiff != 0 {
		d.status.updateTotalActions(totalDiff)
	}
	d.status.startAction(action)
}

func (d *toolStatus) FinishAction(result ActionResult) {
	d.lock.Lock()
	d.counts.RunningActions -= 1
	d.counts.FinishedActions += 1
	d.lock.Unlock()

	d.status.finishAction(result)
}

func (d *toolStatus) Verbose(msg string) {
	d.status.message(VerboseLvl, msg)
}
func (d *toolStatus) Status(msg string) {
	d.status.message(StatusLvl, msg)
}
func (d *toolStatus) Print(msg string) {
	d.status.message(PrintLvl, msg)
}
func (d *toolStatus) Error(msg string) {
	d.status.message(ErrorLvl, msg)
}

func (d *toolStatus) Finish() {
	d.lock.Lock()
	defer d.lock.Unlock()

	if d.counts.TotalActions != d.counts.StartedActions {
		d.status.updateTotalActions(d.counts.StartedActions - d.counts.TotalActions)
	}

	// TODO: update status to correct running/finished edges?
	d.counts.RunningActions = 0
	d.counts.TotalActions = d.counts.StartedActions
}
