package swag

import (
	"errors"
	"fmt"
	"github.com/go-openapi/spec"
	"go/ast"
	"regexp"
	"strings"
)

const (
	// ARRAY represent a array value.
	ARRAY = "array"
	// OBJECT represent a object value.
	OBJECT = "object"
	// PRIMITIVE represent a primitive value.
	PRIMITIVE = "primitive"
	// BOOLEAN represent a boolean value.
	BOOLEAN = "boolean"
	// INTEGER represent a integer value.
	INTEGER = "integer"
	// NUMBER represent a number value.
	NUMBER = "number"
	// STRING represent a string value.
	STRING = "string"
	// FUNC represent a function value.
	FUNC = "func"
	// ERROR represent a error value.
	ERROR = "error"
	// INTERFACE represent a interface value.
	INTERFACE = "interface{}"
	// ANY represent a any value.
	ANY = "any"
	// NIL represent a empty value.
	NIL = "nil"

	// IgnoreNameOverridePrefix Prepend to model to avoid renaming based on comment.
	IgnoreNameOverridePrefix = '$'
)

// CheckSchemaType checks if typeName is not a name of primitive type.
func CheckSchemaType(typeName string) error {
	if !IsPrimitiveType(typeName) {
		return fmt.Errorf("%s is not basic types", typeName)
	}

	return nil
}

// IsSimplePrimitiveType determine whether the type name is a simple primitive type.
func IsSimplePrimitiveType(typeName string) bool {
	switch typeName {
	case STRING, NUMBER, INTEGER, BOOLEAN:
		return true
	}

	return false
}

// IsPrimitiveType determine whether the type name is a primitive type.
func IsPrimitiveType(typeName string) bool {
	switch typeName {
	case STRING, NUMBER, INTEGER, BOOLEAN, ARRAY, OBJECT, FUNC:
		return true
	}

	return false
}

// IsInterfaceLike determines whether the swagger type name is an go named interface type like error type.
func IsInterfaceLike(typeName string) bool {
	return typeName == ERROR || typeName == ANY
}

// IsNumericType determines whether the swagger type name is a numeric type.
func IsNumericType(typeName string) bool {
	return typeName == INTEGER || typeName == NUMBER
}

// TransToValidPrimitiveSchema transfer golang basic type to swagger schema with format considered.
func TransToValidPrimitiveSchema(typeName string) *spec.Schema {
	switch typeName {
	case "int", "uint":
		return &spec.Schema{SchemaProps: spec.SchemaProps{Type: []string{INTEGER}}}
	case "uint8", "int8", "uint16", "int16", "byte", "int32", "uint32", "rune":
		return &spec.Schema{SchemaProps: spec.SchemaProps{Type: []string{INTEGER}, Format: "int32"}}
	case "uint64", "int64":
		return &spec.Schema{SchemaProps: spec.SchemaProps{Type: []string{INTEGER}, Format: "int64"}}
	case "float32", "float64":
		return &spec.Schema{SchemaProps: spec.SchemaProps{Type: []string{NUMBER}, Format: typeName}}
	case "bool":
		return &spec.Schema{SchemaProps: spec.SchemaProps{Type: []string{BOOLEAN}}}
	case "string":
		return &spec.Schema{SchemaProps: spec.SchemaProps{Type: []string{STRING}}}
	}
	return &spec.Schema{SchemaProps: spec.SchemaProps{Type: []string{typeName}}}
}

// TransToValidSchemeTypeWithFormat indicates type will transfer golang basic type to swagger supported type with format.
func TransToValidSchemeTypeWithFormat(typeName string) (string, string) {
	switch typeName {
	case "int", "uint":
		return INTEGER, ""
	case "uint8", "int8", "uint16", "int16", "byte", "int32", "uint32", "rune":
		return INTEGER, "int32"
	case "uint64", "int64":
		return INTEGER, "int64"
	case "float32", "float64":
		return NUMBER, typeName
	case "bool":
		return BOOLEAN, ""
	case "string":
		return STRING, ""
	}
	return typeName, ""
}

// TransToValidSchemeType indicates type will transfer golang basic type to swagger supported type.
func TransToValidSchemeType(typeName string) string {
	switch typeName {
	case "uint", "int", "uint8", "int8", "uint16", "int16", "byte":
		return INTEGER
	case "uint32", "int32", "rune":
		return INTEGER
	case "uint64", "int64":
		return INTEGER
	case "float32", "float64":
		return NUMBER
	case "bool":
		return BOOLEAN
	case "string":
		return STRING
	}

	return typeName
}

// IsGolangPrimitiveType determine whether the type name is a golang primitive type.
func IsGolangPrimitiveType(typeName string) bool {
	switch typeName {
	case "uint",
		"int",
		"uint8",
		"int8",
		"uint16",
		"int16",
		"byte",
		"uint32",
		"int32",
		"rune",
		"uint64",
		"int64",
		"float32",
		"float64",
		"bool",
		"string":
		return true
	}

	return false
}

// TransToValidCollectionFormat determine valid collection format.
func TransToValidCollectionFormat(format string) string {
	switch format {
	case "csv", "multi", "pipes", "tsv", "ssv":
		return format
	}

	return ""
}

func ignoreNameOverride(name string) bool {
	return len(name) != 0 && name[0] == IgnoreNameOverridePrefix
}

var overrideNameRegex = regexp.MustCompile(`(?i)^@name\s+(\S+)`)

