package writebehind

import (
	"bytes"
	"fmt"
	"sync"
	"time"
)

// Incrementer defines an interface that allows items that can be identified
// using a string to be incremented by a value that can be stored as an int64.
// It only concerns itself with basic addition, as supported by the Go runtime.
// math/big.Int and other such types are not supported.
type Incrementer interface {
	Increment(values map[string]int64)
}

// Cache is a type that is used to store values in-memory, to be synced to a
// more permanent store later. It offers no read functionality; the only things
// you can do with it are increment the count for an item and sync it to the
// more permanent store. The more permanent store is written to using the
// Incrementer interface, and must implement that interface.
//
// Cache is a concurrency-safe type, and is safe for use in multiple goroutines
// at once.
type Cache struct {
	incrementer Incrementer
	duration    time.Duration
	values      map[string]int64
	ticker      *time.Ticker
	incoming    chan incrementReq
	kill        chan struct{}
	sync.Mutex
}

func (c *Cache) String() string {
	var b bytes.Buffer
	fmt.Fprintf(&b, "{")
	first := true
	for k, v := range c.values {
		if !first {
			fmt.Fprintf(&b, ", ")
		}
		fmt.Fprintf(&b, "%q: %v", k, v)
		first = false
	}
	fmt.Fprintf(&b, "}")
	return b.String()

}

// NeedsSync returns true if the Cache has any values to sync to the Incrementer,
// and false if the Cache is empty and is safe to dispose of.
func (c *Cache) NeedsSync() bool {
	return len(c.values) > 0
}

// Sync calls through to the Incrementer associated with the Cache, flushing the
// Cache's values to the Incrementer. It can be called manually, at any time,
// safely, but will be called automatically on the Duration specified in the Cache.
func (c *Cache) Sync() {
	c.Lock()
	defer c.Unlock()
	if !c.NeedsSync() {
		return
	}
	c.incrementer.Increment(c.values)
	c.values = map[string]int64{}
}

// NewCache creates, instantiates, and returns a cache using the specified values. It
// is the only way to obtain a usable Cache instance.
func NewCache(incrementer Incrementer, duration time.Duration) *Cache {
	c := &Cache{
		incrementer: incrementer,
		duration:    duration,
		values:      map[string]int64{},
		incoming:    make(chan incrementReq),
		kill:        make(chan struct{}),
		ticker:      time.NewTicker(duration),
	}
	go c.run()
	return c
}

// Stop halts the syncing behaviour of the Cache and frees up the resources used
// in that syncing behaviour. The Cache's store of values remains intact, however.
// It's important to note that the Stop method _does not_ implicitly write to the
// Incrementer, and you should manually call the Sync() method after calling the
// Stop method, to store any lingering values that were written after the last
// Sync.
func (c *Cache) Stop() {
	c.Lock()
	defer c.Unlock()
	close(c.kill)
	c.ticker.Stop()
}

func (c *Cache) run() {
	for {
		select {
		case <-c.ticker.C:
			c.Sync()
		case in := <-c.incoming:
			c.increment(in.key, in.value)
		case <-c.kill:
			return
		}
	}
}

func (c *Cache) increment(key string, value int64) {
	c.Lock()
	defer c.Unlock()
	c.values[key] = c.values[key] + value
}

// Increment updates the item that matches key in the Cache, adding value to it.
// It key does not exist in the Cache, it is created and set to value.
func (c *Cache) Increment(key string, value int64) {
	c.incoming <- incrementReq{key: key, value: value}
}

type incrementReq struct {
	key   string
	value int64
}

// MemoryIncrementer is an implementation of the Incrementer interface that stores
// values in memory. There is obviously no reason to do this outside of tests and
// stubs, and those two reasons are why this type exists. Don't use it for other
// things.
type MemoryIncrementer struct {
	sync.RWMutex
	values map[string]int64
}

// Increment updates the items specified by the keys of the map by adding the
// value of the map to the item's stored value.
func (m *MemoryIncrementer) Increment(increments map[string]int64) {
	m.Lock()
	defer m.Unlock()
	if m.values == nil {
		m.values = map[string]int64{}
	}
	for k, v := range increments {
		m.values[k] = m.values[k] + v
	}
}

// Get is a concurrency-safe helper to return the value for key.
func (m *MemoryIncrementer) Get(key string) int64 {
	m.RLock()
	defer m.RUnlock()
	return m.values[key]
}
