// Copyright (c) 2015 The btcsuite developers
// Copyright (c) 2015-2022 The Decred developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.

package dcrjson

import (
	"bytes"
	"fmt"
	"reflect"
	"strings"
	"text/tabwriter"
)

// baseHelpDescs house the various help labels, types, and example values used
// when generating help.  The per-command synopsis, field descriptions,
// conditions, and result descriptions are to be provided by the caller.
var baseHelpDescs = map[string]string{
	// Misc help labels and output.
	"help-arguments":      "Arguments",
	"help-arguments-none": "None",
	"help-result":         "Result",
	"help-result-nothing": "Nothing",
	"help-default":        "default",
	"help-optional":       "optional",
	"help-required":       "required",

	// JSON types.
	"json-type-numeric": "numeric",
	"json-type-string":  "string",
	"json-type-bool":    "boolean",
	"json-type-array":   "array of ",
	"json-type-object":  "object",
	"json-type-value":   "value",

	// JSON examples.
	"json-example-string":   "value",
	"json-example-bool":     "true|false",
	"json-example-map-data": "data",
	"json-example-unknown":  "unknown",
}

// descLookupFunc is a function which is used to lookup a description given
// a key.
type descLookupFunc func(string) string

// reflectTypeToJSONType returns a string that represents the JSON type
// associated with the provided Go type.
func reflectTypeToJSONType(xT descLookupFunc, rt reflect.Type) string {
	kind := rt.Kind()
	if isNumeric(kind) {
		return xT("json-type-numeric")
	}

	switch kind {
	case reflect.String:
		return xT("json-type-string")

	case reflect.Bool:
		return xT("json-type-bool")

	case reflect.Array, reflect.Slice:
		return xT("json-type-array") + reflectTypeToJSONType(xT,
			rt.Elem())

	case reflect.Struct:
		return xT("json-type-object")

	case reflect.Map:
		return xT("json-type-object")
	}

	return xT("json-type-value")
}

// resultStructHelp returns a slice of strings containing the result help output
// for a struct.  Each line makes use of tabs to separate the relevant pieces so
// a tabwriter can be used later to line everything up.  The descriptions are
// pulled from the active help descriptions map based on the lowercase version
// of the provided reflect type and json name (or the lowercase version of the
// field name if no json tag was specified).
func resultStructHelp(xT descLookupFunc, rt reflect.Type, indentLevel int) []string {
	indent := strings.Repeat(" ", indentLevel)
	typeName := strings.ToLower(rt.Name())

	// Generate the help for each of the fields in the result struct.
	numField := rt.NumField()
	results := make([]string, 0, numField)
	for i := 0; i < numField; i++ {
		rtf := rt.Field(i)

		// The field name to display is the json name when it's
		// available, otherwise use the lowercase field name.
		var fieldName string
		if tag := rtf.Tag.Get("json"); tag != "" {
			fieldName = strings.Split(tag, ",")[0]
		} else {
			fieldName = strings.ToLower(rtf.Name)
		}

		// Deference pointer if needed.
		rtfType := rtf.Type
		if rtfType.Kind() == reflect.Ptr {
			rtfType = rtf.Type.Elem()
		}

		// Generate the JSON example for the result type of this struct
		// field.  When it is a complex type, examine the type and
		// adjust the opening bracket and brace combination accordingly.
		fieldType := reflectTypeToJSONType(xT, rtfType)
		fieldDescKey := typeName + "-" + fieldName
		fieldExamples, isComplex := reflectTypeToJSONExample(xT,
			rtfType, indentLevel, fieldDescKey)
		if isComplex {
			var brace string
			kind := rtfType.Kind()
			if kind == reflect.Array || kind == reflect.Slice {
				brace = "[{"
			} else {
				brace = "{"
			}
			result := fmt.Sprintf("%s\"%s\": %s\t(%s)\t%s", indent,
				fieldName, brace, fieldType, xT(fieldDescKey))
			results = append(results, result)
			results = append(results, fieldExamples...)
		} else {
			result := fmt.Sprintf("%s\"%s\": %s,\t(%s)\t%s", indent,
				fieldName, fieldExamples[0], fieldType,
				xT(fieldDescKey))
			results = append(results, result)
		}
	}

	return results
}

