// Copyright (c) 2023 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

package exsync

import (
	"sync"
)

type empty struct{}

var emptyVal = empty{}

// Set is a wrapper around a map[T]struct{} with a built-in mutex.
type Set[T comparable] struct {
	m map[T]empty
	l sync.RWMutex
}

// NewSet constructs a Set with an empty map.
func NewSet[T comparable]() *Set[T] {
	return NewSetWithMap[T](make(map[T]empty))
}

// NewSetWithSize constructs a Set with a map that has been allocated the given amount of space.
func NewSetWithSize[T comparable](size int) *Set[T] {
	return NewSetWithMap[T](make(map[T]empty, size))
}

// NewSetWithMap constructs a Set with the given map. Accessing the map directly after passing it here is not safe.
func NewSetWithMap[T comparable](m map[T]empty) *Set[T] {
	return &Set[T]{m: m}
}

// NewSetWithItems constructs a Set with items from the given slice pre-filled.
// The slice is not modified or used after the function returns, so using it after this is safe.
func NewSetWithItems[T comparable](items []T) *Set[T] {
	s := NewSetWithSize[T](len(items))
	for _, item := range items {
		s.m[item] = emptyVal
	}
	return s
}

// Add adds an item to the set. The return value is true if the item was added to the set, or false otherwise.
func (s *Set[T]) Add(item T) bool {
	if s == nil {
		return false
	}
	s.l.Lock()
	_, exists := s.m[item]
	if exists {
		s.l.Unlock()
		return false
	}
	s.m[item] = emptyVal
	s.l.Unlock()
	return true
}

// Has checks if the given item is in the set.
func (s *Set[T]) Has(item T) bool {
	if s == nil {
		return false
	}
	s.l.RLock()
	_, exists := s.m[item]
	s.l.RUnlock()
	return exists
}

// Pop removes the given item from the set. The return value is true if the item was in the set, or false otherwise.
func (s *Set[T]) Pop(item T) bool {
	if s == nil {
		return false
	}
	s.l.Lock()
	_, exists := s.m[item]
	if exists {
		delete(s.m, item)
	}
	s.l.Unlock()
	return exists
}

// Remove removes the given item from the set.
func (s *Set[T]) Remove(item T) {
	if s == nil {
		return
	}
	s.l.Lock()
	delete(s.m, item)
	s.l.Unlock()
}

// ReplaceAll replaces this set with the given set. If the given set is nil, the set is cleared.
func (s *Set[T]) ReplaceAll(newSet *Set[T]) {
	if s == nil {
		return
	}
	s.l.Lock()
	if newSet == nil {
		s.m = make(map[T]empty)
	} else {
		s.m = newSet.m
	}
	s.l.Unlock()
}

func (s *Set[T]) Size() int {
	if s == nil {
		return 0
	}
	s.l.RLock()
	size := len(s.m)
	s.l.RUnlock()
	return size
}

func (s *Set[T]) AsList() []T {
	if s == nil {
		return nil
	}
	s.l.RLock()
	list := make([]T, len(s.m))
	i := 0
	for item := range s.m {
		list[i] = item
		i++
	}
	s.l.RUnlock()
	return list
}
