package ttl

import (
	"sync"
	"time"
	_ "unsafe"

	"codeberg.org/gruf/go-maps"
)

// Entry represents an item in the cache, with it's currently calculated Expiry time.
type Entry[Key comparable, Value any] struct {
	Key    Key
	Value  Value
	Expiry uint64
}

// Cache is the underlying TTLCache implementation, providing both the base Cache interface and unsafe access to underlying map to allow flexibility in building your own.
type Cache[Key comparable, Value any] struct {
	// TTL is the cache item TTL.
	TTL time.Duration

	// Evict is the hook that is called when an item is evicted from the cache.
	Evict func(Key, Value)

	// Invalid is the hook that is called when an item's data in the cache is invalidated, includes Add/Set.
	Invalid func(Key, Value)

	// Cache is the underlying hashmap used for this cache.
	Cache maps.LRUMap[Key, *Entry[Key, Value]]

	// stop is the eviction routine cancel func.
	stop func()

	// pool is a memory pool of entry objects.
	pool []*Entry[Key, Value]

	// Embedded mutex.
	sync.Mutex
}

// New returns a new initialized Cache with given initial length, maximum capacity and item TTL.
func New[K comparable, V any](len, cap int, ttl time.Duration) *Cache[K, V] {
	c := new(Cache[K, V])
	c.Init(len, cap, ttl)
	return c
}

// Init will initialize this cache with given initial length, maximum capacity and item TTL.
func (c *Cache[K, V]) Init(len, cap int, ttl time.Duration) {
	if ttl <= 0 {
		// Default duration
		ttl = time.Second * 5
	}
	c.TTL = ttl
	c.SetEvictionCallback(nil)
	c.SetInvalidateCallback(nil)
	c.Cache.Init(len, cap)
}

// Start: implements cache.Cache's Start().
func (c *Cache[K, V]) Start(freq time.Duration) (ok bool) {
	// Nothing to start
	if freq <= 0 {
		return false
	}

	// Safely start
	c.Lock()

	if ok = (c.stop == nil); ok {
		// Not yet running, schedule us
		c.stop = schedule(c.Sweep, freq)
	}

	// Done with lock
	c.Unlock()

	return
}

// Stop: implements cache.Cache's Stop().
func (c *Cache[K, V]) Stop() (ok bool) {
	// Safely stop
	c.Lock()

	if ok = (c.stop != nil); ok {
		// We're running, cancel evicts
		c.stop()
		c.stop = nil
	}

	// Done with lock
	c.Unlock()

	return
}

// Sweep attempts to evict expired items (with callback!) from cache.
func (c *Cache[K, V]) Sweep(_ time.Time) {
	var (
		// evicted key-values.
		kvs []kv[K, V]

		// hook func ptrs.
		evict func(K, V)

		// get current nanoseconds.
		now = runtime_nanotime()
	)

	c.locked(func() {
		if c.TTL <= 0 {
			// sweep is
			// disabled
			return
		}

		// Sentinel value
		after := -1

		// The cache will be ordered by expiry date, we iterate until we reach the index of
		// the youngest item that hsa expired, as all succeeding items will also be expired.
		c.Cache.RangeIf(0, c.Cache.Len(), func(i int, _ K, item *Entry[K, V]) bool {
			if now > item.Expiry {
				after = i

				// evict all older items
				// than this (inclusive)
				return false
			}

			// cont. loop.
			return true
		})

		if after == -1 {
			// No Truncation needed
			return
		}

		// Set hook func ptr.
		evict = c.Evict

		// Truncate determined size.
		sz := c.Cache.Len() - after
		kvs = c.truncate(sz, evict)
	})

	if evict != nil {
		for x := range kvs {
			// Pass to eviction hook.
			evict(kvs[x].K, kvs[x].V)
		}
	}
}

// SetEvictionCallback: implements cache.Cache's SetEvictionCallback().
func (c *Cache[K, V]) SetEvictionCallback(hook func(K, V)) {
	c.locked(func() {
		c.Evict = hook
	})
}

// SetInvalidateCallback: implements cache.Cache's SetInvalidateCallback().
func (c *Cache[K, V]) SetInvalidateCallback(hook func(K, V)) {
	c.locked(func() {
		c.Invalid = hook
	})
}

// SetTTL: implements cache.Cache's SetTTL().
func (c *Cache[K, V]) SetTTL(ttl time.Duration, update bool) {
	c.locked(func() {
		// Set updated TTL
		diff := ttl - c.TTL
		c.TTL = ttl

		if update {
			// Update existing cache entries with new expiry time
			c.Cache.Range(0, c.Cache.Len(), func(i int, _ K, item *Entry[K, V]) {
				item.Expiry += uint64(diff)
			})
		}
	})
}

