// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License"). You may
// not use this file except in compliance with the License. A copy of the
// License is located at
//
//	http://aws.amazon.com/apache2.0/
//
// or in the "license" file accompanying this file. This file is distributed
// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
// express or implied. See the License for the specific language governing
// permissions and limitations under the License.

package async

import (
	"container/list"
	"sync"
	"time"
)

type Cache interface {
	// Get fetches a value from cache, returns nil, false on miss
	Get(key string) (Value, bool)
	// Set sets a value in cache. overrites any existing value
	Set(key string, value Value)
	// Delete deletes the value from the cache
	Delete(key string)
}

// Creates an LRUCache with maximum size, ttl for items.
func NewLRUCache(size int, ttl time.Duration) Cache {
	lru := &lruCache{size: size,
		ttl:       ttl,
		cache:     make(map[string]*entry),
		evictList: list.New(),
	}
	return lru
}

type Value interface{}

type entry struct {
	value Value
	added time.Time
}

type lruCache struct {
	sync.Mutex
	cache     map[string]*entry
	evictList *list.List
	size      int
	ttl       time.Duration
}

// Get returns the value associated with the key
func (lru *lruCache) Get(key string) (Value, bool) {
	lru.Lock()
	defer lru.Unlock()

	entry, ok := lru.cache[key]

	if !ok {
		return nil, false
	}

	ok = lru.evictStale(entry, key)
	if !ok {
		return nil, false
	}

	lru.updateAccessed(key)

	return entry.value, true
}

// Set sets the key-value pair in the cache
func (lru *lruCache) Set(key string, value Value) {
	lru.Lock()
	defer lru.Unlock()

	lru.cache[key] = &entry{value: value, added: time.Now()}

	// Remove from the evict list if an entry already existed
	lru.removeFromEvictList(key)

	lru.evictList.PushBack(key)
	lru.purgeSize()
}

// Delete removes the entry associated with the key from cache
func (lru *lruCache) Delete(key string) {
	lru.Lock()
	defer lru.Unlock()

	lru.removeFromEvictList(key)
	delete(lru.cache, key)
}

func (lru *lruCache) updateAccessed(key string) {
	// update evict list
	for elem := lru.evictList.Front(); elem != nil; elem = elem.Next() {
		if elem.Value == key {
			lru.evictList.MoveToBack(elem)
			break
		}
	}
}

func (lru *lruCache) removeFromEvictList(key string) {
	var next *list.Element
	for element := lru.evictList.Front(); element != nil; element = next {
		next = element.Next()
		if element.Value == key {
			lru.evictList.Remove(element)
			break
		}
	}

}

func (lru *lruCache) evictStale(entry *entry, key string) bool {
	if time.Since(entry.added) >= lru.ttl {
		// remove from evict list
		for elem := lru.evictList.Front(); elem != nil; elem = elem.Next() {
			if elem.Value == key {
				lru.evictList.Remove(elem)
				break
			}
		}

		// delete from cache
		delete(lru.cache, key)

		return false
	}

	return true
}

func (lru *lruCache) purgeSize() {
	for elem := lru.evictList.Front(); elem != nil; elem = elem.Next() {
		if len(lru.cache) <= lru.size {
			break
		}
		key := elem.Value.(string)
		// remove from list
		lru.evictList.Remove(elem)

		// delete from cache
		delete(lru.cache, key)
	}
}
