package gocache

import (
	"errors"
	"reflect"
	"sync"
	"time"
)

var (
	Debug = false
)

const (
	// NoMaxSize means that the cache has no maximum number of entries in the cache
	// Setting Cache.maxSize to this value also means there will be no eviction
	NoMaxSize = 0

	// NoMaxMemoryUsage means that the cache has no maximum number of entries in the cache
	NoMaxMemoryUsage = 0

	// DefaultMaxSize is the max size set if no max size is specified
	DefaultMaxSize = 100000

	// NoExpiration is the value that must be used as TTL to specify that the given key should never expire
	NoExpiration = -1

	Kilobyte = 1024
	Megabyte = 1024 * Kilobyte
	Gigabyte = 1024 * Megabyte
)

var (
	ErrKeyDoesNotExist       = errors.New("key does not exist")         // Returned when a cache key does not exist
	ErrKeyHasNoExpiration    = errors.New("key has no expiration")      // Returned when a cache key has no expiration
	ErrJanitorAlreadyRunning = errors.New("janitor is already running") // Returned when the janitor has already been started
)

// Cache is the core struct of gocache which contains the data as well as all relevant configuration fields
//
// Do not instantiate this struct directly, use NewCache instead
type Cache struct {
	// maxSize is the maximum amount of entries that can be in the cache at any given time
	// By default, this is set to DefaultMaxSize
	maxSize int

	// maxMemoryUsage is the maximum amount of memory that can be taken up by the cache at any time
	// By default, this is set to NoMaxMemoryUsage, meaning that the default behavior is to not evict
	// based on maximum memory usage
	maxMemoryUsage int

	// evictionPolicy is the eviction policy
	evictionPolicy EvictionPolicy

	// defaultTTL is the default TTL for each entry
	// Defaults to NoExpiration
	defaultTTL time.Duration

	// stats is the object that contains cache statistics/metrics
	stats *Statistics

	// entries is the content of the cache
	entries map[string]*Entry

	// mutex is the lock for making concurrent operations on the cache
	mutex sync.RWMutex

	// head is the cache entry at the head of the cache
	head *Entry

	// tail is the last cache node and also the next entry that will be evicted
	tail *Entry

	// stopJanitor is the channel used to stop the janitor
	stopJanitor chan bool

	// memoryUsage is the approximate memory usage of the cache (dataset only) in bytes
	memoryUsage int

	// forceNilInterfaceOnNilPointer determines whether all Set-like functions should set a value as nil if the
	// interface passed has a nil value but not a nil type.
	//
	// By default, interfaces are only nil when both their type and value is nil.
	// This means that when you pass a pointer to a nil value, the type of the interface
	// will still show as nil, which means that if you don't cast the interface after
	// retrieving it, a nil check will return that the value is not false.
	forceNilInterfaceOnNilPointer bool
}

// MaxSize returns the maximum amount of keys that can be present in the cache before
// new entries trigger the eviction of the tail
func (cache *Cache) MaxSize() int {
	return cache.maxSize
}

// MaxMemoryUsage returns the configured maxMemoryUsage of the cache
func (cache *Cache) MaxMemoryUsage() int {
	return cache.maxMemoryUsage
}

// EvictionPolicy returns the EvictionPolicy of the Cache
func (cache *Cache) EvictionPolicy() EvictionPolicy {
	return cache.evictionPolicy
}

// Stats returns statistics from the cache
func (cache *Cache) Stats() Statistics {
	cache.mutex.RLock()
	stats := Statistics{
		EvictedKeys: cache.stats.EvictedKeys,
		ExpiredKeys: cache.stats.ExpiredKeys,
		Hits:        cache.stats.Hits,
		Misses:      cache.stats.Misses,
	}
	cache.mutex.RUnlock()
	return stats
}

// MemoryUsage returns the current memory usage of the cache's dataset in bytes
// If MaxMemoryUsage is set to NoMaxMemoryUsage, this will return 0
func (cache *Cache) MemoryUsage() int {
	return cache.memoryUsage
}

// WithMaxSize sets the maximum amount of entries that can be in the cache at any given time
// A maxSize of 0 or less means infinite
func (cache *Cache) WithMaxSize(maxSize int) *Cache {
	if maxSize < 0 {
		maxSize = NoMaxSize
	}
	if maxSize != NoMaxSize && cache.Count() == 0 {
		cache.entries = make(map[string]*Entry, maxSize)
	}
	cache.maxSize = maxSize
	return cache
}

