package namer

import (
	"fmt"
	"math/rand"
	"strings"
)

const defaultCharset = "abcdefghijklmnopqrstuvwxyz1234567890"

type Options struct {
	// length of random string in generated result
	N int

	Charset []byte
}

// Generator generate a random string with specified options and store it by its name.
type Generator interface {
	Register(ident string, opt *Options) error
	Get(ident string) string
}

func NewGenerator(seed int64) Generator {
	return &generator{
		store: map[string]string{},
		r:     rand.New(rand.NewSource(seed)),
	}
}

type generator struct {
	r     *rand.Rand
	store map[string]string
}

func (r *generator) Get(n string) string {
	s, ok := r.store[n]
	if ok {
		return s
	}

	return n
}

func (r *generator) Register(n string, opt *Options) error {
	if _, ok := r.store[n]; ok {
		return fmt.Errorf("duplicate name %s", n)
	}
	if opt == nil {
		opt = &Options{}
	}

	if len(opt.Charset) == 0 {
		opt.Charset = []byte(defaultCharset)
	}

	s := random(r.r, opt.Charset, opt.N)

	r.store[n] = s

	return nil
}

func indexBits(charset []byte) int {
	bits := 0
	for l := len(charset); l > 0; l >>= 1 {
		bits += 1
	}
	return bits
}

// See https://stackoverflow.com/questions/22892120/how-to-generate-a-random-string-of-a-fixed-length-in-go
func random(r *rand.Rand, charset []byte, n int) string {
	// bits to present index of charset
	bits := indexBits(charset)
	var mask int64 = 1<<bits - 1
	count := 63 / bits

	sb := strings.Builder{}
	sb.Grow(n)
	for i, cache, remain := n-1, r.Int63(), count; i >= 0; {
		if remain == 0 {
			cache, remain = r.Int63(), count
		}
		if idx := int(cache & mask); idx < len(charset) {
			if err := sb.WriteByte(charset[idx]); err != nil {
				panic(err)
			}
			i--
		}
		cache >>= bits
		remain--
	}

	return sb.String()
}