// reflectTypeToJSONExample generates example usage in the format used by the
// help output.  It handles arrays, slices and structs recursively.  The output
// is returned as a slice of lines so the final help can be nicely aligned via
// a tab writer.  A bool is also returned which specifies whether or not the
// type results in a complex JSON object since they need to be handled
// differently.
func reflectTypeToJSONExample(xT descLookupFunc, rt reflect.Type, indentLevel int, fieldDescKey string) ([]string, bool) {
	// Indirect pointer if needed.
	if rt.Kind() == reflect.Ptr {
		rt = rt.Elem()
	}
	kind := rt.Kind()
	if isNumeric(kind) {
		if kind == reflect.Float32 || kind == reflect.Float64 {
			return []string{"n.nnn"}, false
		}

		return []string{"n"}, false
	}

	switch kind {
	case reflect.String:
		return []string{`"` + xT("json-example-string") + `"`}, false

	case reflect.Bool:
		return []string{xT("json-example-bool")}, false

	case reflect.Struct:
		indent := strings.Repeat(" ", indentLevel)
		results := resultStructHelp(xT, rt, indentLevel+1)

		// An opening brace is needed for the first indent level.  For
		// all others, it will be included as a part of the previous
		// field.
		if indentLevel == 0 {
			newResults := make([]string, len(results)+1)
			newResults[0] = "{"
			copy(newResults[1:], results)
			results = newResults
		}

		// The closing brace has a comma after it except for the first
		// indent level.  The final tabs are necessary so the tab writer
		// lines things up properly.
		closingBrace := indent + "}"
		if indentLevel > 0 {
			closingBrace += ","
		}
		results = append(results, closingBrace+"\t\t")
		return results, true

	case reflect.Array, reflect.Slice:
		results, isComplex := reflectTypeToJSONExample(xT, rt.Elem(),
			indentLevel, fieldDescKey)

		// When the result is complex, it is because this is an array of
		// objects.
		if isComplex {
			// When this is at indent level zero, there is no
			// previous field to house the opening array bracket, so
			// replace the opening object brace with the array
			// syntax.  Also, replace the final closing object brace
			// with the variadic array closing syntax.
			indent := strings.Repeat(" ", indentLevel)
			if indentLevel == 0 {
				results[0] = indent + "[{"
				results[len(results)-1] = indent + "},...]"
				return results, true
			}

			// At this point, the indent level is greater than 0, so
			// the opening array bracket and object brace are
			// already a part of the previous field.  However, the
			// closing entry is a simple object brace, so replace it
			// with the variadic array closing syntax.  The final
			// tabs are necessary so the tab writer lines things up
			// properly.
			results[len(results)-1] = indent + "},...],\t\t"
			return results, true
		}

		// It's an array of primitives, so return the formatted text
		// accordingly.
		return []string{fmt.Sprintf("[%s,...]", results[0])}, false

	case reflect.Map:
		indent := strings.Repeat(" ", indentLevel)
		results := make([]string, 0, 3)

		// An opening brace is needed for the first indent level.  For
		// all others, it will be included as a part of the previous
		// field.
		if indentLevel == 0 {
			results = append(results, indent+"{")
		}

		// Maps are a bit special in that they need to have the key,
		// value, and description of the object entry specifically
		// called out.
		innerIndent := strings.Repeat(" ", indentLevel+1)
		result := fmt.Sprintf("%s%q: %s, (%s) %s", innerIndent,
			xT(fieldDescKey+"--key"), xT(fieldDescKey+"--value"),
			reflectTypeToJSONType(xT, rt), xT(fieldDescKey+"--desc"))
		results = append(results, result)
		results = append(results, innerIndent+"...")

		results = append(results, indent+"}")
		return results, true
	}

	return []string{xT("json-example-unknown")}, false
}

// resultTypeHelp generates and returns formatted help for the provided result
// type.
func resultTypeHelp(xT descLookupFunc, rt reflect.Type, fieldDescKey string) string {
	// Generate the JSON example for the result type.
	results, isComplex := reflectTypeToJSONExample(xT, rt, 0, fieldDescKey)

	// When this is a primitive type, add the associated JSON type and
	// result description into the final string, format it accordingly,
	// and return it.
	if !isComplex {
		return fmt.Sprintf("%s (%s) %s", results[0],
			reflectTypeToJSONType(xT, rt), xT(fieldDescKey))
	}

	// At this point, this is a complex type that already has the JSON types
	// and descriptions in the results.  Thus, use a tab writer to nicely
	// align the help text.
	var formatted bytes.Buffer
	w := new(tabwriter.Writer)
	w.Init(&formatted, 0, 4, 1, ' ', 0)
	for i, text := range results {
		if i == len(results)-1 {
			fmt.Fprint(w, text)
		} else {
			fmt.Fprintln(w, text)
		}
	}
	w.Flush()
	return formatted.String()
}

