package gofakeit

import (
	"encoding/json"
	"fmt"
	"math/rand"
	"reflect"
	"strconv"
	"strings"
	"sync"
)

// FuncLookups is the primary map array with mapping to all available data
var FuncLookups map[string]Info
var lockFuncLookups sync.Mutex

// MapParams is the values to pass into a lookup generate
type MapParams map[string]MapParamsValue

type MapParamsValue []string

// Info structures fields to better break down what each one generates
type Info struct {
	Display     string                                                    `json:"display"`
	Category    string                                                    `json:"category"`
	Description string                                                    `json:"description"`
	Example     string                                                    `json:"example"`
	Output      string                                                    `json:"output"`
	ContentType string                                                    `json:"content_type"`
	Params      []Param                                                   `json:"params"`
	Any         any                                                       `json:"any"`
	Generate    func(r *rand.Rand, m *MapParams, info *Info) (any, error) `json:"-"`
}

// Param is a breakdown of param requirements and type definition
type Param struct {
	Field       string   `json:"field"`
	Display     string   `json:"display"`
	Type        string   `json:"type"`
	Optional    bool     `json:"optional"`
	Default     string   `json:"default"`
	Options     []string `json:"options"`
	Description string   `json:"description"`
}

// Field is used for defining what name and function you to generate for file outuputs
type Field struct {
	Name     string    `json:"name"`
	Function string    `json:"function"`
	Params   MapParams `json:"params"`
}

func init() { initLookup() }

// init will add all the functions to MapLookups
func initLookup() {
	addAddressLookup()
	addAnimalLookup()
	addAppLookup()
	addAuthLookup()
	addBeerLookup()
	addBookLookup()
	addCarLookup()
	addCelebrityLookup()
	addColorLookup()
	addCompanyLookup()
	addDatabaseSQLLookup()
	addDateTimeLookup()
	addEmojiLookup()
	addErrorLookup()
	addFileCSVLookup()
	addFileJSONLookup()
	addFileLookup()
	addFileXMLLookup()
	addFinanceLookup()
	addFoodLookup()
	addGameLookup()
	addGenerateLookup()
	addHackerLookup()
	addHipsterLookup()
	addHtmlLookup()
	addImageLookup()
	addInternetLookup()
	addLanguagesLookup()
	addLoremLookup()
	addMinecraftLookup()
	addMiscLookup()
	addMovieLookup()
	addNumberLookup()
	addPaymentLookup()
	addPersonLookup()
	addProductLookup()
	addSchoolLookup()
	addStringLookup()
	addTemplateLookup()
	addWeightedLookup()
	addWordAdjectiveLookup()
	addWordAdverbLookup()
	addWordConnectiveLookup()
	addWordGeneralLookup()
	addWordGrammerLookup()
	addWordNounLookup()
	addWordPhraseLookup()
	addWordPrepositionLookup()
	addWordPronounLookup()
	addWordSentenceLookup()
	addWordVerbLookup()
	addWordCommentLookup()
	addWordMiscLookup()
}

// internalFuncLookups is the internal map array with mapping to all available data
var internalFuncLookups map[string]Info = map[string]Info{
	"fields": {
		Description: "Example fields for generating csv, json, xml, etc",
		Output:      "gofakeit.Field",
		Generate: func(r *rand.Rand, m *MapParams, info *Info) (any, error) {
			function, _ := GetRandomSimpleFunc(r)
			return Field{
				Name:     function,
				Function: function,
			}, nil
		},
	},
}

// NewMapParams will create a new MapParams
func NewMapParams() *MapParams {
	return &MapParams{}
}

// Add will take in a field and value and add it to the map params type
func (m *MapParams) Add(field string, value string) {
	_, ok := (*m)[field]
	if !ok {
		(*m)[field] = []string{value}
		return
	}

	(*m)[field] = append((*m)[field], value)
}

// Get will return the array of string from the provided field
func (m *MapParams) Get(field string) []string {
	return (*m)[field]
}

// Size will return the total size of the underlying map
func (m *MapParams) Size() int {
	size := 0
	for range *m {
		size++
	}
	return size
}