func nameOverride(commentGroup *ast.CommentGroup) string {
	if commentGroup == nil {
		return ""
	}

	// get alias from comment '// @name '
	for _, comment := range commentGroup.List {
		trimmedComment := strings.TrimSpace(strings.TrimLeft(comment.Text, "/"))
		texts := overrideNameRegex.FindStringSubmatch(trimmedComment)
		if len(texts) > 1 {
			return texts[1]
		}
	}

	return ""
}

func commentWithoutNameOverride(commentGroup *ast.CommentGroup) string {
	if commentGroup == nil {
		return ""
	}

	commentBuilder := strings.Builder{}
	for _, comment := range commentGroup.List {
		commentText := comment.Text
		commentText = strings.TrimPrefix(commentText, "//")
		commentText = strings.TrimPrefix(commentText, "/*")
		commentText = strings.TrimSuffix(commentText, "*/")
		commentText = strings.TrimSpace(commentText)
		commentText = overrideNameRegex.ReplaceAllString(commentText, "")
		commentText = strings.TrimSpace(commentText)
		commentBuilder.WriteString(commentText)
	}
	return commentBuilder.String()
}

// IsComplexSchema whether a schema is complex and should be a ref schema
func IsComplexSchema(schema *spec.Schema) bool {
	// a enum type should be complex
	if len(schema.Enum) > 0 {
		return true
	}

	// a deep array type is complex, how to determine deep? here more than 2 ,for example: [][]object,[][][]int
	if len(schema.Type) > 2 {
		return true
	}

	//Object included, such as Object or []Object
	for _, st := range schema.Type {
		if st == OBJECT {
			return true
		}
	}
	return false
}

// IsRefSchema whether a schema is a reference schema.
func IsRefSchema(schema *spec.Schema) bool {
	return schema.Ref.Ref.GetURL() != nil
}

// RefSchema build a reference schema.
func RefSchema(refType string) *spec.Schema {
	return spec.RefSchema("#/definitions/" + refType)
}

// PrimitiveSchema build a primitive schema.
func PrimitiveSchema(refType string) *spec.Schema {
	return &spec.Schema{SchemaProps: spec.SchemaProps{Type: []string{refType}}}
}

// BuildCustomSchema build custom schema specified by tag swaggertype.
func BuildCustomSchema(types []string) (*spec.Schema, error) {
	if len(types) == 0 {
		return nil, nil
	}

	switch types[0] {
	case PRIMITIVE:
		if len(types) == 1 {
			return nil, errors.New("need primitive type after primitive")
		}

		return BuildCustomSchema(types[1:])
	case ARRAY:
		if len(types) == 1 {
			return nil, errors.New("need array item type after array")
		}

		schema, err := BuildCustomSchema(types[1:])
		if err != nil {
			return nil, err
		}

		return spec.ArrayProperty(schema), nil
	case OBJECT:
		if len(types) == 1 {
			return PrimitiveSchema(types[0]), nil
		}

		schema, err := BuildCustomSchema(types[1:])
		if err != nil {
			return nil, err
		}

		return spec.MapProperty(schema), nil
	default:
		err := CheckSchemaType(types[0])
		if err != nil {
			return nil, err
		}

		return PrimitiveSchema(types[0]), nil
	}
}

// MergeSchema merge schemas
func MergeSchema(dst *spec.Schema, src *spec.Schema) *spec.Schema {
	if len(src.Type) > 0 {
		dst.Type = src.Type
	}
	if len(src.Properties) > 0 {
		dst.Properties = src.Properties
	}
	if src.Items != nil {
		dst.Items = src.Items
	}
	if src.AdditionalProperties != nil {
		dst.AdditionalProperties = src.AdditionalProperties
	}
	if len(src.Description) > 0 {
		dst.Description = src.Description
	}
	if src.Nullable {
		dst.Nullable = src.Nullable
	}
	if len(src.Format) > 0 {
		dst.Format = src.Format
	}
	if src.Default != nil {
		dst.Default = src.Default
	}
	if src.Example != nil {
		dst.Example = src.Example
	}
	if len(src.Extensions) > 0 {
		dst.Extensions = src.Extensions
	}
	if src.Maximum != nil {
		dst.Maximum = src.Maximum
	}
	if src.Minimum != nil {
		dst.Minimum = src.Minimum
	}
	if src.ExclusiveMaximum {
		dst.ExclusiveMaximum = src.ExclusiveMaximum
	}
	if src.ExclusiveMinimum {
		dst.ExclusiveMinimum = src.ExclusiveMinimum
	}
	if src.MaxLength != nil {
		dst.MaxLength = src.MaxLength
	}
	if src.MinLength != nil {
		dst.MinLength = src.MinLength
	}
	if len(src.Pattern) > 0 {
		dst.Pattern = src.Pattern
	}
	if src.MaxItems != nil {
		dst.MaxItems = src.MaxItems
	}
	if src.MinItems != nil {
		dst.MinItems = src.MinItems
	}
	if src.UniqueItems {
		dst.UniqueItems = src.UniqueItems
	}
	if src.MultipleOf != nil {
		dst.MultipleOf = src.MultipleOf
	}
	if len(src.Enum) > 0 {
		dst.Enum = src.Enum
	}
	if len(src.Extensions) > 0 {
		dst.Extensions = src.Extensions
	}
	if len(src.ExtraProps) > 0 {
		dst.ExtraProps = src.ExtraProps
	}
	return dst
}
