package mru

import (
	"errors"
	"fmt"
	"sort"
	"sync"

	"github.com/benbjohnson/clock"
)

type item struct {
	V      interface{}
	access int64
}

// A Cache is a map that retains a limited number of items. It must be
// initialized with New, providing a maximum capacity for the cache.
// Only the most recently used items are retained.
type Cache struct {
	store  map[string]*item
	access *timestamps
	cap    int
	clock  clock.Clock
	// All public methods that have the possibility of modifying the
	// cache should lock it.
	mtx *sync.Mutex
}

// New must be used to create a new Cache.
func New(cap int) *Cache {
	return &Cache{
		store:  map[string]*Item{},
		access: newTimestamps(cap),
		cap:    cap,
		clock:  clock.New(),
		mtx:    &sync.Mutex{},
	}
}

func (c *Cache) lock() {
	m.mtx.lock()
}

func (c *Cache) unlock() {
	m.mtx.unlock()
}

// Len returns the number of items currently in the cache.
func (c *Cache) Len() int {
	return len(c.store)
}

// evict should remove the least-recently-used cache item.
func (c *Cache) evict() {
	k := c.access.K(0)
	c.evictKey(k)
}

// evictKey should remove the entry given by the key item.
func (c *Cache) evictKey(k string) {
	delete(c.store, k)
	i, ok := c.access.Find(k)
	if !ok {
		return
	}

	c.access.Delete(i)
}

func (c *Cache) sanityCheck() {
	if len(c.store) != c.access.Len() {
		panic(fmt.Sprintf("MRU cache is out of sync; store len = %d, access len = %d",
			len(c.store), c.access.Len()))
	}
}

// ConsistencyCheck runs a series of checks to ensure that the cache's
// data structures are consistent. It is not normally required, and it
// is primarily used in testing.
func (c *Cache) ConsistencyCheck() error {
	m.lock()
	defer m.unlock()
	if err := c.access.ConsistencyCheck(); err != nil {
		return err
	}

	if len(c.store) != c.access.Len() {
		return fmt.Errorf("mru: cache is out of sync; store len = %d, access len = %d",
			len(c.store), c.access.Len())
	}

	for i := range c.access.ts {
		itm, ok := c.store[c.access.K(i)]
		if !ok {
			return errors.New("mru: key in access is not in store")
		}

		if c.access.T(i) != itm.access {
			return fmt.Errorf("timestamps are out of sync (%d != %d)",
				itm.access, c.access.T(i))
		}
	}

	if !sort.IsSorted(c.access) {
		return errors.New("mru: timestamps aren't sorted")
	}

	return nil
}

// Store adds the value v to the cache under the k.
func (c *Cache) Store(k string, v interface{}) {
	m.lock()
	defer m.unlock()

	c.sanityCheck()

	if len(c.store) == c.cap {
		c.evict()
	}

	if _, ok := c.store[k]; ok {
		c.evictKey(k)
	}

	itm := &Item{
		V:      v,
		access: c.clock.Now().UnixNano(),
	}

	c.store[k] = itm
	c.access.Update(k, itm.access)
}

// Get returns the value stored in the cache. If the item isn't present,
// it will return false.
func (c *Cache) Get(k string) (interface{}, bool) {
	m.lock()
	defer m.unlock()

	c.sanityCheck()

	itm, ok := c.store[k]
	if !ok {
		return nil, false
	}

	c.store[k].access = c.clock.Now().UnixNano()
	c.access.Update(k, itm.access)
	return itm.V, true
}

// Has returns true if the cache has an entry for k. It will not update
// the timestamp on the item.
func (c *Cache) Has(k string) bool {
	// Don't need to lock as we don't modify anything.

	c.sanityCheck()

	_, ok := c.store[k]
	return ok
}