package cache

import (
	"sync"
	"time"
)

type cacheEntry[V any] struct {
	value     V
	maxExpire time.Time
	expire    time.Time
	created   time.Time
	now       func() time.Time
}

func (ce *cacheEntry[V]) isExpired() bool {
	if ce.now().After(ce.maxExpire) {
		return true
	}

	if ce.now().After(ce.expire) {
		return true
	}

	return false
}

// InMemTTL implements a simple TTLed cache in memory. It is concurrency safe.
type InMemTTL[K comparable, V any] struct {
	sync.Mutex
	now     func() time.Time
	ttl     time.Duration
	maxTTL  time.Duration
	entries map[K]cacheEntry[V]
}

// NewInMemTTL creates a new TTLed cache.
func NewInMemTTL[K comparable, V any](ttl time.Duration, maxTTL time.Duration) *InMemTTL[K, V] {
	return &InMemTTL[K, V]{
		now:    time.Now,
		ttl:    ttl,
		maxTTL: maxTTL,
	}
}

// Get retrieves a single entry, extending it's TTL.
func (c *InMemTTL[K, V]) Get(key K) (V, bool) {
	c.Lock()
	defer c.Unlock()

	var zero V
	if c.entries == nil {
		return zero, false
	}

	ce, found := c.entries[key]
	if !found {
		// not found
		return zero, false
	}
	if ce.isExpired() {
		// expired
		return zero, false
	}

	ce.expire = c.now().Add(c.ttl)
	c.entries[key] = ce

	return ce.value, true
}

// purgeExpire will remove expired entries. It is called by Set.
func (c *InMemTTL[K, V]) purgeExpired() {
	for k, ce := range c.entries {
		if ce.isExpired() {
			delete(c.entries, k)
		}
	}
}

// Set creates or overwrites an entry.
func (c *InMemTTL[K, V]) Set(key K, value V) {
	c.Lock()
	defer c.Unlock()

	if c.entries == nil {
		c.entries = make(map[K]cacheEntry[V], 10)
	}

	now := c.now()
	c.entries[key] = cacheEntry[V]{
		value:     value,
		maxExpire: now.Add(c.maxTTL),
		expire:    now.Add(c.ttl),
		created:   now,
		now: func() time.Time {
			return c.now()
		},
	}

	c.purgeExpired()
}

// Remove removes a single entry from the cache.
func (c *InMemTTL[K, V]) Remove(key K) {
	c.Lock()
	defer c.Unlock()

	delete(c.entries, key)
}

// Purge removes all entries from the cache.
func (c *InMemTTL[K, V]) Purge() {
	c.Lock()
	defer c.Unlock()

	c.entries = make(map[K]cacheEntry[V], 10)
}