// argTypeHelp returns the type of provided command argument as a string in the
// format used by the help output.  In particular, it includes the JSON type
// (boolean, numeric, string, array, object) along with optional and the default
// value if applicable.
func argTypeHelp(xT descLookupFunc, structField reflect.StructField, defaultVal *reflect.Value) string {
	// Indirect the pointer if needed and track if it's an optional field.
	fieldType := structField.Type
	var isOptional bool
	if fieldType.Kind() == reflect.Ptr {
		fieldType = fieldType.Elem()
		isOptional = true
	}

	// When there is a default value, it must also be a pointer due to the
	// rules enforced by RegisterCmd.
	if defaultVal != nil {
		indirect := defaultVal.Elem()
		defaultVal = &indirect
	}

	// Convert the field type to a JSON type.
	details := make([]string, 0, 3)
	details = append(details, reflectTypeToJSONType(xT, fieldType))

	// Add optional and default value to the details if needed.
	if isOptional {
		details = append(details, xT("help-optional"))

		// Add the default value if there is one.  This is only checked
		// when the field is optional since a non-optional field can't
		// have a default value.
		if defaultVal != nil {
			val := defaultVal.Interface()
			if defaultVal.Kind() == reflect.String {
				val = fmt.Sprintf(`"%s"`, val)
			}
			str := fmt.Sprintf("%s=%v", xT("help-default"), val)
			details = append(details, str)
		}
	} else {
		details = append(details, xT("help-required"))
	}

	return strings.Join(details, ", ")
}

// argHelp generates and returns formatted help for the provided command.
func argHelp(xT descLookupFunc, rtp reflect.Type, defaults map[int]reflect.Value, method string) string {
	// Return now if the command has no arguments.
	rt := rtp.Elem()
	numFields := rt.NumField()
	if numFields == 0 {
		return ""
	}

	// Generate the help for each argument in the command.  Several
	// simplifying assumptions are made here because the RegisterCmd
	// function has already rigorously enforced the layout.
	args := make([]string, 0, numFields)
	for i := 0; i < numFields; i++ {
		rtf := rt.Field(i)
		var defaultVal *reflect.Value
		if defVal, ok := defaults[i]; ok {
			defaultVal = &defVal
		}

		fieldName := strings.ToLower(rtf.Name)
		helpText := fmt.Sprintf("%d.\t%s\t(%s)\t%s", i+1, fieldName,
			argTypeHelp(xT, rtf, defaultVal),
			xT(method+"-"+fieldName))
		args = append(args, helpText)

		// For types which require a JSON object, or an array of JSON
		// objects, generate the full syntax for the argument.
		fieldType := rtf.Type
		if fieldType.Kind() == reflect.Ptr {
			fieldType = fieldType.Elem()
		}
		kind := fieldType.Kind()
		switch kind {
		case reflect.Struct:
			fieldDescKey := fmt.Sprintf("%s-%s", method, fieldName)
			resultText := resultTypeHelp(xT, fieldType, fieldDescKey)
			args = append(args, resultText)

		case reflect.Map:
			fieldDescKey := fmt.Sprintf("%s-%s", method, fieldName)
			resultText := resultTypeHelp(xT, fieldType, fieldDescKey)
			args = append(args, resultText)

		case reflect.Array, reflect.Slice:
			fieldDescKey := fmt.Sprintf("%s-%s", method, fieldName)
			if rtf.Type.Elem().Kind() == reflect.Struct {
				resultText := resultTypeHelp(xT, fieldType,
					fieldDescKey)
				args = append(args, resultText)
			}
		}
	}

	// Add argument names, types, and descriptions if there are any.  Use a
	// tab writer to nicely align the help text.
	var formatted bytes.Buffer
	w := new(tabwriter.Writer)
	w.Init(&formatted, 0, 4, 1, ' ', 0)
	for _, text := range args {
		fmt.Fprintln(w, text)
	}
	w.Flush()
	return formatted.String()
}

// methodHelp generates and returns the help output for the provided command
// and method info.  This is the main work horse for the exported MethodHelp
// function.
func methodHelp(xT descLookupFunc, rtp reflect.Type, defaults map[int]reflect.Value, method string, resultTypes []interface{}) string {
	// Start off with the method usage and help synopsis.
	help := fmt.Sprintf("%s\n\n%s\n", methodUsageText(rtp, defaults, method),
		xT(method+"--synopsis"))

	// Generate the help for each argument in the command.
	if argText := argHelp(xT, rtp, defaults, method); argText != "" {
		help += fmt.Sprintf("\n%s:\n%s", xT("help-arguments"),
			argText)
	} else {
		help += fmt.Sprintf("\n%s:\n%s\n", xT("help-arguments"),
			xT("help-arguments-none"))
	}

	// Generate the help text for each result type.
	resultTexts := make([]string, 0, len(resultTypes))
	for i := range resultTypes {
		rtp := reflect.TypeOf(resultTypes[i])
		fieldDescKey := fmt.Sprintf("%s--result%d", method, i)
		if resultTypes[i] == nil {
			resultText := xT("help-result-nothing")
			resultTexts = append(resultTexts, resultText)
			continue
		}

		resultText := resultTypeHelp(xT, rtp.Elem(), fieldDescKey)
		resultTexts = append(resultTexts, resultText)
	}

	// Add result types and descriptions.  When there is more than one
	// result type, also add the condition which triggers it.
	if len(resultTexts) > 1 {
		for i, resultText := range resultTexts {
			condKey := fmt.Sprintf("%s--condition%d", method, i)
			help += fmt.Sprintf("\n%s (%s):\n%s\n",
				xT("help-result"), xT(condKey), resultText)
		}
	} else if len(resultTexts) > 0 {
		help += fmt.Sprintf("\n%s:\n%s\n", xT("help-result"),
			resultTexts[0])
	} else {
		help += fmt.Sprintf("\n%s:\n%s\n", xT("help-result"),
			xT("help-result-nothing"))
	}
	return help
}

