package set

import (
	"errors"
	"strings"
	"sync"
)

// Returned when an added key already exists in the set.
var ErrCollision = errors.New("key already exists")

// Returned when a requested item does not exist in the set.
var ErrMissing = errors.New("item does not exist")

// ZeroValue can be used when we only care about the key, not about the value.
var ZeroValue = struct{}{}

// Interface is the Set interface
type Interface interface {
	Clear() int
	Each(fn IterFunc) error
	// Add only if the item does not already exist
	Add(item Item) error
	// Set item, override if it already exists
	Set(item Item) error
	Get(key string) (Item, error)
	In(key string) bool
	Len() int
	ListPrefix(prefix string) []Item
	Remove(key string) error
	Replace(oldKey string, item Item) error
}

type IterFunc func(key string, item Item) error

type Set struct {
	sync.RWMutex
	lookup    map[string]Item
	normalize func(string) string
}

// New creates a new set with case-insensitive keys
func New() *Set {
	return &Set{
		lookup:    map[string]Item{},
		normalize: normalize,
	}
}

// Clear removes all items and returns the number removed.
func (s *Set) Clear() int {
	s.Lock()
	n := len(s.lookup)
	s.lookup = map[string]Item{}
	s.Unlock()
	return n
}

// Len returns the size of the set right now.
func (s *Set) Len() int {
	s.RLock()
	defer s.RUnlock()
	return len(s.lookup)
}

// In checks if an item exists in this set.
func (s *Set) In(key string) bool {
	key = s.normalize(key)
	s.RLock()
	item, ok := s.lookup[key]
	s.RUnlock()
	if ok && item.Value() == nil {
		s.cleanup(key)
		ok = false
	}
	return ok
}

// Get returns an item with the given key.
func (s *Set) Get(key string) (Item, error) {
	key = s.normalize(key)
	s.RLock()
	item, ok := s.lookup[key]
	s.RUnlock()

	if !ok {
		return nil, ErrMissing
	}
	if item.Value() == nil {
		s.cleanup(key)
	}

	return item, nil
}

// Remove potentially expired key (normalized).
func (s *Set) cleanup(key string) {
	s.Lock()
	item, ok := s.lookup[key]
	if ok && item.Value() == nil {
		delete(s.lookup, key)
	}
	s.Unlock()
}

// Add item to this set if it does not exist already.
func (s *Set) Add(item Item) error {
	key := s.normalize(item.Key())

	s.Lock()
	defer s.Unlock()

	oldItem, found := s.lookup[key]
	if found && oldItem.Value() != nil {
		return ErrCollision
	}

	s.lookup[key] = item
	return nil
}

// Set item to this set, even if it already exists.
func (s *Set) Set(item Item) error {
	key := s.normalize(item.Key())

	s.Lock()
	defer s.Unlock()
	s.lookup[key] = item
	return nil
}

// Remove item from this set.
func (s *Set) Remove(key string) error {
	key = s.normalize(key)

	s.Lock()
	defer s.Unlock()

	_, found := s.lookup[key]
	if !found {
		return ErrMissing
	}
	delete(s.lookup, key)
	return nil
}

// Replace oldKey with a new item, which might be a new key.
// Can be used to rename items.
func (s *Set) Replace(oldKey string, item Item) error {
	newKey := s.normalize(item.Key())
	oldKey = s.normalize(oldKey)

	s.Lock()
	defer s.Unlock()

	if newKey != oldKey {
		// Check if it already exists
		_, found := s.lookup[newKey]
		if found {
			return ErrCollision
		}

		// Remove oldKey
		_, found = s.lookup[oldKey]
		if !found {
			return ErrMissing
		}
		delete(s.lookup, oldKey)
	}

	// Add new item
	s.lookup[newKey] = item

	return nil
}

// Each loops over every item while holding a read lock and applies fn to each
// element.
func (s *Set) Each(fn IterFunc) error {
	var err error
	s.RLock()
	for key, item := range s.lookup {
		if item.Value() == nil {
			// Expired
			defer s.cleanup(key)
			continue
		}
		if err = fn(key, item); err != nil {
			// Abort early
			break
		}
	}
	s.RUnlock()
	return err
}

// ListPrefix returns a list of items with a prefix, normalized.
// TODO: Add trie for efficient prefix lookup
func (s *Set) ListPrefix(prefix string) []Item {
	r := []Item{}
	prefix = s.normalize(prefix)

	s.Each(func(key string, item Item) error {
		if strings.HasPrefix(key, prefix) {
			r = append(r, item)
		}
		return nil
	})

	return r
}

func normalize(key string) string {
	return strings.ToLower(key)
}