package d2cache

import (
	"errors"
	"log"
	"sync"

	"github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface"
)

var _ d2interface.Cache = &Cache{} // Static check to confirm struct conforms to interface

type cacheNode struct {
	next   *cacheNode
	prev   *cacheNode
	key    string
	value  interface{}
	weight int
}

// Cache stores arbitrary data for fast retrieval
type Cache struct {
	head    *cacheNode
	tail    *cacheNode
	lookup  map[string]*cacheNode
	weight  int
	budget  int
	verbose bool
	mutex   sync.Mutex
}

// CreateCache creates an  instance of a Cache
func CreateCache(budget int) d2interface.Cache {
	return &Cache{lookup: make(map[string]*cacheNode), budget: budget}
}

// SetVerbose turns on verbose printing (warnings and stuff)
func (c *Cache) SetVerbose(verbose bool) {
	c.verbose = verbose
}

// GetWeight gets the "weight" of a cache
func (c *Cache) GetWeight() int {
	return c.weight
}

// GetBudget gets the memory budget of a cache
func (c *Cache) GetBudget() int {
	return c.budget
}

// Insert inserts an object into the cache
func (c *Cache) Insert(key string, value interface{}, weight int) error {
	c.mutex.Lock()
	defer c.mutex.Unlock()

	if _, found := c.lookup[key]; found {
		return errors.New("key already exists in Cache")
	}

	node := &cacheNode{
		key:    key,
		value:  value,
		weight: weight,
		next:   c.head,
	}

	if c.head != nil {
		c.head.prev = node
	}

	c.head = node
	if c.tail == nil {
		c.tail = node
	}

	c.lookup[key] = node
	c.weight += node.weight

	for ; c.tail != nil && c.tail != c.head && c.weight > c.budget; c.tail = c.tail.prev {
		c.weight -= c.tail.weight
		c.tail.prev.next = nil

		if c.verbose {
			log.Printf(
				"warning -- Cache is evicting %s (%d) for %s (%d); spare weight is now %d",
				c.tail.key,
				c.tail.weight,
				key,
				weight,
				c.budget-c.weight,
			)
		}

		delete(c.lookup, c.tail.key)
	}

	return nil
}

// Retrieve gets an object out of the cache
func (c *Cache) Retrieve(key string) (interface{}, bool) {
	c.mutex.Lock()
	defer c.mutex.Unlock()

	node, found := c.lookup[key]
	if !found {
		return nil, false
	}

	if node != c.head {
		if node.next != nil {
			node.next.prev = node.prev
		}

		if node.prev != nil {
			node.prev.next = node.next
		}

		if node == c.tail {
			c.tail = c.tail.prev
		}

		node.next = c.head
		node.prev = nil

		if c.head != nil {
			c.head.prev = node
		}

		c.head = node
	}

	return node.value, true
}

// Clear removes all cache entries
func (c *Cache) Clear() {
	c.mutex.Lock()
	defer c.mutex.Unlock()

	c.head = nil
	c.tail = nil
	c.lookup = make(map[string]*cacheNode)
	c.weight = 0
}