// WithMaxMemoryUsage sets the maximum amount of memory that can be used by the cache at any given time
//
// NOTE: This is approximate.
//
// Setting this to NoMaxMemoryUsage will disable eviction by memory usage
func (cache *Cache) WithMaxMemoryUsage(maxMemoryUsageInBytes int) *Cache {
	if maxMemoryUsageInBytes < 0 {
		maxMemoryUsageInBytes = NoMaxMemoryUsage
	}
	cache.maxMemoryUsage = maxMemoryUsageInBytes
	return cache
}

// WithEvictionPolicy sets eviction algorithm.
//
// Defaults to FirstInFirstOut (FIFO)
func (cache *Cache) WithEvictionPolicy(policy EvictionPolicy) *Cache {
	cache.evictionPolicy = policy
	return cache
}

// WithDefaultTTL sets the default TTL for each entry (unless a different TTL is specified using SetWithTTL or SetAllWithTTL)
//
// Defaults to NoExpiration (-1)
func (cache *Cache) WithDefaultTTL(ttl time.Duration) *Cache {
	if ttl > 1 {
		cache.defaultTTL = ttl
	}
	return cache
}

// WithForceNilInterfaceOnNilPointer sets whether all Set-like functions should set a value as nil if the
// interface passed has a nil value but not a nil type.
//
// In Go, an interface is only nil if both its type and value are nil, which means that a nil pointer
// (e.g. (*Struct)(nil)) will retain its attribution to the type, and the unmodified value returned from
// Cache.Get, for instance, would return false when compared with nil if this option is set to false.
//
// We can bypass this by detecting if the interface's value is nil and setting it to nil rather than
// a nil pointer, which will make the value returned from Cache.Get return true when compared with nil.
// This is exactly what passing true to WithForceNilInterfaceOnNilPointer does, and it's also the default behavior.
//
// Alternatively, you may pass false to WithForceNilInterfaceOnNilPointer, which will mean that you'll have
// to cast the value returned from Cache.Get to its original type to check for whether the pointer returned
// is nil or not.
//
// If set to true (default):
//
//	cache := gocache.NewCache().WithForceNilInterfaceOnNilPointer(true)
//	cache.Set("key", (*Struct)(nil))
//	value, _ := cache.Get("key")
//	// the following returns true, because the interface{} (any) was forcefully set to nil
//	if value == nil {}
//	// the following will panic, because the value has been casted to its type (which is nil)
//	if value.(*Struct) == nil {}
//
// If set to false:
//
//	cache := gocache.NewCache().WithForceNilInterfaceOnNilPointer(false)
//	cache.Set("key", (*Struct)(nil))
//	value, _ := cache.Get("key")
//	// the following returns false, because the interface{} (any) returned has a non-nil type (*Struct)
//	if value == nil {}
//	// the following returns true, because the value has been cast to its type
//	if value.(*Struct) == nil {}
//
// In other words, if set to true, you do not need to cast the value returned from the cache to
// check if the value is nil.
//
// Defaults to true
func (cache *Cache) WithForceNilInterfaceOnNilPointer(forceNilInterfaceOnNilPointer bool) *Cache {
	cache.forceNilInterfaceOnNilPointer = forceNilInterfaceOnNilPointer
	return cache
}

// NewCache creates a new Cache
//
// Should be used in conjunction with Cache.WithMaxSize, Cache.WithMaxMemoryUsage and/or Cache.WithEvictionPolicy
//
//	gocache.NewCache().WithMaxSize(10000).WithEvictionPolicy(gocache.LeastRecentlyUsed)
func NewCache() *Cache {
	return &Cache{
		maxSize:                       DefaultMaxSize,
		evictionPolicy:                FirstInFirstOut,
		defaultTTL:                    NoExpiration,
		stats:                         &Statistics{},
		entries:                       make(map[string]*Entry),
		mutex:                         sync.RWMutex{},
		stopJanitor:                   nil,
		forceNilInterfaceOnNilPointer: true,
	}
}

// Set creates or updates a key with a given value
func (cache *Cache) Set(key string, value any) {
	cache.SetWithTTL(key, value, cache.defaultTTL)
}

