package bytesutil

import (
	"flag"
	"maps"
	"strings"
	"sync"
	"sync/atomic"
	"time"

	"github.com/VictoriaMetrics/VictoriaMetrics/lib/fasttime"
	"github.com/VictoriaMetrics/VictoriaMetrics/lib/timeutil"
)

var (
	internStringMaxLen = flag.Int("internStringMaxLen", 500, "The maximum length for strings to intern. A lower limit may save memory at the cost of higher CPU usage. "+
		"See https://en.wikipedia.org/wiki/String_interning . See also -internStringDisableCache and -internStringCacheExpireDuration")
	disableCache = flag.Bool("internStringDisableCache", false, "Whether to disable caches for interned strings. This may reduce memory usage at the cost of higher CPU usage. "+
		"See https://en.wikipedia.org/wiki/String_interning . See also -internStringCacheExpireDuration and -internStringMaxLen")
	cacheExpireDuration = flag.Duration("internStringCacheExpireDuration", 6*time.Minute, "The expiry duration for caches for interned strings. "+
		"See https://en.wikipedia.org/wiki/String_interning . See also -internStringMaxLen and -internStringDisableCache")
)

type internStringMap struct {
	mutableLock  sync.Mutex
	mutable      map[string]string
	mutableReads uint64

	readonly atomic.Pointer[map[string]internStringMapEntry]
}

type internStringMapEntry struct {
	deadline uint64
	s        string
}

func newInternStringMap() *internStringMap {
	m := &internStringMap{
		mutable: make(map[string]string),
	}
	readonly := make(map[string]internStringMapEntry)
	m.readonly.Store(&readonly)

	go func() {
		cleanupInterval := timeutil.AddJitterToDuration(*cacheExpireDuration) / 2
		ticker := time.NewTicker(cleanupInterval)
		for range ticker.C {
			m.cleanup()
		}
	}()

	return m
}

func (m *internStringMap) getReadonly() map[string]internStringMapEntry {
	return *m.readonly.Load()
}

func (m *internStringMap) intern(s string) string {
	if isSkipCache(s) {
		return strings.Clone(s)
	}

	readonly := m.getReadonly()
	e, ok := readonly[s]
	if ok {
		// Fast path - the string has been found in readonly map.
		return e.s
	}

	// Slower path - search for the string in mutable map under the lock.
	m.mutableLock.Lock()
	sInterned, ok := m.mutable[s]
	if !ok {
		// Verify whether s has been already registered by concurrent goroutines in m.readonly
		readonly = m.getReadonly()
		e, ok = readonly[s]
		if !ok {
			// Slowest path - register the string in mutable map.
			// Make a new copy for s in order to remove references from possible bigger string s refers to.
			sInterned = strings.Clone(s)
			m.mutable[sInterned] = sInterned
		} else {
			sInterned = e.s
		}
	}
	m.mutableReads++
	if m.mutableReads > uint64(len(readonly)) {
		m.migrateMutableToReadonlyLocked()
		m.mutableReads = 0
	}
	m.mutableLock.Unlock()

	return sInterned
}

func (m *internStringMap) migrateMutableToReadonlyLocked() {
	readonly := m.getReadonly()
	readonlyCopy := make(map[string]internStringMapEntry, len(readonly)+len(m.mutable))
	maps.Copy(readonlyCopy, readonly)
	deadline := fasttime.UnixTimestamp() + uint64(cacheExpireDuration.Seconds()+0.5)
	for k, s := range m.mutable {
		readonlyCopy[k] = internStringMapEntry{
			s:        s,
			deadline: deadline,
		}
	}
	m.mutable = make(map[string]string)
	m.readonly.Store(&readonlyCopy)
}

func (m *internStringMap) cleanup() {
	readonly := m.getReadonly()
	currentTime := fasttime.UnixTimestamp()
	needCleanup := false
	for _, e := range readonly {
		if e.deadline <= currentTime {
			needCleanup = true
			break
		}
	}
	if !needCleanup {
		return
	}

	readonlyCopy := make(map[string]internStringMapEntry, len(readonly))
	for k, e := range readonly {
		if e.deadline > currentTime {
			readonlyCopy[k] = e
		}
	}
	m.readonly.Store(&readonlyCopy)
}

func isSkipCache(s string) bool {
	return *disableCache || len(s) > *internStringMaxLen
}

// InternBytes interns b as a string
func InternBytes(b []byte) string {
	s := ToUnsafeString(b)
	return InternString(s)
}

// InternString returns interned s.
//
// This may be needed for reducing the amounts of allocated memory.
func InternString(s string) string {
	return ism.intern(s)
}

var ism = newInternStringMap()
