package sync

import (
	"sync"
	"sync/atomic"
	"time"
)

// Cache is a barebones cache implementation.
type Cache struct {
	// capacity and length have to be the first words
	// in order to be 64-aligned on 32-bit architectures.
	capacity, length uint64 // access atomically
	entries          sync.Map
	pool             sync.Pool
}

// CacheEntry represents an entry on the cache. You can type assert on V.
type CacheEntry struct {
	V          interface{}
	expiration time.Time
}

// NewCache returns a new instance of Cache.
func NewCache(capacity int) Cache {
	return Cache{
		capacity: uint64(capacity),
		pool: sync.Pool{New: func() interface{} {
			return new(CacheEntry)
		}},
	}
}

// Load loads an entry by given key
func (c *Cache) Load(key string) *CacheEntry {
	if mapEntry, ok := c.entries.Load(key); ok {
		entry := mapEntry.(*CacheEntry)
		if c.expired(entry) {
			c.entries.Delete(key)
			return nil
		}
		return entry
	}
	return nil
}

// Store adds an entry for given key and value
func (c *Cache) Store(key string, val interface{}, expiration time.Time) {
	if c.length > c.capacity {
		c.evict()
	}

	poolEntry := c.pool.Get() //nolint: ifshort
	if mapEntry, loaded := c.entries.LoadOrStore(key, poolEntry); loaded {
		entry := mapEntry.(*CacheEntry)
		entry.V = val
		entry.expiration = expiration

		c.pool.Put(poolEntry)
	} else {
		entry := poolEntry.(*CacheEntry)
		entry.V = val
		entry.expiration = expiration

		atomic.AddUint64(&c.length, 1)
	}
}

// Delete removes an entry by given key
func (c *Cache) Delete(key string) bool {
	_, loaded := c.entries.LoadAndDelete(key)

	if loaded {
		atomic.AddUint64(&c.length, ^uint64(0))
	}

	return loaded
}

// evict frees memory from the cache by removing entries that exceeded the cache TTL.
func (c *Cache) evict() {
	c.entries.Range(func(key, mapEntry interface{}) bool {
		entry := mapEntry.(*CacheEntry)
		if c.expired(entry) {
			c.Delete(key.(string))
		}
		return true
	})
}

// expired checks if an entry is expired
func (c *Cache) expired(e *CacheEntry) bool {
	return e.expiration.Before(time.Now())
}