// SetWithTTL creates or updates a key with a given value and sets an expiration time (-1 is NoExpiration)
//
// The TTL provided must be greater than 0, or NoExpiration (-1). If a negative value that isn't -1 (NoExpiration) is
// provided, the entry will not be created if the key doesn't exist
func (cache *Cache) SetWithTTL(key string, value any, ttl time.Duration) {
	// An interface is only nil if both its value and its type are nil, however, passing a nil pointer as an interface{}
	// means that the interface itself is not nil, because the interface value is nil but not the type.
	if cache.forceNilInterfaceOnNilPointer {
		if value != nil && (reflect.ValueOf(value).Kind() == reflect.Ptr && reflect.ValueOf(value).IsNil()) {
			value = nil
		}
	}
	cache.mutex.Lock()
	entry, ok := cache.get(key)
	if !ok {
		// A negative TTL that isn't -1 (NoExpiration) or 0 is an entry that will expire instantly,
		// so might as well just not create it in the first place
		if ttl != NoExpiration && ttl < 1 {
			cache.mutex.Unlock()
			return
		}
		// Cache entry doesn't exist, so we have to create a new one
		entry = &Entry{
			Key:               key,
			Value:             value,
			RelevantTimestamp: time.Now(),
			next:              cache.head,
		}
		if cache.head == nil {
			cache.tail = entry
		} else {
			cache.head.previous = entry
		}
		cache.head = entry
		cache.entries[key] = entry
		if cache.maxMemoryUsage != NoMaxMemoryUsage {
			cache.memoryUsage += entry.SizeInBytes()
		}
	} else {
		// A negative TTL that isn't -1 (NoExpiration) or 0 is an entry that will expire instantly,
		// so might as well just delete it immediately instead of updating it
		if ttl != NoExpiration && ttl < 1 {
			cache.delete(key)
			cache.mutex.Unlock()
			return
		}
		if cache.maxMemoryUsage != NoMaxMemoryUsage {
			// Subtract the old entry from the cache's memoryUsage
			cache.memoryUsage -= entry.SizeInBytes()
		}
		// Update existing entry's value
		entry.Value = value
		entry.RelevantTimestamp = time.Now()
		if cache.maxMemoryUsage != NoMaxMemoryUsage {
			// Add the memory usage of the new entry to the cache's memoryUsage
			cache.memoryUsage += entry.SizeInBytes()
		}
		// Because we just updated the entry, we need to move it back to HEAD
		cache.moveExistingEntryToHead(entry)
	}
	if ttl != NoExpiration {
		entry.Expiration = time.Now().Add(ttl).UnixNano()
	} else {
		entry.Expiration = NoExpiration
	}
	// If the cache doesn't have a maxSize/maxMemoryUsage, then there's no point
	// checking if we need to evict an entry, so we'll just return now
	if cache.maxSize == NoMaxSize && cache.maxMemoryUsage == NoMaxMemoryUsage {
		cache.mutex.Unlock()
		return
	}
	// If there's a maxSize and the cache has more entries than the maxSize, evict
	if cache.maxSize != NoMaxSize && len(cache.entries) > cache.maxSize {
		cache.evict()
	}
	// If there's a maxMemoryUsage and the memoryUsage is above the maxMemoryUsage, evict
	if cache.maxMemoryUsage != NoMaxMemoryUsage && cache.memoryUsage > cache.maxMemoryUsage {
		for cache.memoryUsage > cache.maxMemoryUsage && len(cache.entries) > 0 {
			cache.evict()
		}
	}
	cache.mutex.Unlock()
}

// SetAll creates or updates multiple values
func (cache *Cache) SetAll(entries map[string]any) {
	cache.SetAllWithTTL(entries, cache.defaultTTL)
}

// SetAllWithTTL creates or updates multiple values
func (cache *Cache) SetAllWithTTL(entries map[string]any, ttl time.Duration) {
	for key, value := range entries {
		cache.SetWithTTL(key, value, ttl)
	}
}

