// Copyright (c) 2015-2021 MinIO, Inc.
//
// This file is part of MinIO Object Storage stack
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.

package cmd

import (
	"time"
)

const (
	sizeLessThan1KiB = iota
	sizeLessThan1MiB
	sizeLessThan10MiB
	sizeLessThan100MiB
	sizeLessThan1GiB
	sizeGreaterThan1GiB
	// Add new entries here

	sizeLastElemMarker
)

// sizeToTag converts a size to a tag.
func sizeToTag(size int64) int {
	switch {
	case size < 1024:
		return sizeLessThan1KiB
	case size < 1024*1024:
		return sizeLessThan1MiB
	case size < 10*1024*1024:
		return sizeLessThan10MiB
	case size < 100*1024*1024:
		return sizeLessThan100MiB
	case size < 1024*1024*1024:
		return sizeLessThan1GiB
	default:
		return sizeGreaterThan1GiB
	}
}

func sizeTagToString(tag int) string {
	switch tag {
	case sizeLessThan1KiB:
		return "LESS_THAN_1_KiB"
	case sizeLessThan1MiB:
		return "LESS_THAN_1_MiB"
	case sizeLessThan10MiB:
		return "LESS_THAN_10_MiB"
	case sizeLessThan100MiB:
		return "LESS_THAN_100_MiB"
	case sizeLessThan1GiB:
		return "LESS_THAN_1_GiB"
	case sizeGreaterThan1GiB:
		return "GREATER_THAN_1_GiB"
	default:
		return "unknown"
	}
}

// AccElem holds information for calculating an average value
type AccElem struct {
	Total int64
	N     int64
}

// add dur to a as a single element.
func (a *AccElem) add(dur time.Duration) {
	a.Total += int64(dur)
	a.N++
}

// merge b into a.
func (a *AccElem) merge(b AccElem) {
	a.N += b.N
	a.Total += b.Total
}

// avg converts total to average.
func (a *AccElem) avg() uint64 {
	if a.N >= 1 && a.Total > 0 {
		return uint64(a.Total / a.N)
	}
	return 0
}

// LastMinuteLatencies keeps track of last minute latencies.
type LastMinuteLatencies struct {
	Totals  [60][sizeLastElemMarker]AccElem
	LastSec int64
}

// Merge safely merges two LastMinuteLatencies structures into one
func (l LastMinuteLatencies) Merge(o LastMinuteLatencies) (merged LastMinuteLatencies) {
	if l.LastSec > o.LastSec {
		o.forwardTo(l.LastSec)
		merged.LastSec = l.LastSec
	} else {
		l.forwardTo(o.LastSec)
		merged.LastSec = o.LastSec
	}

	for i := range merged.Totals {
		for j := range merged.Totals[i] {
			merged.Totals[i][j] = AccElem{
				Total: l.Totals[i][j].Total + o.Totals[i][j].Total,
				N:     l.Totals[i][j].N + o.Totals[i][j].N,
			}
		}
	}
	return merged
}

// Add latency t from object with the specified size.
func (l *LastMinuteLatencies) Add(size int64, t time.Duration) {
	tag := sizeToTag(size)

	// Update...
	sec := time.Now().Unix()
	l.forwardTo(sec)

	winIdx := sec % 60
	l.Totals[winIdx][tag].add(t)

	l.LastSec = sec
}

// GetAvg will return the average for each bucket from the last time minute.
// The number of objects is also included.
func (l *LastMinuteLatencies) GetAvg() [sizeLastElemMarker]AccElem {
	var res [sizeLastElemMarker]AccElem
	sec := time.Now().Unix()
	l.forwardTo(sec)
	for _, elems := range l.Totals[:] {
		for j := range elems {
			res[j].merge(elems[j])
		}
	}
	return res
}

// forwardTo time t, clearing any entries in between.
func (l *LastMinuteLatencies) forwardTo(t int64) {
	if l.LastSec >= t {
		return
	}
	if t-l.LastSec >= 60 {
		l.Totals = [60][sizeLastElemMarker]AccElem{}
		return
	}
	for l.LastSec != t {
		// Clear next element.
		idx := (l.LastSec + 1) % 60
		l.Totals[idx] = [sizeLastElemMarker]AccElem{}
		l.LastSec++
	}
}