// UnmarshalJSON will unmarshal the json into the []string
func (m *MapParamsValue) UnmarshalJSON(data []byte) error {
	// check if the data is an array
	// if so, marshal it into m
	if data[0] == '[' {
		var values []any
		err := json.Unmarshal(data, &values)
		if err != nil {
			return err
		}

		// convert the values to array of strings
		for _, value := range values {
			typeOf := reflect.TypeOf(value).Kind().String()

			if typeOf == "map" {
				v, err := json.Marshal(value)
				if err != nil {
					return err
				}
				*m = append(*m, string(v))
			} else {
				*m = append(*m, fmt.Sprintf("%v", value))
			}
		}
		return nil
	}

	// if not, then convert into a string and add it to m
	var s any
	if err := json.Unmarshal(data, &s); err != nil {
		return err
	}

	*m = append(*m, fmt.Sprintf("%v", s))
	return nil
}

func GetRandomSimpleFunc(r *rand.Rand) (string, Info) {
	// Loop through all the functions and add them to a slice
	var keys []string
	for k, info := range FuncLookups {
		// Only grab simple functions
		if info.Params == nil {
			keys = append(keys, k)
		}
	}

	// Randomly grab a function from the slice
	randomKey := randomString(r, keys)

	// Return the function name and info
	return randomKey, FuncLookups[randomKey]
}

// AddFuncLookup takes a field and adds it to map
func AddFuncLookup(functionName string, info Info) {
	if FuncLookups == nil {
		FuncLookups = make(map[string]Info)
	}

	// Check content type
	if info.ContentType == "" {
		info.ContentType = "text/plain"
	}

	lockFuncLookups.Lock()
	FuncLookups[functionName] = info
	lockFuncLookups.Unlock()
}

// GetFuncLookup will lookup
func GetFuncLookup(functionName string) *Info {
	var info Info
	var ok bool

	// Check internal functions first
	info, ok = internalFuncLookups[functionName]
	if ok {
		return &info
	}

	info, ok = FuncLookups[functionName]
	if ok {
		return &info
	}

	return nil
}

// RemoveFuncLookup will remove a function from lookup
func RemoveFuncLookup(functionName string) {
	_, ok := FuncLookups[functionName]
	if !ok {
		return
	}

	lockFuncLookups.Lock()
	delete(FuncLookups, functionName)
	lockFuncLookups.Unlock()
}

// GetAny will retrieve Any field from Info
func (i *Info) GetAny(m *MapParams, field string) (any, error) {
	_, value, err := i.GetField(m, field)
	if err != nil {
		return nil, err
	}

	var anyValue any

	// Try to convert to int
	valueInt, err := strconv.ParseInt(value[0], 10, 64)
	if err == nil {
		return int(valueInt), nil
	}

	// Try to convert to float
	valueFloat, err := strconv.ParseFloat(value[0], 64)
	if err == nil {
		return valueFloat, nil
	}

	// Try to convert to boolean
	valueBool, err := strconv.ParseBool(value[0])
	if err == nil {
		return valueBool, nil
	}

	err = json.Unmarshal([]byte(value[0]), &anyValue)
	if err == nil {
		return valueBool, nil
	}

	return value[0], nil
}

// GetMap will retrieve map[string]any field from data
func (i *Info) GetMap(m *MapParams, field string) (map[string]any, error) {
	_, value, err := i.GetField(m, field)
	if err != nil {
		return nil, err
	}

	var mapValue map[string]any
	err = json.Unmarshal([]byte(value[0]), &mapValue)
	if err != nil {
		return nil, fmt.Errorf("%s field could not parse to map[string]any", field)
	}

	return mapValue, nil
}

// GetField will retrieve field from data
func (i *Info) GetField(m *MapParams, field string) (*Param, []string, error) {
	// Get param
	var p *Param
	for _, param := range i.Params {
		if param.Field == field {
			p = &param
			break
		}
	}
	if p == nil {
		return nil, nil, fmt.Errorf("could not find param field %s", field)
	}

	// Get value from map
	if m != nil {
		value, ok := (*m)[field]
		if !ok {
			// If default isnt empty use default
			if p.Default != "" {
				return p, []string{p.Default}, nil
			}

			return nil, nil, fmt.Errorf("could not find field: %s", field)
		}

		return p, value, nil
	} else if m == nil && p.Default != "" {
		// If p.Type is []uint, then we need to convert it to []string
		if strings.HasPrefix(p.Default, "[") {
			// Remove [] from type
			defaultClean := p.Default[1 : len(p.Default)-1]

			// Split on comma
			defaultSplit := strings.Split(defaultClean, ",")

			return p, defaultSplit, nil
		}

		// If default isnt empty use default
		return p, []string{p.Default}, nil
	}

	return nil, nil, fmt.Errorf("could not find field: %s", field)
}