// Get: implements cache.Cache's Get().
func (c *Cache[K, V]) Get(key K) (V, bool) {
	var (
		// did exist in cache?
		ok bool

		// cached value.
		v V
	)

	c.locked(func() {
		var item *Entry[K, V]

		// Check for item in cache
		item, ok = c.Cache.Get(key)
		if !ok {
			return
		}

		// Update fetched's expiry
		item.Expiry = c.expiry()

		// Set value.
		v = item.Value
	})

	return v, ok
}

// Add: implements cache.Cache's Add().
func (c *Cache[K, V]) Add(key K, value V) bool {
	var (
		// did exist in cache?
		ok bool

		// was entry evicted?
		ev bool

		// evicted key values.
		evcK K
		evcV V

		// hook func ptrs.
		evict func(K, V)
	)

	c.locked(func() {
		// Check if in cache.
		ok = c.Cache.Has(key)
		if ok {
			return
		}

		// Alloc new entry.
		new := c.alloc()
		new.Expiry = c.expiry()
		new.Key = key
		new.Value = value

		// Add new entry to cache and catched any evicted item.
		c.Cache.SetWithHook(key, new, func(_ K, item *Entry[K, V]) {
			evcK = item.Key
			evcV = item.Value
			ev = true
			c.free(item)
		})

		// Set hook func ptr.
		evict = c.Evict
	})

	if ev && evict != nil {
		// Pass to eviction hook.
		evict(evcK, evcV)
	}

	return !ok
}

// Set: implements cache.Cache's Set().
func (c *Cache[K, V]) Set(key K, value V) {
	var (
		// did exist in cache?
		ok bool

		// was entry evicted?
		ev bool

		// old value.
		oldV V

		// evicted key values.
		evcK K
		evcV V

		// hook func ptrs.
		invalid func(K, V)
		evict   func(K, V)
	)

	c.locked(func() {
		var item *Entry[K, V]

		// Check for item in cache
		item, ok = c.Cache.Get(key)

		if ok {
			// Set old value.
			oldV = item.Value

			// Update the existing item.
			item.Expiry = c.expiry()
			item.Value = value
		} else {
			// Alloc new entry.
			new := c.alloc()
			new.Expiry = c.expiry()
			new.Key = key
			new.Value = value

			// Add new entry to cache and catched any evicted item.
			c.Cache.SetWithHook(key, new, func(_ K, item *Entry[K, V]) {
				evcK = item.Key
				evcV = item.Value
				ev = true
				c.free(item)
			})
		}

		// Set hook func ptrs.
		invalid = c.Invalid
		evict = c.Evict
	})

	if ok && invalid != nil {
		// Pass to invalidate hook.
		invalid(key, oldV)
	}

	if ev && evict != nil {
		// Pass to eviction hook.
		evict(evcK, evcV)
	}
}

// CAS: implements cache.Cache's CAS().
func (c *Cache[K, V]) CAS(key K, old V, new V, cmp func(V, V) bool) bool {
	var (
		// did exist in cache?
		ok bool

		// swapped value.
		oldV V

		// hook func ptrs.
		invalid func(K, V)
	)

	c.locked(func() {
		var item *Entry[K, V]

		// Check for item in cache
		item, ok = c.Cache.Get(key)
		if !ok {
			return
		}

		// Perform the comparison
		if !cmp(old, item.Value) {
			return
		}

		// Set old value.
		oldV = item.Value

		// Update value + expiry.
		item.Expiry = c.expiry()
		item.Value = new

		// Set hook func ptr.
		invalid = c.Invalid
	})

	if ok && invalid != nil {
		// Pass to invalidate hook.
		invalid(key, oldV)
	}

	return ok
}

// Swap: implements cache.Cache's Swap().
func (c *Cache[K, V]) Swap(key K, swp V) V {
	var (
		// did exist in cache?
		ok bool

		// swapped value.
		oldV V

		// hook func ptrs.
		invalid func(K, V)
	)

	c.locked(func() {
		var item *Entry[K, V]

		// Check for item in cache
		item, ok = c.Cache.Get(key)
		if !ok {
			return
		}

		// Set old value.
		oldV = item.Value

		// Update value + expiry.
		item.Expiry = c.expiry()
		item.Value = swp

		// Set hook func ptr.
		invalid = c.Invalid
	})

	if ok && invalid != nil {
		// Pass to invalidate hook.
		invalid(key, oldV)
	}

	return oldV
}

// Has: implements cache.Cache's Has().
func (c *Cache[K, V]) Has(key K) (ok bool) {
	c.locked(func() {
		ok = c.Cache.Has(key)
	})
	return
}

