package randomvalues

import (
	"crypto/rand"
	"encoding/binary"
	"fmt"
	"math/big"
	"strconv"
	"sync"
	"time"

	"github.com/fiskerinc/cloud-services/pkg/utils/mt19937"
)

type Generator struct {
	characters  string
	counter     int32
	maxUniform  int32
	onceUniform sync.Once
	uniform     *mt19937.UniformIntDistribution
	mt          *mt19937.MT19937
}

func NewGenerator(allowedchars string) Generator {
	if len(allowedchars) == 0 {
		allowedchars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
	}
	instance := Generator{
		characters: allowedchars,
		maxUniform: 255,
	}
	val, _ := instance.GetUInt32()
	instance.counter = int32(val)

	return instance
}

func (g *Generator) GetBytes(length int) ([]byte, error) {
	result := make([]byte, length)
	_, err := rand.Read(result)
	if err != nil {
		return nil, err
	}

	return result, nil
}

func (g *Generator) GetString(length int) (string, error) {
	result := make([]byte, length)
	for i := 0; i < length; i++ {
		num, err := rand.Int(rand.Reader, big.NewInt(int64(len(g.characters))))
		if err != nil {
			return "", err
		}
		result[i] = g.characters[num.Int64()]
	}

	return string(result), nil
}

func (g *Generator) GetUInt64() (uint64, error) {
	buf := make([]byte, 8)
	_, err := rand.Read(buf)
	if err != nil {
		return 0, err
	}
	return binary.LittleEndian.Uint64(buf), nil
}

func (g *Generator) GetUInt32() (uint32, error) {
	buf := make([]byte, 4)
	_, err := rand.Read(buf)
	if err != nil {
		return 0, err
	}
	return binary.LittleEndian.Uint32(buf), nil
}

func (g *Generator) GetInt(max int) int {
	result, _ := rand.Int(rand.Reader, big.NewInt(int64(max)))
	return int(result.Int64())
}

func (g *Generator) GetHex() (string, error) {
	value, err := g.GetUInt64()
	if err != nil {
		return "", err
	}

	return fmt.Sprintf("%016s", strconv.FormatUint(value, 16)), nil
}

func (g *Generator) getUniformIntDistribution() *mt19937.UniformIntDistribution {
	g.onceUniform.Do(func() {
		if g.uniform != nil {
			return
		}
		g.mt = mt19937.New()
		g.uniform = mt19937.DistInt64(g.mt, 0, 255)
	})

	return g.uniform
}

func (g *Generator) GetUnformDistInt() int {
	dist := g.getUniformIntDistribution()
	return int(dist.Int64())
}

func (g *Generator) GetUniformDistHex() (string, error) {
	now := time.Now().UnixNano()
	result := uint64(now) << 32
	d := g.GetUnformDistInt() & 0xFF
	result |= uint64(d << 24)
	d = g.GetUnformDistInt() & 0xFF
	result |= uint64(d << 16)
	c := g.counter & 0xFFFF
	result |= uint64(c)
	g.counter++

	return fmt.Sprintf("%016s", strconv.FormatUint(result, 16)), nil
}

func (g *Generator) Close() {
	g.counter = 0
	g.characters = ""
	g.mt = nil
	g.uniform = nil
}
