// Copyright (c) 2012 Jason McVetta.  This is Free Software, released under the
// terms of the GPL v3.  See http://www.gnu.org/copyleft/gpl.html for details.

// Package randutil provides various convenience functions for dealing with
// random numbers and strings.
package utils

import (
	"crypto/rand"
	"errors"
	"math/big"
)

const (
	// Set of characters to use for generating random strings
	Alphabet     = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
	Numerals     = "1234567890"
	Alphanumeric = Alphabet + Numerals
	Ascii        = Alphanumeric + "~!@#$%^&*()-_+={}[]\\|<,>.?/\"';:`"
)

var MinMaxError = errors.New("Min cannot be greater than max.")

// IntRange returns a random integer in the range from min to max.
// [min, max)
func RandInt(min, max int) (int, error) {
	var result int
	switch {
	case min > max:
		// Fail with error
		return result, MinMaxError
	case max == min:
		result = max
	case max > min:
		maxRand := max - min
		b, err := rand.Int(rand.Reader, big.NewInt(int64(maxRand)))
		if err != nil {
			return result, err
		}
		result = min + int(b.Int64())
	}
	return result, nil
}

// String returns a random string n characters long, composed of entities
// from charset.
func String(n int, charset string) (string, error) {
	var randstr string // Random string to return
	charlen := len(charset)
	// This is probably not the most efficient algorithm
	for i := 0; i < n; i++ {
		b, err := rand.Int(rand.Reader, big.NewInt(int64(charlen-1)))
		if err != nil {
			return randstr, err
		}
		r := int(b.Int64())
		c := string(charset[r])
		randstr += c
	}
	return randstr, nil
}

// StringRange returns a random string at least min and no more than max
// characters long, composed of entitites from charset.
func StringRange(min, max int, charset string) (string, error) {
	//
	// First determine the length of string to be generated
	//
	var err error      // Holds errors
	var strlen int     // Length of random string to generate
	var randstr string // Random string to return
	strlen, err = RandInt(min, max)
	if err != nil {
		return randstr, err
	}
	randstr, err = String(strlen, charset)
	if err != nil {
		return randstr, err
	}
	return randstr, nil
}

// AlphaRange returns a random alphanumeric string at least min and no more
// than max characters long.
func AlphaStringRange(min, max int) (string, error) {
	return StringRange(min, max, Alphanumeric)
}

// AlphaString returns a random alphanumeric string n characters long.
func AlphaString(n int) (string, error) {
	return String(n, Alphanumeric)
}

// ChoiceString returns a random selection from an array of strings.
func ChoiceString(choices []string) (string, error) {
	var winner string
	length := len(choices)
	i, err := RandInt(0, length)
	winner = choices[i]
	return winner, err
}

// ChoiceInt returns a random selection from an array of integers.
func ChoiceInt(choices []int) (int, error) {
	var winner int
	length := len(choices)
	i, err := RandInt(0, length)
	winner = choices[i]
	return winner, err
}

// A Choice contains a generic item and a weight controlling the frequency with
// which it will be selected.
type Choice struct {
	Weight int
	Item   interface{}
}

// WeightedChoice used weighted random selection to return one of the supplied
// choices.  Weights of 0 are never selected.  All other weight values are
// relative.  E.g. if you have two choices both weighted 3, they will be
// returned equally often; and each will be returned 3 times as often as a
// choice weighted 1.
func WeightedChoice(choices []Choice) (Choice, error) {
	// Based on this algorithm:
	//     http://eli.thegreenplace.net/2010/01/22/weighted-random-generation-in-python/
	// zaplogger.Debug("Choices", zap.Reflect("", choices))
	var ret Choice
	sum := 0
	for _, c := range choices {
		sum += c.Weight
	}
	r, err := RandInt(0, sum)

	if err != nil {
		return ret, err
	}
	for _, c := range choices {
		r -= c.Weight
		if r < 0 {
			return c, nil
		}
	}

	if r > 0 {
		return choices[0], nil
	}
	err = errors.New("Internal error - code should not reach this point")
	return ret, err
}

func SliceRand(source []interface{}, count int) []interface{} {
	sLen := len(source)
	temp := append([]interface{}{}, source...)

	var result []interface{}
	for ; sLen > 0; sLen-- {
		roll, _ := RandInt(0, sLen)
		result = append(result, temp[roll])
		temp = append(temp[0:roll], temp[roll+1:]...)
		if len(result) >= count {
			break
		}
	}
	return result
}