// GetBool will retrieve boolean field from data
func (i *Info) GetBool(m *MapParams, field string) (bool, error) {
	p, value, err := i.GetField(m, field)
	if err != nil {
		return false, err
	}

	// Try to convert to boolean
	valueBool, err := strconv.ParseBool(value[0])
	if err != nil {
		return false, fmt.Errorf("%s field could not parse to bool value", p.Field)
	}

	return valueBool, nil
}

// GetInt will retrieve int field from data
func (i *Info) GetInt(m *MapParams, field string) (int, error) {
	p, value, err := i.GetField(m, field)
	if err != nil {
		return 0, err
	}

	// Try to convert to int
	valueInt, err := strconv.ParseInt(value[0], 10, 64)
	if err != nil {
		return 0, fmt.Errorf("%s field could not parse to int value", p.Field)
	}

	return int(valueInt), nil
}

// GetUint will retrieve uint field from data
func (i *Info) GetUint(m *MapParams, field string) (uint, error) {
	p, value, err := i.GetField(m, field)
	if err != nil {
		return 0, err
	}

	// Try to convert to int
	valueUint, err := strconv.ParseUint(value[0], 10, 64)
	if err != nil {
		return 0, fmt.Errorf("%s field could not parse to int value", p.Field)
	}

	return uint(valueUint), nil
}

// GetFloat32 will retrieve int field from data
func (i *Info) GetFloat32(m *MapParams, field string) (float32, error) {
	p, value, err := i.GetField(m, field)
	if err != nil {
		return 0, err
	}

	// Try to convert to float
	valueFloat, err := strconv.ParseFloat(value[0], 32)
	if err != nil {
		return 0, fmt.Errorf("%s field could not parse to float value", p.Field)
	}

	return float32(valueFloat), nil
}

// GetFloat64 will retrieve int field from data
func (i *Info) GetFloat64(m *MapParams, field string) (float64, error) {
	p, value, err := i.GetField(m, field)
	if err != nil {
		return 0, err
	}

	// Try to convert to float
	valueFloat, err := strconv.ParseFloat(value[0], 64)
	if err != nil {
		return 0, fmt.Errorf("%s field could not parse to float value", p.Field)
	}

	return valueFloat, nil
}

// GetString will retrieve string field from data
func (i *Info) GetString(m *MapParams, field string) (string, error) {
	_, value, err := i.GetField(m, field)
	if err != nil {
		return "", err
	}

	return value[0], nil
}

// GetStringArray will retrieve []string field from data
func (i *Info) GetStringArray(m *MapParams, field string) ([]string, error) {
	_, values, err := i.GetField(m, field)
	if err != nil {
		return nil, err
	}

	return values, nil
}

// GetIntArray will retrieve []int field from data
func (i *Info) GetIntArray(m *MapParams, field string) ([]int, error) {
	_, value, err := i.GetField(m, field)
	if err != nil {
		return nil, err
	}

	var ints []int
	for i := 0; i < len(value); i++ {
		valueInt, err := strconv.ParseInt(value[i], 10, 64)
		if err != nil {
			return nil, fmt.Errorf("%s value could not parse to int", value[i])
		}
		ints = append(ints, int(valueInt))
	}

	return ints, nil
}

// GetUintArray will retrieve []uint field from data
func (i *Info) GetUintArray(m *MapParams, field string) ([]uint, error) {
	_, value, err := i.GetField(m, field)
	if err != nil {
		return nil, err
	}

	var uints []uint
	for i := 0; i < len(value); i++ {
		valueUint, err := strconv.ParseUint(value[i], 10, 64)
		if err != nil {
			return nil, fmt.Errorf("%s value could not parse to uint", value[i])
		}
		uints = append(uints, uint(valueUint))
	}

	return uints, nil
}

// GetFloat32Array will retrieve []float field from data
func (i *Info) GetFloat32Array(m *MapParams, field string) ([]float32, error) {
	_, value, err := i.GetField(m, field)
	if err != nil {
		return nil, err
	}

	var floats []float32
	for i := 0; i < len(value); i++ {
		valueFloat, err := strconv.ParseFloat(value[i], 32)
		if err != nil {
			return nil, fmt.Errorf("%s value could not parse to float", value[i])
		}
		floats = append(floats, float32(valueFloat))
	}

	return floats, nil
}
