// Package memstore offers an in-memory store implementation for throttled.
package memstore // import "github.com/throttled/throttled/store/memstore"

import (
	"sync"
	"sync/atomic"
	"time"

	"github.com/hashicorp/golang-lru"
)

// MemStore is an in-memory store implementation for throttled. It
// supports evicting the least recently used keys to control memory
// usage. It is stored in memory in the current process and thus
// doesn't share state with other rate limiters.
type MemStore struct {
	sync.RWMutex
	keys *lru.Cache
	m    map[string]*int64
}

// New initializes a Store. If maxKeys > 0, the number of different
// keys is restricted to the specified amount. In this case, it uses
// an LRU algorithm to evict older keys to make room for newer
// ones. If maxKeys <= 0, there is no limit on the number of keys,
// which may use an unbounded amount of memory.
func New(maxKeys int) (*MemStore, error) {
	var m *MemStore

	if maxKeys > 0 {
		keys, err := lru.New(maxKeys)
		if err != nil {
			return nil, err
		}

		m = &MemStore{
			keys: keys,
		}
	} else {
		m = &MemStore{
			m: make(map[string]*int64),
		}
	}
	return m, nil
}

// GetWithTime returns the value of the key if it is in the store or
// -1 if it does not exist. It also returns the current local time on
// the machine.
func (ms *MemStore) GetWithTime(key string) (int64, time.Time, error) {
	now := time.Now()
	valP, ok := ms.get(key, false)

	if !ok {
		return -1, now, nil
	}

	return atomic.LoadInt64(valP), now, nil
}

// SetIfNotExistsWithTTL sets the value of key only if it is not
// already set in the store it returns whether a new value was set. It
// ignores the ttl.
func (ms *MemStore) SetIfNotExistsWithTTL(key string, value int64, _ time.Duration) (bool, error) {
	_, ok := ms.get(key, false)

	if ok {
		return false, nil
	}

	ms.Lock()
	defer ms.Unlock()

	_, ok = ms.get(key, true)

	if ok {
		return false, nil
	}

	// Store a pointer to a new instance so that the caller
	// can't mutate the value after setting
	v := value

	if ms.keys != nil {
		ms.keys.Add(key, &v)
	} else {
		ms.m[key] = &v
	}

	return true, nil
}

// CompareAndSwapWithTTL atomically compares the value at key to the
// old value. If it matches, it sets it to the new value and returns
// true. Otherwise, it returns false. If the key does not exist in the
// store, it returns false with no error. It ignores the ttl.
func (ms *MemStore) CompareAndSwapWithTTL(key string, old, new int64, _ time.Duration) (bool, error) {
	valP, ok := ms.get(key, false)

	if !ok {
		return false, nil
	}

	return atomic.CompareAndSwapInt64(valP, old, new), nil
}

func (ms *MemStore) get(key string, locked bool) (*int64, bool) {
	var valP *int64
	var ok bool

	if ms.keys != nil {
		var valI interface{}

		valI, ok = ms.keys.Get(key)
		if ok {
			valP = valI.(*int64)
		}
	} else {
		if !locked {
			ms.RLock()
			defer ms.RUnlock()
		}
		valP, ok = ms.m[key]
	}

	return valP, ok
}
