package cache

import (
	"bytes"
	"compress/gzip"
	"context"
	"io"
	"strings"
	"sync"
	"time"
)

// InMemory implementation of the Cache client.
type InMemory struct {
	m *sync.Map
}

// NewInMemory instantiates a new in-memory Cache Client.
func NewInMemory() *InMemory {
	return &InMemory{m: new(sync.Map)}
}

// CheckStatus checks that the cache is ready, or returns an error.
func (c *InMemory) CheckStatus(ctx context.Context) (time.Duration, error) {
	return 0, nil
}

// Get fetch the cached asset at the given key, and returns true only if the
// asset was found.
func (c *InMemory) Get(key string) ([]byte, bool) {
	value, ok := c.m.Load(key)
	if !ok {
		return nil, false
	}

	entry := value.(cacheEntry)
	if time.Now().After(entry.expiredAt) {
		// The value is expired. Clean it and return not found
		c.Clear(key)
		return nil, false
	}

	return entry.payload, true
}

// MultiGet can be used to fetch several keys at once.
func (c *InMemory) MultiGet(keys []string) [][]byte {
	results := make([][]byte, len(keys))

	for i, key := range keys {
		results[i], _ = c.Get(key)
	}

	return results
}

// Keys returns the list of keys with the given prefix.
func (c *InMemory) Keys(prefix string) []string {
	results := make([]string, 0)

	c.m.Range(func(key, _ interface{}) bool {
		k := key.(string)
		if strings.HasPrefix(k, prefix) {
			results = append(results, k)
		}

		return true
	})

	return results
}

// Clear removes a key from the cache
func (c *InMemory) Clear(key string) {
	c.m.Delete(key)
}

// Set stores an asset to the given key.
func (c *InMemory) Set(key string, data []byte, expiration time.Duration) {
	c.m.Store(key, cacheEntry{
		payload:   data,
		expiredAt: time.Now().Add(expiration),
	})
}

// SetNX stores the data in the cache only if the key doesn't exist yet.
func (c *InMemory) SetNX(key string, data []byte, expiration time.Duration) {
	c.m.LoadOrStore(key, cacheEntry{
		payload:   data,
		expiredAt: time.Now().Add(expiration),
	})
}

// GetCompressed works like Get but expect a compressed asset that is
// uncompressed.
func (c *InMemory) GetCompressed(key string) (io.Reader, bool) {
	if r, ok := c.Get(key); ok {
		if gr, err := gzip.NewReader(bytes.NewReader(r)); err == nil {
			return gr, true
		}
	}
	return nil, false
}

// SetCompressed works like Set but compress the asset data before storing it.
func (c *InMemory) SetCompressed(key string, data []byte, expiration time.Duration) {
	dataCompressed := new(bytes.Buffer)
	gw := gzip.NewWriter(dataCompressed)
	defer gw.Close()
	if _, err := io.Copy(gw, bytes.NewReader(data)); err != nil {
		return
	}
	c.Set(key, dataCompressed.Bytes(), expiration)
}

// RefreshTTL can be used to update the TTL of an existing entry in the cache.
func (c *InMemory) RefreshTTL(key string, expiration time.Duration) {
	value, ok := c.m.Load(key)
	if !ok {
		return
	}

	entry := value.(cacheEntry)
	entry.expiredAt = time.Now().Add(expiration)
	c.m.Store(key, entry)
}
