package gofakeit

import (
	"bytes"
	"encoding/json"
	"encoding/xml"
	"errors"
	"math/rand"
	"reflect"
)

// XMLOptions defines values needed for json generation
type XMLOptions struct {
	Type          string  `json:"type" xml:"type" fake:"{randomstring:[array,single]}"` // single or array
	RootElement   string  `json:"root_element" xml:"root_element"`
	RecordElement string  `json:"record_element" xml:"record_element"`
	RowCount      int     `json:"row_count" xml:"row_count" fake:"{number:1,10}"`
	Indent        bool    `json:"indent" xml:"indent"`
	Fields        []Field `json:"fields" xml:"fields" fake:"{fields}"`
}

type xmlArray struct {
	XMLName xml.Name
	Array   []xmlMap
}

type xmlMap struct {
	XMLName  xml.Name
	KeyOrder []string
	Map      map[string]any `xml:",chardata"`
}

type xmlEntry struct {
	XMLName xml.Name
	Value   any `xml:",chardata"`
}

func (m xmlMap) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
	if len(m.Map) == 0 {
		return nil
	}

	start.Name = m.XMLName

	err := e.EncodeToken(start)
	if err != nil {
		return err
	}

	err = xmlMapLoop(e, &m)
	if err != nil {
		return err
	}

	return e.EncodeToken(start.End())
}

func xmlMapLoop(e *xml.Encoder, m *xmlMap) error {
	var err error

	// Check if xmlmap has key order if not create it
	// Get key order by order of fields array
	if m.KeyOrder == nil {
		m.KeyOrder = make([]string, len(m.Map))
		for k := range m.Map {
			m.KeyOrder = append(m.KeyOrder, k)
		}
	}

	for _, key := range m.KeyOrder {
		v := reflect.ValueOf(m.Map[key])

		// Always get underlyning Value of value
		if v.Kind() == reflect.Ptr {
			v = reflect.Indirect(v)
		}

		switch v.Kind() {
		case reflect.Bool,
			reflect.String,
			reflect.Int, reflect.Int8, reflect.Int32, reflect.Int64,
			reflect.Uint, reflect.Uint8, reflect.Uint32, reflect.Uint64,
			reflect.Float32, reflect.Float64:
			err = e.Encode(xmlEntry{XMLName: xml.Name{Local: key}, Value: m.Map[key]})
			if err != nil {
				return err
			}
		case reflect.Slice:
			e.EncodeToken(xml.StartElement{Name: xml.Name{Local: key}})
			for i := 0; i < v.Len(); i++ {
				err = e.Encode(xmlEntry{XMLName: xml.Name{Local: "value"}, Value: v.Index(i).String()})
				if err != nil {
					return err
				}
			}
			e.EncodeToken(xml.EndElement{Name: xml.Name{Local: key}})
		case reflect.Map:
			err = e.Encode(xmlMap{
				XMLName: xml.Name{Local: key},
				Map:     m.Map[key].(map[string]any),
			})
			if err != nil {
				return err
			}
		case reflect.Struct:
			// Convert struct to map[string]any
			// So we can rewrap element
			var inInterface map[string]any
			inrec, _ := json.Marshal(m.Map[key])
			json.Unmarshal(inrec, &inInterface)

			err = e.Encode(xmlMap{
				XMLName: xml.Name{Local: key},
				Map:     inInterface,
			})
			if err != nil {
				return err
			}
		default:
			err = e.Encode(m.Map[key])
			if err != nil {
				return err
			}
		}
	}

	return nil
}

// XML generates an object or an array of objects in json format
// A nil XMLOptions returns a randomly structured XML.
func XML(xo *XMLOptions) ([]byte, error) { return xmlFunc(globalFaker, xo) }

// XML generates an object or an array of objects in json format
// A nil XMLOptions returns a randomly structured XML.
func (f *Faker) XML(xo *XMLOptions) ([]byte, error) { return xmlFunc(f, xo) }

