package store

import (
	"context"
	"path/filepath"
	"sort"
	"strings"
	"time"

	"github.com/patrickmn/go-cache"
)

// NewStore returns a memory store
func NewStore(opts ...Option) Store {
	return &memoryStore{
		opts:  NewOptions(opts...),
		store: cache.New(cache.NoExpiration, 5*time.Minute),
	}
}

func (m *memoryStore) Connect(ctx context.Context) error {
	return nil
}

func (m *memoryStore) Disconnect(ctx context.Context) error {
	m.store.Flush()
	return nil
}

type memoryStore struct {
	store *cache.Cache
	opts  Options
}

func (m *memoryStore) key(prefix, key string) string {
	return filepath.Join(prefix, key)
}

func (m *memoryStore) exists(prefix, key string) error {
	key = m.key(prefix, key)

	_, found := m.store.Get(key)
	if !found {
		return ErrNotFound
	}

	return nil
}

func (m *memoryStore) get(prefix, key string, val interface{}) error {
	key = m.key(prefix, key)

	r, found := m.store.Get(key)
	if !found {
		return ErrNotFound
	}

	buf, ok := r.([]byte)
	if !ok {
		return ErrNotFound
	}

	return m.opts.Codec.Unmarshal(buf, val)
}

func (m *memoryStore) delete(prefix, key string) {
	key = m.key(prefix, key)
	m.store.Delete(key)
}

func (m *memoryStore) list(prefix string, limit, offset uint) []string {
	allItems := m.store.Items()
	allKeys := make([]string, 0, len(allItems))

	for k := range allItems {
		if !strings.HasPrefix(k, prefix) {
			continue
		}
		k = strings.TrimPrefix(k, prefix)
		if k[0] == '/' {
			k = k[1:]
		}
		allKeys = append(allKeys, k)
	}

	if limit != 0 || offset != 0 {
		sort.Slice(allKeys, func(i, j int) bool { return allKeys[i] < allKeys[j] })
		sort.Slice(allKeys, func(i, j int) bool { return allKeys[i] < allKeys[j] })
		end := len(allKeys)
		if limit > 0 {
			calcLimit := int(offset + limit)
			if calcLimit < end {
				end = calcLimit
			}
		}

		if int(offset) >= end {
			return nil
		}
		return allKeys[offset:end]
	}
	return allKeys
}

func (m *memoryStore) Init(opts ...Option) error {
	for _, o := range opts {
		o(&m.opts)
	}
	return nil
}

func (m *memoryStore) String() string {
	return "memory"
}

func (m *memoryStore) Name() string {
	return m.opts.Name
}

func (m *memoryStore) Exists(ctx context.Context, key string, opts ...ExistsOption) error {
	options := NewExistsOptions(opts...)
	if options.Namespace == "" {
		options.Namespace = m.opts.Namespace
	}
	return m.exists(options.Namespace, key)
}

func (m *memoryStore) Read(ctx context.Context, key string, val interface{}, opts ...ReadOption) error {
	options := NewReadOptions(opts...)
	if options.Namespace == "" {
		options.Namespace = m.opts.Namespace
	}
	return m.get(options.Namespace, key, val)
}

func (m *memoryStore) Write(ctx context.Context, key string, val interface{}, opts ...WriteOption) error {
	options := NewWriteOptions(opts...)
	if options.Namespace == "" {
		options.Namespace = m.opts.Namespace
	}
	if options.TTL == 0 {
		options.TTL = cache.NoExpiration
	}

	key = m.key(options.Namespace, key)

	buf, err := m.opts.Codec.Marshal(val)
	if err != nil {
		return err
	}

	m.store.Set(key, buf, options.TTL)
	return nil
}

func (m *memoryStore) Delete(ctx context.Context, key string, opts ...DeleteOption) error {
	options := NewDeleteOptions(opts...)
	if options.Namespace == "" {
		options.Namespace = m.opts.Namespace
	}

	m.delete(options.Namespace, key)
	return nil
}

func (m *memoryStore) Options() Options {
	return m.opts
}

func (m *memoryStore) List(ctx context.Context, opts ...ListOption) ([]string, error) {
	options := NewListOptions(opts...)
	if options.Namespace == "" {
		options.Namespace = m.opts.Namespace
	}

	keys := m.list(options.Namespace, options.Limit, options.Offset)

	if len(options.Prefix) > 0 {
		var prefixKeys []string
		for _, k := range keys {
			if strings.HasPrefix(k, options.Prefix) {
				prefixKeys = append(prefixKeys, k)
			}
		}
		keys = prefixKeys
	}

	if len(options.Suffix) > 0 {
		var suffixKeys []string
		for _, k := range keys {
			if strings.HasSuffix(k, options.Suffix) {
				suffixKeys = append(suffixKeys, k)
			}
		}
		keys = suffixKeys
	}

	return keys, nil
}