/*
 * SPDX-FileCopyrightText: © Hypermode Inc. <hello@hypermode.com>
 * SPDX-License-Identifier: Apache-2.0
 */

package ristretto

import (
	
	
	

	
)

const (
	// lfuSample is the number of items to sample when looking at eviction
	// candidates. 5 seems to be the most optimal number [citation needed].
	lfuSample = 5
)

func newPolicy[ any](,  int64) *defaultPolicy[] {
	return newDefaultPolicy[](, )
}

type defaultPolicy[ any] struct {
	sync.Mutex
	admit    *tinyLFU
	evict    *sampledLFU
	itemsCh  chan []uint64
	stop     chan struct{}
	done     chan struct{}
	isClosed bool
	metrics  *Metrics
}

func newDefaultPolicy[ any](,  int64) *defaultPolicy[] {
	 := &defaultPolicy[]{
		admit:   newTinyLFU(),
		evict:   newSampledLFU(),
		itemsCh: make(chan []uint64, 3),
		stop:    make(chan struct{}),
		done:    make(chan struct{}),
	}
	go .processItems()
	return 
}

func ( *defaultPolicy[]) ( *Metrics) {
	.metrics = 
	.evict.metrics = 
}

type policyPair struct {
	key  uint64
	cost int64
}

func ( *defaultPolicy[]) () {
	for {
		select {
		case  := <-.itemsCh:
			.Lock()
			.admit.Push()
			.Unlock()
		case <-.stop:
			.done <- struct{}{}
			return
		}
	}
}

func ( *defaultPolicy[]) ( []uint64) bool {
	if .isClosed {
		return false
	}

	if len() == 0 {
		return true
	}

	select {
	case .itemsCh <- :
		.metrics.add(keepGets, [0], uint64(len()))
		return true
	default:
		.metrics.add(dropGets, [0], uint64(len()))
		return false
	}
}

// Add decides whether the item with the given key and cost should be accepted by
// the policy. It returns the list of victims that have been evicted and a boolean
// indicating whether the incoming item should be accepted.
func ( *defaultPolicy[]) ( uint64,  int64) ([]*Item[], bool) {
	.Lock()
	defer .Unlock()

	// Cannot add an item bigger than entire cache.
	if  > .evict.getMaxCost() {
		return nil, false
	}

	// No need to go any further if the item is already in the cache.
	if  := .evict.updateIfHas(, );  {
		// An update does not count as an addition, so return false.
		return nil, false
	}

	// If the execution reaches this point, the key doesn't exist in the cache.
	// Calculate the remaining room in the cache (usually bytes).
	 := .evict.roomLeft()
	if  >= 0 {
		// There's enough room in the cache to store the new item without
		// overflowing. Do that now and stop here.
		.evict.add(, )
		.metrics.add(costAdd, , uint64())
		return nil, true
	}

	// incHits is the hit count for the incoming item.
	 := .admit.Estimate()
	// sample is the eviction candidate pool to be filled via random sampling.
	// TODO: perhaps we should use a min heap here. Right now our time
	// complexity is N for finding the min. Min heap should bring it down to
	// O(lg N).
	 := make([]*policyPair, 0, lfuSample)
	// As items are evicted they will be appended to victims.
	 := make([]*Item[], 0)

	// Delete victims until there's enough space or a minKey is found that has
	// more hits than incoming item.
	for ;  < 0;  = .evict.roomLeft() {
		// Fill up empty slots in sample.
		 = .evict.fillSample()

		// Find minimally used item in sample.
		, , ,  := uint64(0), int64(math.MaxInt64), 0, int64(0)
		for ,  := range  {
			// Look up hit count for sample key.
			if  := .admit.Estimate(.key);  <  {
				, , ,  = .key, , , .cost
			}
		}

		// If the incoming item isn't worth keeping in the policy, reject.
		if  <  {
			.metrics.add(rejectSets, , 1)
			return , false
		}

		// Delete the victim from metadata.
		.evict.del()

		// Delete the victim from sample.
		[] = [len()-1]
		 = [:len()-1]
		// Store victim in evicted victims slice.
		 = append(, &Item[]{
			Key:      ,
			Conflict: 0,
			Cost:     ,
		})
	}

	.evict.add(, )
	.metrics.add(costAdd, , uint64())
	return , true
}

func ( *defaultPolicy[]) ( uint64) bool {
	.Lock()
	,  := .evict.keyCosts[]
	.Unlock()
	return 
}

func ( *defaultPolicy[]) ( uint64) {
	.Lock()
	.evict.del()
	.Unlock()
}

func ( *defaultPolicy[]) () int64 {
	.Lock()
	 := .evict.getMaxCost() - .evict.used
	.Unlock()
	return 
}