// Invalidate: implements cache.Cache's Invalidate().
func (c *Cache[K, V]) Invalidate(key K) (ok bool) {
	var (
		// old value.
		oldV V

		// hook func ptrs.
		invalid func(K, V)
	)

	c.locked(func() {
		var item *Entry[K, V]

		// Check for item in cache
		item, ok = c.Cache.Get(key)
		if !ok {
			return
		}

		// Set old value.
		oldV = item.Value

		// Remove from cache map
		_ = c.Cache.Delete(key)

		// Free entry
		c.free(item)

		// Set hook func ptrs.
		invalid = c.Invalid
	})

	if ok && invalid != nil {
		// Pass to invalidate hook.
		invalid(key, oldV)
	}

	return
}

// InvalidateAll: implements cache.Cache's InvalidateAll().
func (c *Cache[K, V]) InvalidateAll(keys ...K) (ok bool) {
	var (
		// invalidated kvs.
		kvs []kv[K, V]

		// hook func ptrs.
		invalid func(K, V)
	)

	// Allocate a slice for invalidated.
	kvs = make([]kv[K, V], 0, len(keys))

	c.locked(func() {
		for x := range keys {
			var item *Entry[K, V]

			// Check for item in cache
			item, ok = c.Cache.Get(keys[x])
			if !ok {
				continue
			}

			// Append this old value to slice
			kvs = append(kvs, kv[K, V]{
				K: keys[x],
				V: item.Value,
			})

			// Remove from cache map
			_ = c.Cache.Delete(keys[x])

			// Free entry
			c.free(item)
		}

		// Set hook func ptrs.
		invalid = c.Invalid
	})

	if invalid != nil {
		for x := range kvs {
			// Pass to invalidate hook.
			invalid(kvs[x].K, kvs[x].V)
		}
	}

	return
}

// Clear: implements cache.Cache's Clear().
func (c *Cache[K, V]) Clear() {
	var (
		// deleted key-values.
		kvs []kv[K, V]

		// hook func ptrs.
		invalid func(K, V)
	)

	c.locked(func() {
		// Set hook func ptr.
		invalid = c.Invalid

		// Truncate the entire cache length.
		kvs = c.truncate(c.Cache.Len(), invalid)
	})

	if invalid != nil {
		for x := range kvs {
			// Pass to invalidate hook.
			invalid(kvs[x].K, kvs[x].V)
		}
	}
}

// Len: implements cache.Cache's Len().
func (c *Cache[K, V]) Len() (l int) {
	c.locked(func() { l = c.Cache.Len() })
	return
}

// Cap: implements cache.Cache's Cap().
func (c *Cache[K, V]) Cap() (l int) {
	c.locked(func() { l = c.Cache.Cap() })
	return
}

// locked performs given function within mutex lock (NOTE: UNLOCK IS NOT DEFERRED).
func (c *Cache[K, V]) locked(fn func()) {
	c.Lock()
	fn()
	c.Unlock()
}

// truncate will truncate the cache by given size, returning deleted items.
func (c *Cache[K, V]) truncate(sz int, hook func(K, V)) []kv[K, V] {
	if hook == nil {
		// No hook to execute, simply free all truncated entries.
		c.Cache.Truncate(sz, func(_ K, e *Entry[K, V]) { c.free(e) })
		return nil
	}

	// Allocate a slice for deleted k-v pairs.
	deleted := make([]kv[K, V], 0, sz)

	c.Cache.Truncate(sz, func(_ K, item *Entry[K, V]) {
		// Store key-value pair for later access.
		deleted = append(deleted, kv[K, V]{
			K: item.Key,
			V: item.Value,
		})

		// Free entry.
		c.free(item)
	})

	return deleted
}

// alloc will acquire cache entry from pool, or allocate new.
func (c *Cache[K, V]) alloc() *Entry[K, V] {
	if len(c.pool) == 0 {
		return &Entry[K, V]{}
	}
	idx := len(c.pool) - 1
	e := c.pool[idx]
	c.pool = c.pool[:idx]
	return e
}

// clone allocates a new Entry and copies all info from passed Entry.
func (c *Cache[K, V]) clone(e *Entry[K, V]) *Entry[K, V] {
	e2 := c.alloc()
	e2.Key = e.Key
	e2.Value = e.Value
	e2.Expiry = e.Expiry
	return e2
}

// free will reset entry fields and place back in pool.
func (c *Cache[K, V]) free(e *Entry[K, V]) {
	var (
		zk K
		zv V
	)
	e.Expiry = 0
	e.Key = zk
	e.Value = zv
	c.pool = append(c.pool, e)
}

//go:linkname runtime_nanotime runtime.nanotime
func runtime_nanotime() uint64

// expiry returns an the next expiry time to use for an entry,
// which is equivalent to time.Now().Add(ttl), or zero if disabled.
func (c *Cache[K, V]) expiry() uint64 {
	if ttl := c.TTL; ttl > 0 {
		return runtime_nanotime() +
			uint64(c.TTL)
	}
	return 0
}

type kv[K comparable, V any] struct {
	K K
	V V
}
