package cachemap

import (
	"context"
	"iter"
	"time"

	"git.tordarus.net/tordarus/cmap"
)

// CacheMap represents a map for caching values for a specific amount of time.
// CacheMap is thread-safe.
type CacheMap[K comparable, V any] struct {
	data                 *cmap.Map[K, cacheMapEntry[V]]
	defaultCacheDuration time.Duration
	autoCleanUpEnabled   bool
}

// New returns a CacheMap which caches its items for the given cache duration (if not specified otherwise).
// It does not collect its own garbage until Get() or CleanUp() is called
func New[K comparable, V any](defaultCacheDuration time.Duration) *CacheMap[K, V] {
	return &CacheMap[K, V]{
		data:                 cmap.New[K, cacheMapEntry[V]](),
		defaultCacheDuration: defaultCacheDuration,
	}
}

// New returns a CacheMap which caches its items for the given cache duration (if not specified otherwise).
// It collects its own garbage periodically. Memory leaks are not possible but the map is locked for the cleanup process
func NewSelfCleaning[K comparable, V any](ctx context.Context, defaultCacheDuration, cleanUpInterval time.Duration) *CacheMap[K, V] {
	cacheMap := New[K, V](defaultCacheDuration)
	cacheMap.AutoCleanUp(ctx, cleanUpInterval)
	return cacheMap
}

// Put puts the given key-value pair into the cache map with the CacheMaps default cache duration
func (cm *CacheMap[K, V]) Put(key K, value V) {
	cm.data.Put(key, cacheMapEntry[V]{
		Value:         value,
		CacheTime:     time.Now(),
		CacheDuration: cm.defaultCacheDuration,
	})
}

// Put puts the given key-value pair into the cache map with the given cache duration
func (cm *CacheMap[K, V]) PutFor(key K, value V, cacheDuration time.Duration) {
	cm.data.Put(key, cacheMapEntry[V]{
		Value:         value,
		CacheTime:     time.Now(),
		CacheDuration: cacheDuration,
	})
}

// Get returns the value associated with the given key and a boolean indicating an existing entry.
// If the entry associated with the key is expired, the key is deleted from the map immediately.
// KeepAlive is called for the entry if it is not expired yet.
func (cm *CacheMap[K, V]) Get(key K) (V, bool) {
	entry, ok := cm.data.GetHas(key)
	if !ok {
		return *new(V), false
	}

	if !entry.IsAlive() {
		cm.data.Delete(key)
		return *new(V), false
	}

	cm.KeepAlive(key)
	return entry.Value, true
}

// KeepAlive restarts the cache duration for the entry associated with the given key.
// This is the same as deleting and re-adding the entry with the same cache duration.
func (cm *CacheMap[K, V]) KeepAlive(key K) {
	entry, ok := cm.data.GetHas(key)
	if ok {
		entry.CacheTime = time.Now()
		cm.data.Put(key, entry)
	}
}

// Iterate returns an iterator for the CacheMap to use in for-range loops
func (cm *CacheMap[K, V]) Iterate() iter.Seq2[K, V] {
	return func(yield func(K, V) bool) {
		for key, value := range cm.data.Iterate() {
			if !yield(key, value.Value) {
				return
			}
		}
	}
}

// CleanUp iterates through the CacheMap and deletes all expired entries.
// The CacheMap is locked during the cleanup process.
func (cm *CacheMap[K, V]) CleanUp() {
	cm.data.Do(func(m map[K]cacheMapEntry[V]) {
		for key, entry := range m {
			if !entry.IsAlive() {
				delete(m, key)
			}
		}
	})
}

// AutoCleanUp periodically calls CleanUp() to ensure that no garbage is accumulating
// over long periods of time without Get calls.
func (cm *CacheMap[K, V]) AutoCleanUp(ctx context.Context, interval time.Duration) {
	if cm.autoCleanUpEnabled {
		return
	}
	cm.autoCleanUpEnabled = true

	go func() {
		ticker := time.NewTicker(interval)
		defer ticker.Stop()
		defer func() { cm.autoCleanUpEnabled = false }()

		for {
			select {
			case <-ticker.C:
				cm.CleanUp()
			case <-ctx.Done():
				return
			}
		}
	}()
}

type cacheMapEntry[T any] struct {
	Value         T
	CacheTime     time.Time
	CacheDuration time.Duration
}

func (e cacheMapEntry[T]) IsAlive() bool {
	return time.Since(e.CacheTime) < e.CacheDuration
}