package common

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

// The error returned when an added id already exists in the set.
var ErrIdTaken = errors.New("id already taken")

// The error returned when a requested item does not exist in the set.
var ErrIdentifiedMissing = errors.New("item does not exist")

// Interface for an item storeable in the set
type Identified interface {
	Id() string
}

// Set with string lookup.
// TODO: Add trie for efficient prefix lookup?
type IdSet struct {
	sync.RWMutex
	lookup map[string]Identified
}

// newIdSet creates a new set.
func NewIdSet() *IdSet {
	return &IdSet{
		lookup: map[string]Identified{},
	}
}

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

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

// In checks if an item exists in this set.
func (s *IdSet) In(item Identified) bool {
	id := s.normalize(item.Id())
	s.RLock()
	_, ok := s.lookup[id]
	s.RUnlock()
	return ok
}

// Get returns an item with the given Id.
func (s *IdSet) Get(id string) (Identified, error) {
	s.RLock()
	item, ok := s.lookup[s.normalize(id)]
	s.RUnlock()

	if !ok {
		return nil, ErrIdentifiedMissing
	}

	return item, nil
}

// Add item to this set if it does not exist already.
func (s *IdSet) Add(item Identified) error {
	s.Lock()
	defer s.Unlock()

	id := s.normalize(item.Id())
	_, found := s.lookup[id]
	if found {
		return ErrIdTaken
	}

	s.lookup[id] = item
	return nil
}

// Remove item from this set.
func (s *IdSet) Remove(item Identified) error {
	s.Lock()
	defer s.Unlock()
	id := s.normalize(item.Id())
	_, found := s.lookup[id]
	if !found {
		return ErrIdentifiedMissing
	}
	delete(s.lookup, id)
	return nil
}

// Replace item from old id with new Identified.
// Used for moving the same Identified to a new Id, such as a rename.
func (s *IdSet) Replace(oldId string, item Identified) error {
	id := s.normalize(item.Id())
	oldId = s.normalize(oldId)

	s.Lock()
	defer s.Unlock()

	// Check if it already exists
	_, found := s.lookup[id]
	if found {
		return ErrIdTaken
	}

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

	// Add new Identified
	s.lookup[id] = item

	return nil
}

// Each loops over every item while holding a read lock and applies fn to each
// element.
func (s *IdSet) Each(fn func(item Identified)) {
	s.RLock()
	for _, item := range s.lookup {
		fn(item)
	}
	s.RUnlock()
}

// ListPrefix returns a list of items with a prefix, case insensitive.
func (s *IdSet) ListPrefix(prefix string) []Identified {
	r := []Identified{}
	prefix = strings.ToLower(prefix)

	s.RLock()
	defer s.RUnlock()

	for id, item := range s.lookup {
		if !strings.HasPrefix(string(id), prefix) {
			continue
		}
		r = append(r, item)
	}

	return r
}

func (s *IdSet) normalize(id string) string {
	return strings.ToLower(id)
}