func ( *defaultPolicy[]) ( uint64,  int64) {
	.Lock()
	.evict.updateIfHas(, )
	.Unlock()
}

func ( *defaultPolicy[]) ( uint64) int64 {
	.Lock()
	if ,  := .evict.keyCosts[];  {
		.Unlock()
		return 
	}
	.Unlock()
	return -1
}

func ( *defaultPolicy[]) () {
	.Lock()
	.admit.clear()
	.evict.clear()
	.Unlock()
}

func ( *defaultPolicy[]) () {
	if .isClosed {
		return
	}

	// Block until the p.processItems goroutine returns.
	.stop <- struct{}{}
	<-.done
	close(.stop)
	close(.done)
	close(.itemsCh)
	.isClosed = true
}

func ( *defaultPolicy[]) () int64 {
	if  == nil || .evict == nil {
		return 0
	}
	return .evict.getMaxCost()
}

func ( *defaultPolicy[]) ( int64) {
	if  == nil || .evict == nil {
		return
	}
	.evict.updateMaxCost()
}

// sampledLFU is an eviction helper storing key-cost pairs.
type sampledLFU struct {
	// NOTE: align maxCost to 64-bit boundary for use with atomic.
	// As per https://golang.org/pkg/sync/atomic/: "On ARM, x86-32,
	// and 32-bit MIPS, it is the caller’s responsibility to arrange
	// for 64-bit alignment of 64-bit words accessed atomically.
	// The first word in a variable or in an allocated struct, array,
	// or slice can be relied upon to be 64-bit aligned."
	maxCost  int64
	used     int64
	metrics  *Metrics
	keyCosts map[uint64]int64
}

func newSampledLFU( int64) *sampledLFU {
	return &sampledLFU{
		keyCosts: make(map[uint64]int64),
		maxCost:  ,
	}
}

func ( *sampledLFU) () int64 {
	return atomic.LoadInt64(&.maxCost)
}

func ( *sampledLFU) ( int64) {
	atomic.StoreInt64(&.maxCost, )
}

func ( *sampledLFU) ( int64) int64 {
	return .getMaxCost() - (.used + )
}

func ( *sampledLFU) ( []*policyPair) []*policyPair {
	if len() >= lfuSample {
		return 
	}
	for ,  := range .keyCosts {
		 = append(, &policyPair{, })
		if len() >= lfuSample {
			return 
		}
	}
	return 
}

func ( *sampledLFU) ( uint64) {
	,  := .keyCosts[]
	if ! {
		return
	}
	.used -= 
	delete(.keyCosts, )
	.metrics.add(costEvict, , uint64())
	.metrics.add(keyEvict, , 1)
}

func ( *sampledLFU) ( uint64,  int64) {
	.keyCosts[] = 
	.used += 
}

func ( *sampledLFU) ( uint64,  int64) bool {
	if ,  := .keyCosts[];  {
		// Update the cost of an existing key, but don't worry about evicting.
		// Evictions will be handled the next time a new item is added.
		.metrics.add(keyUpdate, , 1)
		if  >  {
			 :=  - 
			.metrics.add(costAdd, , ^(uint64() - 1))
		} else if  >  {
			 :=  - 
			.metrics.add(costAdd, , uint64())
		}
		.used +=  - 
		.keyCosts[] = 
		return true
	}
	return false
}

func ( *sampledLFU) () {
	.used = 0
	.keyCosts = make(map[uint64]int64)
}

// tinyLFU is an admission helper that keeps track of access frequency using
// tiny (4-bit) counters in the form of a count-min sketch.
// tinyLFU is NOT thread safe.
type tinyLFU struct {
	freq    *cmSketch
	door    *z.Bloom
	incrs   int64
	resetAt int64
}

func newTinyLFU( int64) *tinyLFU {
	return &tinyLFU{
		freq:    newCmSketch(),
		door:    z.NewBloomFilter(float64(), 0.01),
		resetAt: ,
	}
}

func ( *tinyLFU) ( []uint64) {
	for ,  := range  {
		.Increment()
	}
}

func ( *tinyLFU) ( uint64) int64 {
	 := .freq.Estimate()
	if .door.Has() {
		++
	}
	return 
}

func ( *tinyLFU) ( uint64) {
	// Flip doorkeeper bit if not already done.
	if  := .door.AddIfNotHas(); ! {
		// Increment count-min counter if doorkeeper bit is already set.
		.freq.Increment()
	}
	.incrs++
	if .incrs >= .resetAt {
		.reset()
	}
}

func ( *tinyLFU) () {
	// Zero out incrs.
	.incrs = 0
	// clears doorkeeper bits
	.door.Clear()
	// halves count-min counters
	.freq.Reset()
}

func ( *tinyLFU) () {
	.incrs = 0
	.door.Clear()
	.freq.Clear()
}