package snowflake_server

import (
	"net"
	"sync"

	"gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/v2/common/turbotunnel"
)

// clientIDMap is a fixed-capacity mapping from ClientIDs to a net.Addr.
// Adding a new entry using the Set method causes the oldest existing entry to
// be forgotten.
//
// This data type is meant to be used to remember the IP address associated with
// a ClientID, during the short period of time between when a WebSocket
// connection with that ClientID began, and when a KCP session is established.
//
// The design requirements of this type are that it needs to remember a mapping
// for only a short time, and old entries should expire so as not to consume
// unbounded memory. It is not a critical error if an entry is forgotten before
// it is needed; better to forget entries than to use too much memory.
type clientIDMap struct {
	lock sync.Mutex
	// entries is a circular buffer of (ClientID, addr) pairs.
	entries []struct {
		clientID turbotunnel.ClientID
		addr     net.Addr
	}
	// oldest is the index of the oldest member of the entries buffer, the
	// one that will be overwritten at the next call to Set.
	oldest int
	// current points to the index of the most recent entry corresponding to
	// each ClientID.
	current map[turbotunnel.ClientID]int
}

// newClientIDMap makes a new clientIDMap with the given capacity.
func newClientIDMap(capacity int) *clientIDMap {
	return &clientIDMap{
		entries: make([]struct {
			clientID turbotunnel.ClientID
			addr     net.Addr
		}, capacity),
		oldest:  0,
		current: make(map[turbotunnel.ClientID]int),
	}
}

// Set adds a mapping from clientID to addr, replacing any previous mapping for
// clientID. It may also cause the clientIDMap to forget at most one other
// mapping, the oldest one.
func (m *clientIDMap) Set(clientID turbotunnel.ClientID, addr net.Addr) {
	m.lock.Lock()
	defer m.lock.Unlock()
	if len(m.entries) == 0 {
		// The invariant m.oldest < len(m.entries) does not hold in this
		// special case.
		return
	}
	// m.oldest is the index of the entry we're about to overwrite. If it's
	// the current entry for any ClientID, we need to delete that clientID
	// from the current map (that ClientID is now forgotten).
	if i, ok := m.current[m.entries[m.oldest].clientID]; ok && i == m.oldest {
		delete(m.current, m.entries[m.oldest].clientID)
	}
	// Overwrite the oldest entry.
	m.entries[m.oldest].clientID = clientID
	m.entries[m.oldest].addr = addr
	// Add the overwritten entry to the quick-lookup map.
	m.current[clientID] = m.oldest
	// What was the oldest entry is now the newest.
	m.oldest = (m.oldest + 1) % len(m.entries)
}

// Get returns a previously stored mapping. The second return value indicates
// whether clientID was actually present in the map. If it is false, then the
// returned address will be nil.
func (m *clientIDMap) Get(clientID turbotunnel.ClientID) (net.Addr, bool) {
	m.lock.Lock()
	defer m.lock.Unlock()
	if i, ok := m.current[clientID]; ok {
		return m.entries[i].addr, true
	} else {
		return nil, false
	}
}
