// Package encodertest provides test helpers for testing package encoder
package encodertest

import (
	"errors"
	"fmt"
	"log"
	"reflect"

	mathrand "math/rand"

	"github.com/skycoin/skycoin/src/cipher/encoder"
)

// PopulateRandomOptions are configuration options for the PopulateRandom method
type PopulateRandomOptions struct {
	// Maximum random length for strings, slices, maps. If a string, slice or map has a `maxlen` struct tag, the lesser of the two will be used
	MaxRandLen int
	// Minimum random length for strings, slices, maps
	MinRandLen int
	// Leave slices as nil if they are empty
	EmptySliceNil bool
	// Leaves maps as nil if they are empty
	EmptyMapNil bool
	// Leave an omitempty field empty
	ForceEmptyOmitEmpty bool
	// Don't populate maps
	IgnoreMaps bool
}

// PopulateRandom constructs an object with random data.
// This is used by the autogenerated skyencoder tests and can be useful in other testing.
func PopulateRandom(obj interface{}, rand *mathrand.Rand, options PopulateRandomOptions) error {
	if options.MaxRandLen < options.MinRandLen {
		return errors.New("MaxRandLen must be >= MinRandLen")
	}

	v := reflect.ValueOf(obj)
	switch v.Kind() {
	case reflect.Ptr:
		v = v.Elem()
	case reflect.Map:
	default:
		return fmt.Errorf("DeserializeRaw value must be a ptr, is %s", v.Kind().String())
	}

	return populate(v, rand, options, 0)
}

func populate(v reflect.Value, rand *mathrand.Rand, options PopulateRandomOptions, maxlen int) error {
	kind := v.Kind()
	switch kind {
	case reflect.Array:
		length := v.Len()
		for i := 0; i < length; i++ {
			if err := populate(v.Index(i), rand, options, 0); err != nil {
				return err
			}
		}

	case reflect.Map:
		length := 0

		if !options.IgnoreMaps {
			length = randLen(rand, options, maxlen)
		}

		if length == 0 && options.EmptyMapNil {
			return nil
		}

		t := v.Type()

		if v.IsNil() {
			v.Set(reflect.Indirect(reflect.MakeMap(t)))
		}

		key := t.Key()
		elem := t.Elem()
		for i := 0; i < length; i++ {
			keyv := reflect.Indirect(reflect.New(key))
			elemv := reflect.Indirect(reflect.New(elem))
			if err := populate(keyv, rand, options, 0); err != nil {
				return err
			}
			if err := populate(elemv, rand, options, 0); err != nil {
				return err
			}
			v.SetMapIndex(keyv, elemv)
		}

	case reflect.Slice:
		length := randLen(rand, options, maxlen)

		if length == 0 && options.EmptySliceNil {
			return nil
		}

		t := v.Type()
		elemvs := reflect.MakeSlice(t, length, length)
		for i := 0; i < length; i++ {
			elemv := reflect.Indirect(elemvs.Index(i))
			if err := populate(elemv, rand, options, 0); err != nil {
				return err
			}
		}
		v.Set(elemvs)

	case reflect.Struct:
		t := v.Type()
		nFields := v.NumField()
		for i := 0; i < nFields; i++ {
			ff := t.Field(i)
			// Skip unexported fields
			if ff.PkgPath != "" {
				continue
			}

			tag := ff.Tag.Get("enc")
			omitempty := encoder.TagOmitempty(tag)

			if omitempty && i != nFields-1 {
				log.Panic(encoder.ErrInvalidOmitEmpty)
			}

			if len(tag) > 0 && tag[0] == '-' {
				continue
			}

			fv := v.Field(i)
			if fv.CanSet() && ff.Name != "_" {
				maxlen := encoder.TagMaxLen(tag)

				opts := options
				if omitempty && opts.ForceEmptyOmitEmpty {
					opts.MaxRandLen = 0
					opts.MinRandLen = 0
				}

				if err := populate(fv, rand, opts, maxlen); err != nil {
					return err
				}
			}
		}

	case reflect.String:
		length := randLen(rand, options, maxlen)
		v.SetString(randString(rand, length))

	case reflect.Bool:
		v.SetBool(rand.Uint64()%2 == 0)

	// rand.Int63 is not used because it returns a non-negative integer
	case reflect.Int8:
		v.SetInt(int64(rand.Uint64()))
	case reflect.Int16:
		v.SetInt(int64(rand.Uint64()))
	case reflect.Int32:
		v.SetInt(int64(rand.Uint64()))
	case reflect.Int64:
		v.SetInt(int64(rand.Uint64()))

	case reflect.Uint8:
		v.SetUint(rand.Uint64())
	case reflect.Uint16:
		v.SetUint(rand.Uint64())
	case reflect.Uint32:
		v.SetUint(rand.Uint64())
	case reflect.Uint64:
		v.SetUint(rand.Uint64())

	case reflect.Float32:
		v.SetFloat(rand.Float64())
	case reflect.Float64:
		v.SetFloat(rand.Float64())

	default:
		log.Panicf("Decode error: kind %s not handled", v.Kind().String())
	}

	return nil
}

func randLen(rand *mathrand.Rand, options PopulateRandomOptions, maxlen int) int {
	max := options.MaxRandLen
	if maxlen != 0 && maxlen < max {
		max = maxlen
	}

	min := options.MinRandLen
	if maxlen != 0 && maxlen < min {
		min = maxlen
	}

	return rand.Intn(max-min+1) + min
}

const printableChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!@#$%^&*()-=_+[]{}\\|:\";'<>,./?`~\n\t "

func randString(rand *mathrand.Rand, n int) string {
	b := make([]byte, n)
	for i := range b {
		b[i] = printableChars[rand.Int63()%int64(len(printableChars))]
	}
	return string(b)
}
