// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause

package set

import (
	"iter"
	"maps"

	"tailscale.com/types/structs"
)

// SmallSet is a set that is optimized for reducing memory overhead when the
// expected size of the set is 0 or 1 elements.
//
// The zero value of SmallSet is a usable empty set.
//
// When storing a SmallSet in a map as a value type, it is important to re-assign
// the map entry after calling Add or Delete, as the SmallSet's representation
// may change.
//
// Copying a SmallSet by value may alias the previous value. Use the Clone method
// to create a new SmallSet with the same contents.
type SmallSet[T comparable] struct {
	_   structs.Incomparable // to prevent == mistakes
	one T                    // if non-zero, then single item in set
	m   Set[T]               // if non-nil, the set of items, which might be size 1 if it's the zero value of T
}

// Values returns an iterator over the elements of the set.
// The iterator will yield the elements in no particular order.
func (s SmallSet[T]) Values() iter.Seq[T] {
	if s.m != nil {
		return maps.Keys(s.m)
	}
	var zero T
	return func(yield func(T) bool) {
		if s.one != zero {
			yield(s.one)
		}
	}
}

// Contains reports whether e is in the set.
func (s SmallSet[T]) Contains(e T) bool {
	if s.m != nil {
		return s.m.Contains(e)
	}
	var zero T
	return e != zero && s.one == e
}

// SoleElement returns the single value in the set, if the set has exactly one
// element.
//
// If the set is empty or has more than one element, ok will be false and e will
// be the zero value of T.
func (s SmallSet[T]) SoleElement() (e T, ok bool) {
	return s.one, s.Len() == 1
}

// Add adds e to the set.
//
// When storing a SmallSet in a map as a value type, it is important to
// re-assign the map entry after calling Add or Delete, as the SmallSet's
// representation may change.
func (s *SmallSet[T]) Add(e T) {
	var zero T
	if s.m != nil {
		s.m.Add(e)
		return
	}
	// Non-zero elements can go into s.one.
	if e != zero {
		if s.one == zero {
			s.one = e // Len 0 to Len 1
			return
		}
		if s.one == e {
			return // dup
		}
	}
	// Need to make a multi map, either
	// because we now have two items, or
	// because e is the zero value.
	s.m = Set[T]{}
	if s.one != zero {
		s.m.Add(s.one) // move single item to multi
	}
	s.m.Add(e) // add new item, possibly zero
	s.one = zero
}

// Len reports the number of elements in the set.
func (s SmallSet[T]) Len() int {
	var zero T
	if s.m != nil {
		return s.m.Len()
	}
	if s.one != zero {
		return 1
	}
	return 0
}

// Delete removes e from the set.
//
// When storing a SmallSet in a map as a value type, it is important to
// re-assign the map entry after calling Add or Delete, as the SmallSet's
// representation may change.
func (s *SmallSet[T]) Delete(e T) {
	var zero T
	if s.m == nil {
		if s.one == e {
			s.one = zero
		}
		return
	}
	s.m.Delete(e)

	// If the map size drops to zero, that means
	// it only contained the zero value of T.
	if s.m.Len() == 0 {
		s.m = nil
		return
	}

	// If the map size drops to one element and doesn't
	// contain the zero value, we can switch back to the
	// single-item representation.
	if s.m.Len() == 1 {
		for v := range s.m {
			if v != zero {
				s.one = v
				s.m = nil
			}
		}
	}
	return
}

// Clone returns a copy of s that doesn't alias the original.
func (s SmallSet[T]) Clone() SmallSet[T] {
	return SmallSet[T]{
		one: s.one,
		m:   maps.Clone(s.m), // preserves nilness
	}
}
