// Copyright 2018 The gVisor Authors.
//
// 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 state

import (
	"bytes"
	"fmt"
	"sort"
	"time"
)

type statEntry struct {
	count uint
	total time.Duration
}

// Stats tracks encode / decode timing.
//
// This currently provides a meaningful String function and no other way to
// extract stats about individual types.
//
// All exported receivers accept nil.
type Stats struct {
	// byType contains a breakdown of time spent by type.
	//
	// This is indexed *directly* by typeID, including zero.
	byType []statEntry

	// stack contains objects in progress.
	stack []typeID

	// names contains type names.
	//
	// This is also indexed *directly* by typeID, including zero, which we
	// hard-code as "state.default". This is only resolved by calling fini
	// on the stats object.
	names []string

	// last is the last start time.
	last time.Time
}

// init initializes statistics.
func (s *Stats) init() {
	s.last = time.Now()
	s.stack = append(s.stack, 0)
}

// fini finalizes statistics.
func (s *Stats) fini(resolve func(id typeID) string) {
	s.done()

	// Resolve all type names.
	s.names = make([]string, len(s.byType))
	s.names[0] = "state.default" // See above.
	for id := typeID(1); int(id) < len(s.names); id++ {
		s.names[id] = resolve(id)
	}
}

// sample adds the samples to the given object.
func (s *Stats) sample(id typeID) {
	now := time.Now()
	if len(s.byType) <= int(id) {
		// Allocate all the missing entries in one fell swoop.
		s.byType = append(s.byType, make([]statEntry, 1+int(id)-len(s.byType))...)
	}
	s.byType[id].total += now.Sub(s.last)
	s.last = now
}

// start starts a sample.
func (s *Stats) start(id typeID) {
	last := s.stack[len(s.stack)-1]
	s.sample(last)
	s.stack = append(s.stack, id)
}

// done finishes the current sample.
func (s *Stats) done() {
	last := s.stack[len(s.stack)-1]
	s.sample(last)
	s.byType[last].count++
	s.stack = s.stack[:len(s.stack)-1]
}

type sliceEntry struct {
	name  string
	entry *statEntry
}

// String returns a table representation of the stats.
func (s *Stats) String() string {
	// Build a list of stat entries.
	ss := make([]sliceEntry, 0, len(s.byType))
	for id := 0; id < len(s.names); id++ {
		ss = append(ss, sliceEntry{
			name:  s.names[id],
			entry: &s.byType[id],
		})
	}

	// Sort by total time (descending).
	sort.Slice(ss, func(i, j int) bool {
		return ss[i].entry.total > ss[j].entry.total
	})

	// Print the stat results.
	var (
		buf   bytes.Buffer
		count uint
		total time.Duration
	)
	buf.WriteString("\n")
	buf.WriteString(fmt.Sprintf("% 16s | % 8s | % 16s | %s\n", "total", "count", "per", "type"))
	buf.WriteString("-----------------+----------+------------------+----------------\n")
	for _, se := range ss {
		if se.entry.count == 0 {
			// Since we store all types linearly, we are not
			// guaranteed that any entry actually has time.
			continue
		}
		count += se.entry.count
		total += se.entry.total
		per := se.entry.total / time.Duration(se.entry.count)
		buf.WriteString(fmt.Sprintf("% 16s | %8d | % 16s | %s\n",
			se.entry.total, se.entry.count, per, se.name))
	}
	buf.WriteString("-----------------+----------+------------------+----------------\n")
	buf.WriteString(fmt.Sprintf("% 16s | % 8d | % 16s | [all]",
		total, count, total/time.Duration(count)))
	return buf.String()
}