// isValidResultType returns whether the passed reflect kind is one of the
// acceptable types for results.
func isValidResultType(kind reflect.Kind) bool {
	if isNumeric(kind) {
		return true
	}

	switch kind {
	case reflect.String, reflect.Struct, reflect.Array, reflect.Slice,
		reflect.Bool, reflect.Map:

		return true
	}

	return false
}

// GenerateHelp generates and returns help output for the provided method and
// result types given a map to provide the appropriate keys for the method
// synopsis, field descriptions, conditions, and result descriptions.  The
// method must be associated with a registered type.  All commands provided by
// this package are registered by default.
//
// The resultTypes must be pointer-to-types which represent the specific types
// of values the command returns.  For example, if the command only returns a
// boolean value, there should only be a single entry of (*bool)(nil).  Note
// that each type must be a single pointer to the type.  Therefore, it is
// recommended to simply pass a nil pointer cast to the appropriate type as
// previously shown.
//
// The provided descriptions map must contain all of the keys or an error will
// be returned which includes the missing key, or the final missing key when
// there is more than one key missing.  The generated help in the case of such
// an error will use the key in place of the description.
//
// The following outlines the required keys:
//
//	"<method>--synopsis"             Synopsis for the command
//	"<method>-<lowerfieldname>"      Description for each command argument
//	"<typename>-<lowerfieldname>"    Description for each object field
//	"<method>--condition<#>"         Description for each result condition
//	"<method>--result<#>"            Description for each primitive result num
//
// Notice that the "special" keys synopsis, condition<#>, and result<#> are
// preceded by a double dash to ensure they don't conflict with field names.
//
// The condition keys are only required when there is more than on result type,
// and the result key for a given result type is only required if it's not an
// object.
//
// For example, consider the 'help' command itself.  There are two possible
// returns depending on the provided parameters.  So, the help would be
// generated by calling the function as follows:
//
//	GenerateHelp("help", descs, (*string)(nil), (*string)(nil)).
//
// The following keys would then be required in the provided descriptions map:
//
//	"help--synopsis":   "Returns a list of all commands or help for ...."
//	"help-command":     "The command to retrieve help for",
//	"help--condition0": "no command provided"
//	"help--condition1": "command specified"
//	"help--result0":    "List of commands"
//	"help--result1":    "Help for specified command"
func GenerateHelp(method interface{}, descs map[string]string, resultTypes ...interface{}) (string, error) {
	// Look up details about the provided method and error out if not
	// registered.
	registerLock.RLock()
	rtp, ok := methodToConcreteType[method]
	info := methodToInfo[method]
	registerLock.RUnlock()
	if !ok {
		str := fmt.Sprintf("%#v is not registered", method)
		return "", makeError(ErrUnregisteredMethod, str)
	}

	// Validate each result type is a pointer to a supported type (or nil).
	for i, resultType := range resultTypes {
		if resultType == nil {
			continue
		}

		rtp := reflect.TypeOf(resultType)
		if rtp.Kind() != reflect.Ptr {
			str := fmt.Sprintf("result #%d (%v) is not a pointer",
				i, rtp.Kind())
			return "", makeError(ErrInvalidType, str)
		}

		elemKind := rtp.Elem().Kind()
		if !isValidResultType(elemKind) {
			str := fmt.Sprintf("result #%d (%v) is not an allowed "+
				"type", i, elemKind)
			return "", makeError(ErrInvalidType, str)
		}
	}

	// Create a closure for the description lookup function which falls back
	// to the base help descriptions map for unrecognized keys and tracks
	// and missing keys.
	var missingKey string
	xT := func(key string) string {
		if desc, ok := descs[key]; ok {
			return desc
		}
		if desc, ok := baseHelpDescs[key]; ok {
			return desc
		}

		missingKey = key
		return key
	}

	// Generate and return the help for the method.
	methodStr := reflect.ValueOf(method).String()
	help := methodHelp(xT, rtp, info.defaults, methodStr, resultTypes)
	if missingKey != "" {
		return help, makeError(ErrMissingDescription, missingKey)
	}
	return help, nil
}
