// Copyright (c) 2015 The btcsuite developers
// Copyright (c) 2015-2016 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 (
	"fmt"
	"reflect"
	"strings"
)

// CmdMethod returns the method for the passed command.  The provided command
// type must be a registered type.  All commands provided by this package are
// registered by default.
func CmdMethod(cmd interface{}) (string, error) {
	// Look up the cmd type and error out if not registered.
	rt := reflect.TypeOf(cmd)
	registerLock.RLock()
	method, ok := concreteTypeToMethod[rt]
	registerLock.RUnlock()
	if !ok {
		str := fmt.Sprintf("%T is not registered", cmd)
		return "", makeError(ErrUnregisteredMethod, str)
	}

	return reflect.ValueOf(method).String(), nil
}

// MethodUsageFlags returns the usage flags for the passed command method.  The
// provided method must be associated with a registered type.  All commands
// provided by this package are registered by default.
func MethodUsageFlags(method interface{}) (UsageFlag, error) {
	// Look up details about the provided method and error out if not
	// registered.
	registerLock.RLock()
	info, ok := methodToInfo[method]
	registerLock.RUnlock()
	if !ok {
		str := fmt.Sprintf("%q is not registered", method)
		return 0, makeError(ErrUnregisteredMethod, str)
	}

	return info.flags, nil
}

// subStructUsage returns a string for use in the one-line usage for the given
// sub struct.  Note that this is specifically for fields which consist of
// structs (or an array/slice of structs) as opposed to the top-level command
// struct.
//
// Any fields that include a jsonrpcusage struct tag will use that instead of
// being automatically generated.
func subStructUsage(structType reflect.Type) string {
	numFields := structType.NumField()
	fieldUsages := make([]string, 0, numFields)
	for i := 0; i < structType.NumField(); i++ {
		rtf := structType.Field(i)

		// When the field has a jsonrpcusage struct tag specified use
		// that instead of automatically generating it.
		if tag := rtf.Tag.Get("jsonrpcusage"); tag != "" {
			fieldUsages = append(fieldUsages, tag)
			continue
		}

		// Create the name/value entry for the field while considering
		// the type of the field.  Not all possible types are covered
		// here and when one of the types not specifically covered is
		// encountered, the field name is simply reused for the value.
		fieldName := strings.ToLower(rtf.Name)
		fieldValue := fieldName
		fieldKind := rtf.Type.Kind()
		switch {
		case isNumeric(fieldKind):
			if fieldKind == reflect.Float32 || fieldKind == reflect.Float64 {
				fieldValue = "n.nnn"
			} else {
				fieldValue = "n"
			}
		case fieldKind == reflect.String:
			fieldValue = `"value"`

		case fieldKind == reflect.Struct:
			fieldValue = subStructUsage(rtf.Type)

		case fieldKind == reflect.Array || fieldKind == reflect.Slice:
			fieldValue = subArrayUsage(rtf.Type, fieldName)
		}

		usage := fmt.Sprintf("%q:%s", fieldName, fieldValue)
		fieldUsages = append(fieldUsages, usage)
	}

	return fmt.Sprintf("{%s}", strings.Join(fieldUsages, ","))
}

// subArrayUsage returns a string for use in the one-line usage for the given
// array or slice.  It also contains logic to convert plural field names to
// singular so the generated usage string reads better.
func subArrayUsage(arrayType reflect.Type, fieldName string) string {
	// Convert plural field names to singular.  Only works for English.
	singularFieldName := fieldName
	if strings.HasSuffix(fieldName, "ies") {
		singularFieldName = strings.TrimSuffix(fieldName, "ies")
		singularFieldName = singularFieldName + "y"
	} else if strings.HasSuffix(fieldName, "es") {
		singularFieldName = strings.TrimSuffix(fieldName, "es")
	} else if strings.HasSuffix(fieldName, "s") {
		singularFieldName = strings.TrimSuffix(fieldName, "s")
	}

	elemType := arrayType.Elem()
	switch elemType.Kind() {
	case reflect.String:
		return fmt.Sprintf("[%q,...]", singularFieldName)

	case reflect.Struct:
		return fmt.Sprintf("[%s,...]", subStructUsage(elemType))
	}

	// Fall back to simply showing the field name in array syntax.
	return fmt.Sprintf(`[%s,...]`, singularFieldName)
}