func xmlFunc(f *Faker, xo *XMLOptions) ([]byte, error) {
	if xo == nil {
		// We didn't get a XMLOptions, so create a new random one
		err := f.Struct(&xo)
		if err != nil {
			return nil, err
		}
	}

	// Check to make sure they passed in a type
	if xo.Type != "single" && xo.Type != "array" {
		return nil, errors.New("invalid type, must be array or object")
	}

	// Check fields length
	if xo.Fields == nil || len(xo.Fields) <= 0 {
		return nil, errors.New("must pass fields in order to build json object(s)")
	}

	// Check root element string
	if xo.RootElement == "" {
		xo.RecordElement = "xml"
	}

	// Check record element string
	if xo.RecordElement == "" {
		xo.RecordElement = "record"
	}

	// Get key order by order of fields array
	keyOrder := make([]string, len(xo.Fields))
	for _, f := range xo.Fields {
		keyOrder = append(keyOrder, f.Name)
	}

	if xo.Type == "single" {
		v := xmlMap{
			XMLName:  xml.Name{Local: xo.RootElement},
			KeyOrder: keyOrder,
			Map:      make(map[string]any),
		}

		// Loop through fields and add to them to map[string]any
		for _, field := range xo.Fields {
			// Get function info
			funcInfo := GetFuncLookup(field.Function)
			if funcInfo == nil {
				return nil, errors.New("invalid function, " + field.Function + " does not exist")
			}

			value, err := funcInfo.Generate(f.Rand, &field.Params, funcInfo)
			if err != nil {
				return nil, err
			}

			v.Map[field.Name] = value
		}

		// Marshal into bytes
		var b bytes.Buffer
		x := xml.NewEncoder(&b)
		if xo.Indent {
			x.Indent("", "    ")
		}
		err := x.Encode(v)
		if err != nil {
			return nil, err
		}

		return b.Bytes(), nil
	}

	if xo.Type == "array" {
		// Make sure you set a row count
		if xo.RowCount <= 0 {
			return nil, errors.New("must have row count")
		}

		xa := xmlArray{
			XMLName: xml.Name{Local: xo.RootElement},
			Array:   make([]xmlMap, xo.RowCount),
		}

		for i := 1; i <= int(xo.RowCount); i++ {
			v := xmlMap{
				XMLName:  xml.Name{Local: xo.RecordElement},
				KeyOrder: keyOrder,
				Map:      make(map[string]any),
			}

			// Loop through fields and add to them to map[string]any
			for _, field := range xo.Fields {
				if field.Function == "autoincrement" {
					v.Map[field.Name] = i
					continue
				}

				// Get function info
				funcInfo := GetFuncLookup(field.Function)
				if funcInfo == nil {
					return nil, errors.New("invalid function, " + field.Function + " does not exist")
				}

				value, err := funcInfo.Generate(f.Rand, &field.Params, funcInfo)
				if err != nil {
					return nil, err
				}

				v.Map[field.Name] = value
			}

			xa.Array = append(xa.Array, v)
		}

		// Marshal into bytes
		var b bytes.Buffer
		x := xml.NewEncoder(&b)
		if xo.Indent {
			x.Indent("", "    ")
		}
		err := x.Encode(xa)
		if err != nil {
			return nil, err
		}

		return b.Bytes(), nil
	}

	return nil, errors.New("invalid type, must be array or object")
}

func addFileXMLLookup() {
	AddFuncLookup("xml", Info{
		Display:     "XML",
		Category:    "file",
		Description: "Generates an single or an array of elements in xml format",
		Example: `<xml>
	<record>
		<first_name>Markus</first_name>
		<last_name>Moen</last_name>
		<password>Dc0VYXjkWABx</password>
	</record>
	<record>
		<first_name>Osborne</first_name>
		<last_name>Hilll</last_name>
		<password>XPJ9OVNbs5lm</password>
	</record>
</xml>`,
		Output:      "[]byte",
		ContentType: "application/xml",
		Params: []Param{
			{Field: "type", Display: "Type", Type: "string", Default: "single", Options: []string{"single", "array"}, Description: "Type of XML, single or array"},
			{Field: "rootelement", Display: "Root Element", Type: "string", Default: "xml", Description: "Root element wrapper name"},
			{Field: "recordelement", Display: "Record Element", Type: "string", Default: "record", Description: "Record element for each record row"},
			{Field: "rowcount", Display: "Row Count", Type: "int", Default: "100", Description: "Number of rows in JSON array"},
			{Field: "indent", Display: "Indent", Type: "bool", Default: "false", Description: "Whether or not to add indents and newlines"},
			{Field: "fields", Display: "Fields", Type: "[]Field", Description: "Fields containing key name and function to run in json format"},
		},
		Generate: func(r *rand.Rand, m *MapParams, info *Info) (any, error) {
			xo := XMLOptions{}

			typ, err := info.GetString(m, "type")
			if err != nil {
				return nil, err
			}
			xo.Type = typ

			rootElement, err := info.GetString(m, "rootelement")
			if err != nil {
				return nil, err
			}
			xo.RootElement = rootElement

			recordElement, err := info.GetString(m, "recordelement")
			if err != nil {
				return nil, err
			}
			xo.RecordElement = recordElement

			rowcount, err := info.GetInt(m, "rowcount")
			if err != nil {
				return nil, err
			}
			xo.RowCount = rowcount

			fieldsStr, err := info.GetStringArray(m, "fields")
			if err != nil {
				return nil, err
			}

			indent, err := info.GetBool(m, "indent")
			if err != nil {
				return nil, err
			}
			xo.Indent = indent

			// Check to make sure fields has length
			if len(fieldsStr) > 0 {
				xo.Fields = make([]Field, len(fieldsStr))

				for i, f := range fieldsStr {
					// Unmarshal fields string into fields array
					err = json.Unmarshal([]byte(f), &xo.Fields[i])
					if err != nil {
						return nil, errors.New("unable to decode json string")
					}
				}
			}

			f := &Faker{Rand: r}
			return xmlFunc(f, &xo)
		},
	})
}