package concurrent

import (
	"sync"

	. "gitea.zaclys.com/bvaudour/gob/option"
)

// Map fournit une solution pour ajouter des éléments dans un map de façon concurrente.
type Map[K comparable, V any] struct {
	data   map[K]V
	mtx    sync.Mutex
	closed bool
}

func (m *Map[K, V]) checkOpened() {
	if m.closed {
		panic("The map cannot be modified anymore.")
	}
}

func (m *Map[K, V]) checkClosed() {
	if !m.closed {
		panic("The Map needs to be opened.")
	}
}

func (m *Map[K, V]) changeState(closed bool) {
	m.mtx.Lock()
	defer m.mtx.Unlock()

	m.closed = closed
}

func (m *Map[K, V]) get() (out map[K]V) {
	out = make(map[K]V)
	for k, v := range m.data {
		out[k] = v
	}

	return
}

// Clear vide le map.
// Cette méthode nécessite que le map soit ouvert.
func (m *Map[K, V]) Clear() {
	m.mtx.Lock()
	defer m.mtx.Unlock()

	m.checkOpened()
	clear(m.data)
}

// Close empêche les modifications du map.
func (m *Map[K, V]) Close() {
	m.changeState(true)
}

// Open permet les modifications du map.
func (m *Map[K, V]) Open() {
	m.changeState(false)
}

// Set associe la clé et la valeur données dans le map.
// Cette méthode nécessite que le map soit ouvert.
func (m *Map[K, V]) Set(key K, value V) {
	m.mtx.Lock()
	defer m.mtx.Unlock()

	m.checkOpened()
	m.data[key] = value
}

// Get retourne la valeur du map à la clé donnée.
func (m *Map[K, V]) Get(key K) (value Option[V]) {
	m.mtx.Lock()
	defer m.mtx.Unlock()

	if v, ok := m.data[key]; ok {
		value = Some(v)
	}

	return
}

// Data retourne le map.
// Cette méthode nécessite que le map soit fermé.
func (m *Map[K, V]) Data() map[K]V {
	m.mtx.Lock()
	defer m.mtx.Unlock()

	m.checkClosed()

	return m.get()
}

// CloseData retourne le map après l’avoir préalablement fermé en écriture.
func (m *Map[K, V]) CloseData() map[K]V {
	m.mtx.Lock()
	defer m.mtx.Unlock()

	m.closed = true

	return m.get()
}

// NewMap retourne un map initialisé avec les valeurs fournies.
func NewMap[K comparable, V any](elems ...map[K]V) *Map[K, V] {
	out := make(map[K]V)
	for _, m := range elems {
		for k, v := range m {
			out[k] = v
		}
	}

	return &Map[K, V]{
		data: out,
	}
}