// fieldUsage returns a string for use in the one-line usage for the struct
// field of a command.
//
// Any fields that include a jsonrpcusage struct tag will use that instead of
// being automatically generated.
func fieldUsage(structField reflect.StructField, defaultVal *reflect.Value) string {
	// When the field has a jsonrpcusage struct tag specified use that
	// instead of automatically generating it.
	if tag := structField.Tag.Get("jsonrpcusage"); tag != "" {
		return tag
	}

	// Indirect the pointer if needed.
	fieldType := structField.Type
	if fieldType.Kind() == reflect.Ptr {
		fieldType = fieldType.Elem()
	}

	// 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
	}

	// Handle certain types uniquely to provide nicer usage.
	fieldName := strings.ToLower(structField.Name)
	switch fieldType.Kind() {
	case reflect.String:
		if defaultVal != nil {
			return fmt.Sprintf("%s=%q", fieldName,
				defaultVal.Interface())
		}

		return fmt.Sprintf("%q", fieldName)

	case reflect.Array, reflect.Slice:
		return subArrayUsage(fieldType, fieldName)

	case reflect.Struct:
		return subStructUsage(fieldType)
	}

	// Simply return the field name when none of the above special cases
	// apply.
	if defaultVal != nil {
		return fmt.Sprintf("%s=%v", fieldName, defaultVal.Interface())
	}
	return fieldName
}

// methodUsageText returns a one-line usage string for the provided command and
// method info.  This is the main work horse for the exported MethodUsageText
// function.
func methodUsageText(rtp reflect.Type, defaults map[int]reflect.Value, method interface{}) string {
	// Generate the individual usage for each field in the command.  Several
	// simplifying assumptions are made here because the RegisterCmd
	// function has already rigorously enforced the layout.
	rt := rtp.Elem()
	numFields := rt.NumField()
	reqFieldUsages := make([]string, 0, numFields)
	optFieldUsages := make([]string, 0, numFields)
	for i := 0; i < numFields; i++ {
		rtf := rt.Field(i)
		var isOptional bool
		if kind := rtf.Type.Kind(); kind == reflect.Ptr {
			isOptional = true
		}

		var defaultVal *reflect.Value
		if defVal, ok := defaults[i]; ok {
			defaultVal = &defVal
		}

		// Add human-readable usage to the appropriate slice that is
		// later used to generate the one-line usage.
		usage := fieldUsage(rtf, defaultVal)
		if isOptional {
			optFieldUsages = append(optFieldUsages, usage)
		} else {
			reqFieldUsages = append(reqFieldUsages, usage)
		}
	}

	// Generate and return the one-line usage string.
	usageStr := reflect.ValueOf(method).String()
	if len(reqFieldUsages) > 0 {
		usageStr += " " + strings.Join(reqFieldUsages, " ")
	}
	if len(optFieldUsages) > 0 {
		usageStr += fmt.Sprintf(" (%s)", strings.Join(optFieldUsages, " "))
	}
	return usageStr
}

// MethodUsageText returns a one-line usage string for the provided method.  The
// provided method must be associated with a registered type.  All commands
// provided by this package are registered by default.
func MethodUsageText(method 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("%q is not registered", method)
		return "", makeError(ErrUnregisteredMethod, str)
	}

	// When the usage for this method has already been generated, simply
	// return it.
	if info.usage != "" {
		return info.usage, nil
	}

	// Generate and store the usage string for future calls and return it.
	usage := methodUsageText(rtp, info.defaults, method)
	registerLock.Lock()
	info.usage = usage
	methodToInfo[method] = info
	registerLock.Unlock()
	return usage, nil
}