// Get retrieves an entry using the key passed as parameter
// If there is no such entry, the value returned will be nil and the boolean will be false
// If there is an entry, the value returned will be the value cached and the boolean will be true
func (cache *Cache) Get(key string) (any, bool) {
	cache.mutex.Lock()
	entry, ok := cache.get(key)
	if !ok {
		cache.stats.Misses++
		cache.mutex.Unlock()
		return nil, false
	}
	if entry.Expired() {
		cache.stats.ExpiredKeys++
		cache.delete(key)
		cache.mutex.Unlock()
		return nil, false
	}
	cache.stats.Hits++
	if cache.evictionPolicy == LeastRecentlyUsed {
		entry.Accessed()
		if cache.head == entry {
			cache.mutex.Unlock()
			return entry.Value, true
		}
		// Because the eviction policy is LRU, we need to move the entry back to HEAD
		cache.moveExistingEntryToHead(entry)
	}
	cache.mutex.Unlock()
	return entry.Value, true
}

// GetValue retrieves an entry using the key passed as parameter
// Unlike Get, this function only returns the value
func (cache *Cache) GetValue(key string) any {
	value, _ := cache.Get(key)
	return value
}

// GetByKeys retrieves multiple entries using the keys passed as parameter
// All keys are returned in the map, regardless of whether they exist or not, however, entries that do not exist in the
// cache will return nil, meaning that there is no way of determining whether a key genuinely has the value nil, or
// whether it doesn't exist in the cache using only this function.
func (cache *Cache) GetByKeys(keys []string) map[string]any {
	entries := make(map[string]any)
	for _, key := range keys {
		entries[key], _ = cache.Get(key)
	}
	return entries
}

// GetAll retrieves all cache entries
//
// If the eviction policy is LeastRecentlyUsed, note that unlike Get and GetByKeys, this does not update the last access
// timestamp. The reason for this is that since all cache entries will be accessed, updating the last access timestamp
// would provide very little benefit while harming the ability to accurately determine the next key that will be evicted
//
// You should probably avoid using this if you have a lot of entries.
//
// GetKeysByPattern is a good alternative if you want to retrieve entries that you do not have the key for, as it only
// retrieves the keys and does not trigger active eviction and has a parameter for setting a limit to the number of keys
// you wish to retrieve.
func (cache *Cache) GetAll() map[string]any {
	entries := make(map[string]any)
	cache.mutex.Lock()
	for key, entry := range cache.entries {
		if entry.Expired() {
			cache.delete(key)
			continue
		}
		entries[key] = entry.Value
	}
	cache.stats.Hits += uint64(len(entries))
	cache.mutex.Unlock()
	return entries
}

// GetKeysByPattern retrieves a slice of keys that match a given pattern
// If the limit is set to 0, the entire cache will be searched for matching keys.
// If the limit is above 0, the search will stop once the specified number of matching keys have been found.
//
// e.g.
//
//	cache.GetKeysByPattern("*some*", 0) will return all keys containing "some" in them
//	cache.GetKeysByPattern("*some*", 5) will return 5 keys (or less) containing "some" in them
//
// Note that GetKeysByPattern does not trigger active evictions, nor does it count as accessing the entry (if LRU).
// The reason for that behavior is that these two (active eviction and access) only applies when you access the value
// of the cache entry, and this function only returns the keys.
func (cache *Cache) GetKeysByPattern(pattern string, limit int) []string {
	var matchingKeys []string
	cache.mutex.Lock()
	for key, value := range cache.entries {
		if value.Expired() {
			continue
		}
		if MatchPattern(pattern, key) {
			matchingKeys = append(matchingKeys, key)
			if limit > 0 && len(matchingKeys) >= limit {
				break
			}
		}
	}
	cache.mutex.Unlock()
	return matchingKeys
}

// Delete removes a key from the cache
//
// Returns false if the key did not exist.
func (cache *Cache) Delete(key string) bool {
	cache.mutex.Lock()
	ok := cache.delete(key)
	cache.mutex.Unlock()
	return ok
}

// DeleteAll deletes multiple entries based on the keys passed as parameter
//
// Returns the number of keys deleted
func (cache *Cache) DeleteAll(keys []string) int {
	numberOfKeysDeleted := 0
	cache.mutex.Lock()
	for _, key := range keys {
		if cache.delete(key) {
			numberOfKeysDeleted++
		}
	}
	cache.mutex.Unlock()
	return numberOfKeysDeleted
}

// DeleteKeysByPattern deletes all entries matching a given key pattern and returns the number of entries deleted.
//
// Note that DeleteKeysByPattern does not trigger active evictions, nor does it count as accessing the entry (if LRU).
func (cache *Cache) DeleteKeysByPattern(pattern string) int {
	return cache.DeleteAll(cache.GetKeysByPattern(pattern, 0))
}

