/*
 *
 * Copyright 2017 gRPC 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 stats

import (
	"bytes"
	"fmt"
	"io"
	"math"
	"sort"
	"strconv"
	"time"
)

// Features contains most fields for a benchmark
type Features struct {
	NetworkMode        string
	EnableTrace        bool
	Latency            time.Duration
	Kbps               int
	Mtu                int
	MaxConcurrentCalls int
	ReqSizeBytes       int
	RespSizeBytes      int
	EnableCompressor   bool
}

// String returns the textual output of the Features as string.
func (f Features) String() string {
	return fmt.Sprintf("traceMode_%t-latency_%s-kbps_%#v-MTU_%#v-maxConcurrentCalls_"+
		"%#v-reqSize_%#vB-respSize_%#vB-Compressor_%t", f.EnableTrace,
		f.Latency.String(), f.Kbps, f.Mtu, f.MaxConcurrentCalls, f.ReqSizeBytes, f.RespSizeBytes, f.EnableCompressor)
}

// PartialPrintString can print certain features with different format.
func PartialPrintString(noneEmptyPos []bool, f Features, shared bool) string {
	s := ""
	var (
		prefix, suffix, linker string
		isNetwork              bool
	)
	if shared {
		suffix = "\n"
		linker = ": "
	} else {
		prefix = "-"
		linker = "_"
	}
	if noneEmptyPos[0] {
		s += fmt.Sprintf("%sTrace%s%t%s", prefix, linker, f.EnableCompressor, suffix)
	}
	if shared && f.NetworkMode != "" {
		s += fmt.Sprintf("Network: %s \n", f.NetworkMode)
		isNetwork = true
	}
	if !isNetwork {
		if noneEmptyPos[1] {
			s += fmt.Sprintf("%slatency%s%s%s", prefix, linker, f.Latency.String(), suffix)
		}
		if noneEmptyPos[2] {
			s += fmt.Sprintf("%skbps%s%#v%s", prefix, linker, f.Kbps, suffix)
		}
		if noneEmptyPos[3] {
			s += fmt.Sprintf("%sMTU%s%#v%s", prefix, linker, f.Mtu, suffix)
		}
	}
	if noneEmptyPos[4] {
		s += fmt.Sprintf("%sCallers%s%#v%s", prefix, linker, f.MaxConcurrentCalls, suffix)
	}
	if noneEmptyPos[5] {
		s += fmt.Sprintf("%sreqSize%s%#vB%s", prefix, linker, f.ReqSizeBytes, suffix)
	}
	if noneEmptyPos[6] {
		s += fmt.Sprintf("%srespSize%s%#vB%s", prefix, linker, f.RespSizeBytes, suffix)
	}
	if noneEmptyPos[7] {
		s += fmt.Sprintf("%sCompressor%s%t%s", prefix, linker, f.EnableCompressor, suffix)
	}
	return s
}

type percentLatency struct {
	Percent int
	Value   time.Duration
}

// BenchResults records features and result of a benchmark.
type BenchResults struct {
	RunMode           string
	Features          Features
	Latency           []percentLatency
	Operations        int
	NsPerOp           int64
	AllocedBytesPerOp int64
	AllocsPerOp       int64
	SharedPosion      []bool
}

// SetBenchmarkResult sets features of benchmark and basic results.
func (stats *Stats) SetBenchmarkResult(mode string, features Features, o int, allocdBytes, allocs int64, sharedPos []bool) {
	stats.result.RunMode = mode
	stats.result.Features = features
	stats.result.Operations = o
	stats.result.AllocedBytesPerOp = allocdBytes
	stats.result.AllocsPerOp = allocs
	stats.result.SharedPosion = sharedPos
}

// GetBenchmarkResults returns the result of the benchmark including features and result.
func (stats *Stats) GetBenchmarkResults() BenchResults {
	return stats.result
}

// BenchString output latency stats as the format as time + unit.
func (stats *Stats) BenchString() string {
	stats.maybeUpdate()
	s := stats.result
	res := s.RunMode + "-" + s.Features.String() + ": \n"
	if len(s.Latency) != 0 {
		var statsUnit = s.Latency[0].Value
		var timeUnit = fmt.Sprintf("%v", statsUnit)[1:]
		for i := 1; i < len(s.Latency)-1; i++ {
			res += fmt.Sprintf("%d_Latency: %s %s \t", s.Latency[i].Percent,
				strconv.FormatFloat(float64(s.Latency[i].Value)/float64(statsUnit), 'f', 4, 64), timeUnit)
		}
		res += fmt.Sprintf("Avg latency: %s %s \t",
			strconv.FormatFloat(float64(s.Latency[len(s.Latency)-1].Value)/float64(statsUnit), 'f', 4, 64), timeUnit)
	}
	res += fmt.Sprintf("Count: %v \t", s.Operations)
	res += fmt.Sprintf("%v Bytes/op\t", s.AllocedBytesPerOp)
	res += fmt.Sprintf("%v Allocs/op\t", s.AllocsPerOp)

	return res
}

// Stats is a simple helper for gathering additional statistics like histogram
// during benchmarks. This is not thread safe.
type Stats struct {
	numBuckets int
	unit       time.Duration
	min, max   int64
	histogram  *Histogram

	durations durationSlice
	dirty     bool

	sortLatency bool
	result      BenchResults
}

type durationSlice []time.Duration

// NewStats creates a new Stats instance. If numBuckets is not positive,
// the default value (16) will be used.
func NewStats(numBuckets int) *Stats {
	if numBuckets <= 0 {
		numBuckets = 16
	}
	return &Stats{
		// Use one more bucket for the last unbounded bucket.
		numBuckets: numBuckets + 1,
		durations:  make(durationSlice, 0, 100000),
	}
}

// Add adds an elapsed time per operation to the stats.
func (stats *Stats) Add(d time.Duration) {
	stats.durations = append(stats.durations, d)
	stats.dirty = true
}

// Clear resets the stats, removing all values.
func (stats *Stats) Clear() {
	stats.durations = stats.durations[:0]
	stats.histogram = nil
	stats.dirty = false
	stats.result = BenchResults{}
}

//Sort method for durations
func (a durationSlice) Len() int           { return len(a) }
func (a durationSlice) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
func (a durationSlice) Less(i, j int) bool { return a[i] < a[j] }
func max(a, b int64) int64 {
	if a > b {
		return a
	}
	return b
}

// maybeUpdate updates internal stat data if there was any newly added
// stats since this was updated.
func (stats *Stats) maybeUpdate() {
	if !stats.dirty {
		return
	}

	if stats.sortLatency {
		sort.Sort(stats.durations)
		stats.min = int64(stats.durations[0])
		stats.max = int64(stats.durations[len(stats.durations)-1])
	}

	stats.min = math.MaxInt64
	stats.max = 0
	for _, d := range stats.durations {
		if stats.min > int64(d) {
			stats.min = int64(d)
		}
		if stats.max < int64(d) {
			stats.max = int64(d)
		}
	}

	// Use the largest unit that can represent the minimum time duration.
	stats.unit = time.Nanosecond
	for _, u := range []time.Duration{time.Microsecond, time.Millisecond, time.Second} {
		if stats.min <= int64(u) {
			break
		}
		stats.unit = u
	}

	numBuckets := stats.numBuckets
	if n := int(stats.max - stats.min + 1); n < numBuckets {
		numBuckets = n
	}
	stats.histogram = NewHistogram(HistogramOptions{
		NumBuckets: numBuckets,
		// max-min(lower bound of last bucket) = (1 + growthFactor)^(numBuckets-2) * baseBucketSize.
		GrowthFactor:   math.Pow(float64(stats.max-stats.min), 1/float64(numBuckets-2)) - 1,
		BaseBucketSize: 1.0,
		MinValue:       stats.min})

	for _, d := range stats.durations {
		stats.histogram.Add(int64(d))
	}

	stats.dirty = false

	if stats.durations.Len() != 0 {
		var percentToObserve = []int{50, 90}
		// First data record min unit from the latency result.
		stats.result.Latency = append(stats.result.Latency, percentLatency{Percent: -1, Value: stats.unit})
		for _, position := range percentToObserve {
			stats.result.Latency = append(stats.result.Latency, percentLatency{Percent: position, Value: stats.durations[max(stats.histogram.Count*int64(position)/100-1, 0)]})
		}
		// Last data record the average latency.
		avg := float64(stats.histogram.Sum) / float64(stats.histogram.Count)
		stats.result.Latency = append(stats.result.Latency, percentLatency{Percent: -1, Value: time.Duration(avg)})
	}
}

// SortLatency blocks the output
func (stats *Stats) SortLatency() {
	stats.sortLatency = true
}

// Print writes textual output of the Stats.
func (stats *Stats) Print(w io.Writer) {
	stats.maybeUpdate()
	if stats.histogram == nil {
		fmt.Fprint(w, "Histogram (empty)\n")
	} else {
		fmt.Fprintf(w, "Histogram (unit: %s)\n", fmt.Sprintf("%v", stats.unit)[1:])
		stats.histogram.PrintWithUnit(w, float64(stats.unit))
	}
}

// String returns the textual output of the Stats as string.
func (stats *Stats) String() string {
	var b bytes.Buffer
	stats.Print(&b)
	return b.String()
}
