// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package nomad

import (
	"sort"
	"sync"
	"time"

	"github.com/hashicorp/go-msgpack/codec"
)

// TimeTable is used to associate a Raft index with a timestamp.
// This is used so that we can quickly go from a timestamp to an
// index or visa versa.
type TimeTable struct {
	granularity time.Duration
	limit       time.Duration
	table       []TimeTableEntry
	l           sync.RWMutex
}

// TimeTableEntry is used to track a time and index
type TimeTableEntry struct {
	Index uint64
	Time  time.Time
}

// NewTimeTable creates a new time table which stores entries
// at a given granularity for a maximum limit. The storage space
// required is (limit/granularity)
func NewTimeTable(granularity time.Duration, limit time.Duration) *TimeTable {
	size := limit / granularity
	if size < 1 {
		size = 1
	}
	t := &TimeTable{
		granularity: granularity,
		limit:       limit,
		table:       make([]TimeTableEntry, 1, size),
	}
	return t
}

// Serialize is used to serialize the time table
func (t *TimeTable) Serialize(enc *codec.Encoder) error {
	t.l.RLock()
	defer t.l.RUnlock()
	return enc.Encode(t.table)
}

// Deserialize is used to deserialize the time table
// and restore the state
func (t *TimeTable) Deserialize(dec *codec.Decoder) error {
	// Decode the table
	var table []TimeTableEntry
	if err := dec.Decode(&table); err != nil {
		return err
	}

	// Witness from oldest to newest
	n := len(table)
	for i := n - 1; i >= 0; i-- {
		t.Witness(table[i].Index, table[i].Time)
	}
	return nil
}

// Witness is used to witness a new index and time.
func (t *TimeTable) Witness(index uint64, when time.Time) {
	t.l.Lock()
	defer t.l.Unlock()

	// Ensure monotonic indexes
	if t.table[0].Index > index {
		return
	}

	// Skip if we already have a recent enough entry
	if when.Sub(t.table[0].Time) < t.granularity {
		return
	}

	// Grow the table if we haven't reached the size
	if len(t.table) < cap(t.table) {
		t.table = append(t.table, TimeTableEntry{})
	}

	// Add this entry
	copy(t.table[1:], t.table[:len(t.table)-1])
	t.table[0].Index = index
	t.table[0].Time = when
}

// NearestIndex returns the nearest index older than the given time
func (t *TimeTable) NearestIndex(when time.Time) uint64 {
	t.l.RLock()
	defer t.l.RUnlock()

	n := len(t.table)
	idx := sort.Search(n, func(i int) bool {
		return !t.table[i].Time.After(when)
	})
	if idx < n && idx >= 0 {
		return t.table[idx].Index
	}
	return 0
}

// NearestTime returns the nearest time older than the given index
func (t *TimeTable) NearestTime(index uint64) time.Time {
	t.l.RLock()
	defer t.l.RUnlock()

	n := len(t.table)
	idx := sort.Search(n, func(i int) bool {
		return t.table[i].Index <= index
	})
	if idx < n && idx >= 0 {
		return t.table[idx].Time
	}
	return time.Time{}
}