// Count returns the total amount of entries in the cache, regardless of whether they're expired or not
func (cache *Cache) Count() int {
	cache.mutex.RLock()
	count := len(cache.entries)
	cache.mutex.RUnlock()
	return count
}

// Clear deletes all entries from the cache
func (cache *Cache) Clear() {
	cache.mutex.Lock()
	cache.entries = make(map[string]*Entry)
	cache.memoryUsage = 0
	cache.head = nil
	cache.tail = nil
	cache.mutex.Unlock()
}

// TTL returns the time until the cache entry specified by the key passed as parameter
// will be deleted.
func (cache *Cache) TTL(key string) (time.Duration, error) {
	cache.mutex.RLock()
	entry, ok := cache.get(key)
	cache.mutex.RUnlock()
	if !ok {
		return 0, ErrKeyDoesNotExist
	}
	if entry.Expiration == NoExpiration {
		return 0, ErrKeyHasNoExpiration
	}
	timeUntilExpiration := time.Until(time.Unix(0, entry.Expiration))
	if timeUntilExpiration < 0 {
		// The key has already expired but hasn't been deleted yet.
		// From the client's perspective, this means that the cache entry doesn't exist
		return 0, ErrKeyDoesNotExist
	}
	return timeUntilExpiration, nil
}

// Expire sets a key's expiration time
//
// A TTL of -1 means that the key will never expire
// A TTL of 0 means that the key will expire immediately
// If using LRU, note that this does not reset the position of the key
//
// Returns true if the cache key exists and has had its expiration time altered
func (cache *Cache) Expire(key string, ttl time.Duration) bool {
	entry, ok := cache.get(key)
	if !ok || entry.Expired() {
		return false
	}
	if ttl != NoExpiration {
		entry.Expiration = time.Now().Add(ttl).UnixNano()
	} else {
		entry.Expiration = NoExpiration
	}
	return true
}

// get retrieves an entry using the key passed as parameter, but unlike Get, it doesn't update the access time or
// move the position of the entry to the head
func (cache *Cache) get(key string) (*Entry, bool) {
	entry, ok := cache.entries[key]
	return entry, ok
}

func (cache *Cache) delete(key string) bool {
	entry, ok := cache.entries[key]
	if ok {
		if cache.maxMemoryUsage != NoMaxMemoryUsage {
			cache.memoryUsage -= entry.SizeInBytes()
		}
		cache.removeExistingEntryReferences(entry)
		delete(cache.entries, key)
	}
	return ok
}

// moveExistingEntryToHead replaces the current cache head for an existing entry
func (cache *Cache) moveExistingEntryToHead(entry *Entry) {
	if !(entry == cache.head && entry == cache.tail) {
		cache.removeExistingEntryReferences(entry)
	}
	if entry != cache.head {
		entry.next = cache.head
		entry.previous = nil
		if cache.head != nil {
			cache.head.previous = entry
		}
		cache.head = entry
	}
}

// removeExistingEntryReferences modifies the next and previous reference of an existing entry and re-links
// the next and previous entry accordingly, as well as the cache head or/and the cache tail if necessary.
// Note that it does not remove the entry from the cache, only the references.
func (cache *Cache) removeExistingEntryReferences(entry *Entry) {
	if cache.tail == entry && cache.head == entry {
		cache.tail = nil
		cache.head = nil
	} else if cache.tail == entry {
		cache.tail = cache.tail.previous
	} else if cache.head == entry {
		cache.head = cache.head.next
	}
	if entry.previous != nil {
		entry.previous.next = entry.next
	}
	if entry.next != nil {
		entry.next.previous = entry.previous
	}
	entry.next = nil
	entry.previous = nil
}

// evict removes the tail from the cache
func (cache *Cache) evict() {
	if cache.tail == nil || len(cache.entries) == 0 {
		return
	}
	if cache.tail != nil {
		oldTail := cache.tail
		cache.removeExistingEntryReferences(oldTail)
		delete(cache.entries, oldTail.Key)
		if cache.maxMemoryUsage != NoMaxMemoryUsage {
			cache.memoryUsage -= oldTail.SizeInBytes()
		}
		cache.stats.EvictedKeys++
	}
}
