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

// nocasemaps provides efficient functions to set and get entries in Go maps
// keyed by a string, where the string is always lower-case.
package nocasemaps

import (
	"unicode"
	"unicode/utf8"
)

// TODO(https://github.com/golang/go/discussions/54245):
// Define a generic Map type instead. The main reason to avoid that is because
// there is currently no convenient API for iteration.
// An opaque Map type would force callers to interact with the map through
// the methods, preventing accidental interactions with the underlying map
// without using functions in this package.

const stackArraySize = 32

// Get is equivalent to:
//
//	v := m[strings.ToLower(k)]
func Get[K ~string, V any](m map[K]V, k K) V {
	if isLowerASCII(string(k)) {
		return m[k]
	}
	var a [stackArraySize]byte
	return m[K(appendToLower(a[:0], string(k)))]
}

// GetOk is equivalent to:
//
//	v, ok := m[strings.ToLower(k)]
func GetOk[K ~string, V any](m map[K]V, k K) (V, bool) {
	if isLowerASCII(string(k)) {
		v, ok := m[k]
		return v, ok
	}
	var a [stackArraySize]byte
	v, ok := m[K(appendToLower(a[:0], string(k)))]
	return v, ok
}

// Set is equivalent to:
//
//	m[strings.ToLower(k)] = v
func Set[K ~string, V any](m map[K]V, k K, v V) {
	if isLowerASCII(string(k)) {
		m[k] = v
		return
	}
	// TODO(https://go.dev/issues/55930): This currently always allocates.
	// An optimization to the compiler and runtime could make this allocate-free
	// in the event that we are overwriting a map entry.
	//
	// Alternatively, we could use string interning.
	// See an example intern data structure, see:
	//	https://github.com/go-json-experiment/json/blob/master/intern.go
	var a [stackArraySize]byte
	m[K(appendToLower(a[:0], string(k)))] = v
}

// Delete is equivalent to:
//
//	delete(m, strings.ToLower(k))
func Delete[K ~string, V any](m map[K]V, k K) {
	if isLowerASCII(string(k)) {
		delete(m, k)
		return
	}
	var a [stackArraySize]byte
	delete(m, K(appendToLower(a[:0], string(k))))
}

// AppendSliceElem is equivalent to:
//
//	append(m[strings.ToLower(k)], v)
func AppendSliceElem[K ~string, S []E, E any](m map[K]S, k K, vs ...E) {
	// if the key is already lowercased
	if isLowerASCII(string(k)) {
		m[k] = append(m[k], vs...)
		return
	}

	// if key needs to become lowercase, uses appendToLower
	var a [stackArraySize]byte
	s := appendToLower(a[:0], string(k))
	m[K(s)] = append(m[K(s)], vs...)
}

func isLowerASCII(s string) bool {
	for i := range len(s) {
		if c := s[i]; c >= utf8.RuneSelf || ('A' <= c && c <= 'Z') {
			return false
		}
	}
	return true
}

func appendToLower(b []byte, s string) []byte {
	for i := 0; i < len(s); i++ {
		switch c := s[i]; {
		case 'A' <= c && c <= 'Z':
			b = append(b, c+('a'-'A'))
		case c < utf8.RuneSelf:
			b = append(b, c)
		default:
			r, n := utf8.DecodeRuneInString(s[i:])
			b = utf8.AppendRune(b, unicode.ToLower(r))
			i += n - 1 // -1 to compensate for i++ in loop advancement
		}
	}
	return b
}
