package langext

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

type AnyBaseConverter struct {
	base    uint64
	charset []rune
}

func NewAnyBaseConverter(cs string) AnyBaseConverter {
	rcs := []rune(cs)
	return AnyBaseConverter{
		base:    uint64(len(rcs)),
		charset: rcs,
	}
}

func (bc AnyBaseConverter) Rand(rlen int) string {
	biBase := big.NewInt(int64(bc.base))

	randMax := big.NewInt(math.MaxInt64)

	r := ""

	for i := 0; i < rlen; i++ {
		v, err := rand.Int(rand.Reader, randMax)
		if err != nil {
			panic(err)
		}

		r += string(bc.charset[v.Mod(v, biBase).Int64()])
	}

	return r
}

func (bc AnyBaseConverter) EncodeUInt64(num uint64) string {
	if num == 0 {
		return "0"
	}

	b := ""

	// loop as long the num is bigger than zero
	for num > 0 {
		r := num % bc.base

		num -= r
		num /= base62Base

		b += string(bc.charset[int(r)])
	}

	return b
}

func (bc AnyBaseConverter) DecodeUInt64(str string) (uint64, error) {
	if str == "" {
		return 0, errors.New("empty string")
	}

	result := uint64(0)

	for _, v := range str {
		result *= base62Base

		pos := ArrFirstIndex(bc.charset, v)
		if pos == -1 {
			return 0, errors.New("invalid character: " + string(v))
		}

		result += uint64(pos)
	}

	return result, nil
}

func (bc AnyBaseConverter) Encode(src []byte) string {
	value := new(big.Int)
	value.SetBytes(src)
	return bc.EncodeBigInt(value)
}

func (bc AnyBaseConverter) EncodeBigInt(src *big.Int) string {
	value := new(big.Int)
	value.Set(src)

	isneg := value.Sign() < 0

	answer := ""

	if isneg {
		value.Neg(value)
	}

	biBase := big.NewInt(int64(bc.base))

	rem := new(big.Int)

	for value.Sign() > 0 {
		value.QuoRem(value, biBase, rem)
		answer = string(bc.charset[rem.Int64()]) + answer
	}

	if isneg {
		return "-" + answer
	} else {
		return answer
	}
}

func (bc AnyBaseConverter) Decode(src string) ([]byte, error) {
	value, err := bc.DecodeToBigInt(src)
	if err != nil {
		return nil, err
	}
	return value.Bytes(), nil
}

func (bc AnyBaseConverter) DecodeToBigInt(_src string) (*big.Int, error) {
	result := new(big.Int)
	result.SetInt64(0)

	src := []rune(_src)

	if len(src) == 0 {
		return nil, errors.New("string is empty")
	}
	if bc.base < 2 {
		return nil, errors.New("not enough digits")
	}

	i := 0

	sign := new(big.Int)
	sign.SetInt64(1)
	if src[i] == '+' {
		i++
	} else if src[i] == '-' {
		i++
		sign.SetInt64(-1)
	}

	if i >= len(src) {
		return nil, errors.New("no digits in input")
	}

	biBase := big.NewInt(int64(bc.base))

	oldResult := new(big.Int)

	for ; i < len(src); i++ {
		n := ArrFirstIndex(bc.charset, src[i])
		if n < 0 {
			return nil, errors.New("invalid characters in input")
		}

		oldResult.Set(result)

		result.Mul(result, biBase)
		result.Add(result, big.NewInt(int64(n)))

		if result.Cmp(oldResult) < 0 {
			return nil, errors.New("overflow")
		}
	}

	if sign.Cmp(big.NewInt(0)) < 0 {
		result.Neg(result)
	}

	return result, nil
}