package codescan

import (
	"encoding/json"
	"errors"
	"fmt"
	"go/ast"
	"go/importer"
	"go/types"
	"log"
	"os"
	"reflect"
	"strconv"
	"strings"

	"golang.org/x/tools/go/ast/astutil"

	"github.com/go-openapi/spec"
)

func addExtension(ve *spec.VendorExtensible, key string, value interface{}) {
	if os.Getenv("SWAGGER_GENERATE_EXTENSION") == "false" {
		return
	}

	ve.AddExtension(key, value)
}

type schemaTypable struct {
	schema *spec.Schema
	level  int
}

func (st schemaTypable) Typed(tpe, format string) {
	st.schema.Typed(tpe, format)
}

func (st schemaTypable) SetRef(ref spec.Ref) {
	st.schema.Ref = ref
}

func (st schemaTypable) Schema() *spec.Schema {
	return st.schema
}

func (st schemaTypable) Items() swaggerTypable {
	if st.schema.Items == nil {
		st.schema.Items = new(spec.SchemaOrArray)
	}
	if st.schema.Items.Schema == nil {
		st.schema.Items.Schema = new(spec.Schema)
	}

	st.schema.Typed("array", "")
	return schemaTypable{st.schema.Items.Schema, st.level + 1}
}

func (st schemaTypable) AdditionalProperties() swaggerTypable {
	if st.schema.AdditionalProperties == nil {
		st.schema.AdditionalProperties = new(spec.SchemaOrBool)
	}
	if st.schema.AdditionalProperties.Schema == nil {
		st.schema.AdditionalProperties.Schema = new(spec.Schema)
	}

	st.schema.Typed("object", "")
	return schemaTypable{st.schema.AdditionalProperties.Schema, st.level + 1}
}

func (st schemaTypable) Level() int { return st.level }

func (st schemaTypable) AddExtension(key string, value interface{}) {
	addExtension(&st.schema.VendorExtensible, key, value)
}

func (st schemaTypable) WithEnum(values ...interface{}) {
	st.schema.WithEnum(values...)
}

func (st schemaTypable) WithEnumDescription(desc string) {
	if desc == "" {
		return
	}
	st.AddExtension(extEnumDesc, desc)
}

type schemaValidations struct {
	current *spec.Schema
}

func (sv schemaValidations) SetMaximum(val float64, exclusive bool) {
	sv.current.Maximum = &val
	sv.current.ExclusiveMaximum = exclusive
}

func (sv schemaValidations) SetMinimum(val float64, exclusive bool) {
	sv.current.Minimum = &val
	sv.current.ExclusiveMinimum = exclusive
}
func (sv schemaValidations) SetMultipleOf(val float64)  { sv.current.MultipleOf = &val }
func (sv schemaValidations) SetMinItems(val int64)      { sv.current.MinItems = &val }
func (sv schemaValidations) SetMaxItems(val int64)      { sv.current.MaxItems = &val }
func (sv schemaValidations) SetMinLength(val int64)     { sv.current.MinLength = &val }
func (sv schemaValidations) SetMaxLength(val int64)     { sv.current.MaxLength = &val }
func (sv schemaValidations) SetPattern(val string)      { sv.current.Pattern = val }
func (sv schemaValidations) SetUnique(val bool)         { sv.current.UniqueItems = val }
func (sv schemaValidations) SetDefault(val interface{}) { sv.current.Default = val }
func (sv schemaValidations) SetExample(val interface{}) { sv.current.Example = val }
func (sv schemaValidations) SetEnum(val string) {
	sv.current.Enum = parseEnum(val, &spec.SimpleSchema{Format: sv.current.Format, Type: sv.current.Type[0]})
}

type schemaBuilder struct {
	ctx        *scanCtx
	decl       *entityDecl
	GoName     string
	Name       string
	annotated  bool
	discovered []*entityDecl
	postDecls  []*entityDecl
}

func (s *schemaBuilder) inferNames() (goName string, name string) {
	if s.GoName != "" {
		goName, name = s.GoName, s.Name
		return
	}

	goName = s.decl.Ident.Name
	name = goName
	defer func() {
		s.GoName = goName
		s.Name = name
	}()
	if s.decl.Comments == nil {
		return
	}

DECLS:
	for _, cmt := range s.decl.Comments.List {
		for _, ln := range strings.Split(cmt.Text, "\n") {
			matches := rxModelOverride.FindStringSubmatch(ln)
			if len(matches) > 0 {
				s.annotated = true
			}
			if len(matches) > 1 && len(matches[1]) > 0 {
				name = matches[1]
				break DECLS
			}
		}
	}
	return
}

func (s *schemaBuilder) Build(definitions map[string]spec.Schema) error {
	s.inferNames()

	schema := definitions[s.Name]
	err := s.buildFromDecl(s.decl, &schema)
	if err != nil {
		return err
	}
	definitions[s.Name] = schema
	return nil
}

func (s *schemaBuilder) buildFromDecl(_ *entityDecl, schema *spec.Schema) error {
	// analyze doc comment for the model
	sp := new(sectionedParser)
	sp.setTitle = func(lines []string) { schema.Title = joinDropLast(lines) }
	sp.setDescription = func(lines []string) {
		schema.Description = joinDropLast(lines)
		enumDesc := getEnumDesc(schema.VendorExtensible.Extensions)
		if enumDesc != "" {
			schema.Description += "\n" + enumDesc
		}
	}
	if err := sp.Parse(s.decl.Comments); err != nil {
		return err
	}

	// if the type is marked to ignore, just return
	if sp.ignored {
		return nil
	}

	switch tpe := s.decl.Type.Obj().Type().(type) {
	case *types.Basic:
		debugLog("basic: %v", tpe.Name())
	case *types.Struct:
		if err := s.buildFromStruct(s.decl, tpe, schema, make(map[string]string)); err != nil {
			return err
		}
	case *types.Interface:
		if err := s.buildFromInterface(s.decl, tpe, schema, make(map[string]string)); err != nil {
			return err
		}
	case *types.Array:
		debugLog("array: %v -> %v", s.decl.Ident.Name, tpe.Elem().String())
	case *types.Slice:
		debugLog("slice: %v -> %v", s.decl.Ident.Name, tpe.Elem().String())
	case *types.Map:
		debugLog("map: %v -> [%v]%v", s.decl.Ident.Name, tpe.Key().String(), tpe.Elem().String())
	case *types.Named:
		o := tpe.Obj()
		if o != nil {
			debugLog("got the named type object: %s.%s | isAlias: %t | exported: %t", o.Pkg().Path(), o.Name(), o.IsAlias(), o.Exported())
			if o.Pkg().Name() == "time" && o.Name() == "Time" {
				schema.Typed("string", "date-time")
				return nil
			}

			ps := schemaTypable{schema, 0}
			for {
				ti := s.decl.Pkg.TypesInfo.Types[s.decl.Spec.Type]
				if ti.IsBuiltin() {
					break
				}
				if ti.IsType() {
					if err := s.buildFromType(ti.Type, ps); err != nil {
						return err
					}
					break
				}
			}
		}
	default:
		log.Printf("WARNING: Missing parser for a %T, skipping model: %s\n", tpe, s.Name)
		return nil
	}

	if schema.Ref.String() == "" {
		if s.Name != s.GoName {
			addExtension(&schema.VendorExtensible, "x-go-name", s.GoName)
		}
		addExtension(&schema.VendorExtensible, "x-go-package", s.decl.Type.Obj().Pkg().Path())
	}
	return nil
}

func (s *schemaBuilder) buildFromTextMarshal(tpe types.Type, tgt swaggerTypable) error {
	if typePtr, ok := tpe.(*types.Pointer); ok {
		return s.buildFromTextMarshal(typePtr.Elem(), tgt)
	}

	typeNamed, ok := tpe.(*types.Named)
	if !ok {
		tgt.Typed("string", "")
		return nil
	}

	tio := typeNamed.Obj()
	if tio.Pkg() == nil && tio.Name() == "error" {
		return swaggerSchemaForType(tio.Name(), tgt)
	}

	debugLog("named refined type %s.%s", tio.Pkg().Path(), tio.Name())
	pkg, found := s.ctx.PkgForType(tpe)

	if strings.ToLower(tio.Name()) == "uuid" {
		tgt.Typed("string", "uuid")
		return nil
	}

	if !found {
		// this must be a builtin
		debugLog("skipping because package is nil: %s", tpe.String())
		return nil
	}
	if pkg.Name == "time" && tio.Name() == "Time" {
		tgt.Typed("string", "date-time")
		return nil
	}
	if pkg.PkgPath == "encoding/json" && tio.Name() == "RawMessage" {
		tgt.Typed("object", "")
		return nil
	}
	cmt, hasComments := s.ctx.FindComments(pkg, tio.Name())
	if !hasComments {
		cmt = new(ast.CommentGroup)
	}

	if sfnm, isf := strfmtName(cmt); isf {
		tgt.Typed("string", sfnm)
		return nil
	}

	tgt.Typed("string", "")
	return nil
}

func (s *schemaBuilder) buildFromType(tpe types.Type, tgt swaggerTypable) error {
	pkg, err := importer.Default().Import("encoding")
	if err != nil {
		return nil
	}
	ifc := pkg.Scope().Lookup("TextMarshaler").Type().Underlying().(*types.Interface)

	// check if the type implements encoding.TextMarshaler interface
	isTextMarshaler := types.Implements(tpe, ifc)
	if isTextMarshaler {
		return s.buildFromTextMarshal(tpe, tgt)
	}

	switch titpe := tpe.(type) {
	case *types.Basic:
		return swaggerSchemaForType(titpe.String(), tgt)
	case *types.Pointer:
		return s.buildFromType(titpe.Elem(), tgt)
	case *types.Struct:
		return s.buildFromStruct(s.decl, titpe, tgt.Schema(), make(map[string]string))
	case *types.Interface:
		return s.buildFromInterface(s.decl, titpe, tgt.Schema(), make(map[string]string))
	case *types.Slice:
		return s.buildFromType(titpe.Elem(), tgt.Items())
	case *types.Array:
		return s.buildFromType(titpe.Elem(), tgt.Items())
	case *types.Map:
		// debugLog("map: %v -> [%v]%v", fld.Name(), ftpe.Key().String(), ftpe.Elem().String())
		// check if key is a string type, if not print a message
		// and skip the map property. Only maps with string keys can go into additional properties
		sch := tgt.Schema()
		if sch == nil {
			return errors.New("items doesn't support maps")
		}
		eleProp := schemaTypable{sch, tgt.Level()}
		key := titpe.Key()
		isTextMarshaler := types.Implements(key, ifc)
		if key.Underlying().String() == "string" || isTextMarshaler {
			return s.buildFromType(titpe.Elem(), eleProp.AdditionalProperties())
		}
	case *types.Named:
		tio := titpe.Obj()
		if tio.Pkg() == nil && tio.Name() == "error" {
			return swaggerSchemaForType(tio.Name(), tgt)
		}
		debugLog("named refined type %s.%s", tio.Pkg().Path(), tio.Name())
		pkg, found := s.ctx.PkgForType(tpe)
		if !found {
			// this must be a builtin
			debugLog("skipping because package is nil: %s", tpe.String())
			return nil
		}
		if pkg.Name == "time" && tio.Name() == "Time" {
			tgt.Typed("string", "date-time")
			return nil
		}
		if pkg.PkgPath == "encoding/json" && tio.Name() == "RawMessage" {
			tgt.Typed("object", "")
			return nil
		}
		cmt, hasComments := s.ctx.FindComments(pkg, tio.Name())
		if !hasComments {
			cmt = new(ast.CommentGroup)
		}

		if typeName, ok := typeName(cmt); ok {
			_ = swaggerSchemaForType(typeName, tgt)
			return nil
		}

		if s.decl.Spec.Assign.IsValid() {
			return s.buildFromType(titpe.Underlying(), tgt)
		}

		if titpe.TypeArgs() != nil && titpe.TypeArgs().Len() > 0 {
			return s.buildFromType(titpe.Underlying(), tgt)
		}

		switch utitpe := tpe.Underlying().(type) {
		case *types.Struct:
			if decl, ok := s.ctx.FindModel(tio.Pkg().Path(), tio.Name()); ok {
				if decl.Type.Obj().Pkg().Path() == "time" && decl.Type.Obj().Name() == "Time" {
					tgt.Typed("string", "date-time")
					return nil
				}
				if sfnm, isf := strfmtName(cmt); isf {
					tgt.Typed("string", sfnm)
					return nil
				}
				if typeName, ok := typeName(cmt); ok {
					_ = swaggerSchemaForType(typeName, tgt)
					return nil
				}

				return s.makeRef(decl, tgt)
			}
		case *types.Interface:
			if decl, ok := s.ctx.FindModel(tio.Pkg().Path(), tio.Name()); ok {
				return s.makeRef(decl, tgt)
			}
		case *types.Basic:
			if sfnm, isf := strfmtName(cmt); isf {
				tgt.Typed("string", sfnm)
				return nil
			}

			if enumName, ok := enumName(cmt); ok {
				enumValues, enumDesces, _ := s.ctx.FindEnumValues(pkg, enumName)
				if len(enumValues) > 0 {
					tgt.WithEnum(enumValues...)
					enumTypeName := reflect.TypeOf(enumValues[0]).String()
					_ = swaggerSchemaForType(enumTypeName, tgt)
				}
				if len(enumDesces) > 0 {
					tgt.WithEnumDescription(strings.Join(enumDesces, "\n"))
				}
				return nil
			}

			if defaultName, ok := defaultName(cmt); ok {
				debugLog(defaultName) //nolint:govet
				return nil
			}

			if typeName, ok := typeName(cmt); ok {
				_ = swaggerSchemaForType(typeName, tgt)
				return nil

			}

			if isAliasParam(tgt) || aliasParam(cmt) {
				err := swaggerSchemaForType(utitpe.Name(), tgt)
				if err == nil {
					return nil
				}
			}
			if decl, ok := s.ctx.FindModel(tio.Pkg().Path(), tio.Name()); ok {
				return s.makeRef(decl, tgt)
			}
			return swaggerSchemaForType(utitpe.String(), tgt)
		case *types.Array:
			if sfnm, isf := strfmtName(cmt); isf {
				if sfnm == "byte" {
					tgt.Typed("string", sfnm)
					return nil
				}
				if sfnm == "bsonobjectid" {
					tgt.Typed("string", sfnm)
					return nil
				}

				tgt.Items().Typed("string", sfnm)
				return nil
			}
			if decl, ok := s.ctx.FindModel(tio.Pkg().Path(), tio.Name()); ok {
				return s.makeRef(decl, tgt)
			}
			return s.buildFromType(utitpe.Elem(), tgt.Items())
		case *types.Slice:
			if sfnm, isf := strfmtName(cmt); isf {
				if sfnm == "byte" {
					tgt.Typed("string", sfnm)
					return nil
				}
				tgt.Items().Typed("string", sfnm)
				return nil
			}
			if decl, ok := s.ctx.FindModel(tio.Pkg().Path(), tio.Name()); ok {
				return s.makeRef(decl, tgt)
			}
			return s.buildFromType(utitpe.Elem(), tgt.Items())
		case *types.Map:
			if decl, ok := s.ctx.FindModel(tio.Pkg().Path(), tio.Name()); ok {
				return s.makeRef(decl, tgt)
			}
			return nil

		default:
			log.Printf("WARNING: can't figure out object type for named type (%T): %v [alias: %t]", tpe.Underlying(), tpe.Underlying(), titpe.Obj().IsAlias())

			return nil
		}
	default:
		panic(fmt.Sprintf("WARNING: can't determine refined type %s (%T)", titpe.String(), titpe))
	}

	return nil
}

func (s *schemaBuilder) buildFromInterface(decl *entityDecl, it *types.Interface, schema *spec.Schema, seen map[string]string) error {
	if it.Empty() {
		return nil
	}

	var (
		tgt      *spec.Schema
		hasAllOf bool
	)

	var flist []*ast.Field
	if specType, ok := decl.Spec.Type.(*ast.InterfaceType); ok {
		flist = make([]*ast.Field, it.NumEmbeddeds()+it.NumExplicitMethods())
		copy(flist, specType.Methods.List)
		// for i := range specType.Methods.List {
		// 	flist[i] = specType.Methods.List[i]
		// }
	}

	// First collect the embedded interfaces
	// create refs when the embedded interface is decorated with an allOf annotation
	for i := 0; i < it.NumEmbeddeds(); i++ {
		fld := it.EmbeddedType(i)

		switch ftpe := fld.(type) {
		case *types.Named:
			o := ftpe.Obj()
			var afld *ast.Field
			for _, an := range flist {
				if len(an.Names) != 0 {
					continue
				}

				tpp := decl.Pkg.TypesInfo.Types[an.Type]
				if tpp.Type.String() != o.Type().String() {
					continue
				}

				// decl.
				debugLog("maybe interface field %s: %s(%T)", o.Name(), o.Type().String(), o.Type())
				afld = an
				break
			}

			if afld == nil {
				debugLog("can't find source associated with %s for %s", fld.String(), it.String())
				continue
			}

			// if the field is annotated with swagger:ignore, ignore it
			if ignored(afld.Doc) {
				continue
			}

			if !allOfMember(afld.Doc) {
				var newSch spec.Schema
				if err := s.buildEmbedded(o.Type(), &newSch, seen); err != nil {
					return err
				}
				schema.AllOf = append(schema.AllOf, newSch)
				hasAllOf = true
				continue
			}

			hasAllOf = true
			if tgt == nil {
				tgt = &spec.Schema{}
			}
			var newSch spec.Schema
			// when the embedded struct is annotated with swagger:allOf it will be used as allOf property
			// otherwise the fields will just be included as normal properties
			if err := s.buildAllOf(o.Type(), &newSch); err != nil {
				return err
			}
			if afld.Doc != nil {
				for _, cmt := range afld.Doc.List {
					for _, ln := range strings.Split(cmt.Text, "\n") {
						matches := rxAllOf.FindStringSubmatch(ln)
						ml := len(matches)
						if ml > 1 {
							mv := matches[ml-1]
							if mv != "" {
								schema.AddExtension("x-class", mv)
							}
						}
					}
				}
			}

			schema.AllOf = append(schema.AllOf, newSch)
		default:
			log.Printf("WARNING: can't figure out object type for allOf named type (%T): %v", ftpe, ftpe.Underlying())
		}
		debugLog("got embedded interface: %s {%T}", fld.String(), fld)
	}

	if tgt == nil {
		tgt = schema
	}
	// We can finally build the actual schema for the struct
	if tgt.Properties == nil {
		tgt.Properties = make(map[string]spec.Schema)
	}
	tgt.Typed("object", "")

	for i := 0; i < it.NumExplicitMethods(); i++ {
		fld := it.ExplicitMethod(i)
		if !fld.Exported() {
			continue
		}
		sig, isSignature := fld.Type().(*types.Signature)
		if !isSignature {
			continue
		}
		if sig.Params().Len() > 0 {
			continue
		}
		if sig.Results() == nil || sig.Results().Len() != 1 {
			continue
		}

		var afld *ast.Field
		ans, _ := astutil.PathEnclosingInterval(decl.File, fld.Pos(), fld.Pos())
		// debugLog("got %d nodes (exact: %t)", len(ans), isExact)
		for _, an := range ans {
			at, valid := an.(*ast.Field)
			if !valid {
				continue
			}

			debugLog("maybe interface field %s: %s(%T)", fld.Name(), fld.Type().String(), fld.Type())
			afld = at
			break
		}

		if afld == nil {
			debugLog("can't find source associated with %s for %s", fld.String(), it.String())
			continue
		}

		// if the field is annotated with swagger:ignore, ignore it
		if ignored(afld.Doc) {
			continue
		}

		name := fld.Name()
		if afld.Doc != nil {
			for _, cmt := range afld.Doc.List {
				for _, ln := range strings.Split(cmt.Text, "\n") {
					matches := rxName.FindStringSubmatch(ln)
					ml := len(matches)
					if ml > 1 {
						name = matches[ml-1]
					}
				}
			}
		}
		ps := tgt.Properties[name]
		if err := s.buildFromType(sig.Results().At(0).Type(), schemaTypable{&ps, 0}); err != nil {
			return err
		}
		if sfName, isStrfmt := strfmtName(afld.Doc); isStrfmt {
			ps.Typed("string", sfName)
			ps.Ref = spec.Ref{}
			ps.Items = nil
		}

		if err := s.createParser(name, tgt, &ps, afld).Parse(afld.Doc); err != nil {
			return err
		}

		if ps.Ref.String() == "" && name != fld.Name() {
			ps.AddExtension("x-go-name", fld.Name())
		}

		if s.ctx.app.setXNullableForPointers {
			if _, isPointer := fld.Type().(*types.Signature).Results().At(0).Type().(*types.Pointer); isPointer && (ps.Extensions == nil || (ps.Extensions["x-nullable"] == nil && ps.Extensions["x-isnullable"] == nil)) {
				ps.AddExtension("x-nullable", true)
			}
		}

		seen[name] = fld.Name()
		tgt.Properties[name] = ps
	}

	if tgt == nil {
		return nil
	}
	if hasAllOf && len(tgt.Properties) > 0 {
		schema.AllOf = append(schema.AllOf, *tgt)
	}
	for k := range tgt.Properties {
		if _, ok := seen[k]; !ok {
			delete(tgt.Properties, k)
		}
	}
	return nil
}

func (s *schemaBuilder) buildFromStruct(decl *entityDecl, st *types.Struct, schema *spec.Schema, seen map[string]string) error {
	s.ctx.FindComments(decl.Pkg, decl.Type.Obj().Name())
	cmt, hasComments := s.ctx.FindComments(decl.Pkg, decl.Type.Obj().Name())
	if !hasComments {
		cmt = new(ast.CommentGroup)
	}
	if typeName, ok := typeName(cmt); ok {
		_ = swaggerSchemaForType(typeName, schemaTypable{schema: schema})
		return nil
	}
	// First check for all of schemas
	var tgt *spec.Schema
	hasAllOf := false

	for i := 0; i < st.NumFields(); i++ {
		fld := st.Field(i)
		if !fld.Anonymous() {
			debugLog("skipping field %q for allOf scan because not anonymous", fld.Name())
			continue
		}
		tg := st.Tag(i)

		debugLog("maybe allof field(%t) %s: %s (%T) [%q](anon: %t, embedded: %t)", fld.IsField(), fld.Name(), fld.Type().String(), fld.Type(), tg, fld.Anonymous(), fld.Embedded())
		var afld *ast.Field
		ans, _ := astutil.PathEnclosingInterval(decl.File, fld.Pos(), fld.Pos())
		// debugLog("got %d nodes (exact: %t)", len(ans), isExact)
		for _, an := range ans {
			at, valid := an.(*ast.Field)
			if !valid {
				continue
			}

			debugLog("maybe allof field %s: %s(%T) [%q]", fld.Name(), fld.Type().String(), fld.Type(), tg)
			afld = at
			break
		}

		if afld == nil {
			debugLog("can't find source associated with %s for %s", fld.String(), st.String())
			continue
		}

		// if the field is annotated with swagger:ignore, ignore it
		if ignored(afld.Doc) {
			continue
		}

		_, ignore, _, _, err := parseJSONTag(afld)
		if err != nil {
			return err
		}
		if ignore {
			continue
		}

		if !allOfMember(afld.Doc) {
			if tgt == nil {
				tgt = schema
			}
			if err := s.buildEmbedded(fld.Type(), tgt, seen); err != nil {
				return err
			}
			continue
		}
		// if this created an allOf property then we have to rejig the schema var
		// because all the fields collected that aren't from embedded structs should go in
		// their own proper schema
		// first process embedded structs in order of embedding
		hasAllOf = true
		if tgt == nil {
			tgt = &spec.Schema{}
		}
		var newSch spec.Schema
		// when the embedded struct is annotated with swagger:allOf it will be used as allOf property
		// otherwise the fields will just be included as normal properties
		if err := s.buildAllOf(fld.Type(), &newSch); err != nil {
			return err
		}

		if afld.Doc != nil {
			for _, cmt := range afld.Doc.List {
				for _, ln := range strings.Split(cmt.Text, "\n") {
					matches := rxAllOf.FindStringSubmatch(ln)
					ml := len(matches)
					if ml > 1 {
						mv := matches[ml-1]
						if mv != "" {
							schema.AddExtension("x-class", mv)
						}
					}
				}
			}
		}

		schema.AllOf = append(schema.AllOf, newSch)
	}

	if tgt == nil {
		if schema != nil {
			tgt = schema
		} else {
			tgt = &spec.Schema{}
		}
	}
	// We can finally build the actual schema for the struct
	if tgt.Properties == nil {
		tgt.Properties = make(map[string]spec.Schema)
	}
	tgt.Typed("object", "")

	for i := 0; i < st.NumFields(); i++ {
		fld := st.Field(i)
		tg := st.Tag(i)

		if fld.Embedded() {
			continue
		}

		if !fld.Exported() {
			debugLog("skipping field %s because it's not exported", fld.Name())
			continue
		}

		var afld *ast.Field
		ans, _ := astutil.PathEnclosingInterval(decl.File, fld.Pos(), fld.Pos())
		// debugLog("got %d nodes (exact: %t)", len(ans), isExact)
		for _, an := range ans {
			at, valid := an.(*ast.Field)
			if !valid {
				continue
			}

			debugLog("field %s: %s(%T) [%q] ==> %s", fld.Name(), fld.Type().String(), fld.Type(), tg, at.Doc.Text())
			afld = at
			break
		}

		if afld == nil {
			debugLog("can't find source associated with %s", fld.String())
			continue
		}

		// if the field is annotated with swagger:ignore, ignore it
		if ignored(afld.Doc) {
			continue
		}

		name, ignore, isString, omitEmpty, err := parseJSONTag(afld)
		if err != nil {
			return err
		}
		if ignore {
			for seenTagName, seenFieldName := range seen {
				if seenFieldName == fld.Name() {
					delete(tgt.Properties, seenTagName)
					break
				}
			}
			continue
		}

		ps := tgt.Properties[name]
		if err = s.buildFromType(fld.Type(), schemaTypable{&ps, 0}); err != nil {
			return err
		}
		if isString {
			ps.Typed("string", ps.Format)
			ps.Ref = spec.Ref{}
			ps.Items = nil
		}
		if sfName, isStrfmt := strfmtName(afld.Doc); isStrfmt {
			ps.Typed("string", sfName)
			ps.Ref = spec.Ref{}
			ps.Items = nil
		}

		if err = s.createParser(name, tgt, &ps, afld).Parse(afld.Doc); err != nil {
			return err
		}

		if ps.Ref.String() == "" && name != fld.Name() {
			addExtension(&ps.VendorExtensible, "x-go-name", fld.Name())
		}

		if s.ctx.app.setXNullableForPointers {
			if _, isPointer := fld.Type().(*types.Pointer); isPointer && !omitEmpty &&
				(ps.Extensions == nil || (ps.Extensions["x-nullable"] == nil && ps.Extensions["x-isnullable"] == nil)) {
				ps.AddExtension("x-nullable", true)
			}
		}

		// we have 2 cases:
		// 1. field with different name override tag
		// 2. field with different name removes tag
		// so we need to save both tag&name
		seen[name] = fld.Name()
		tgt.Properties[name] = ps
	}

	if tgt == nil {
		return nil
	}
	if hasAllOf && len(tgt.Properties) > 0 {
		schema.AllOf = append(schema.AllOf, *tgt)
	}
	for k := range tgt.Properties {
		if _, ok := seen[k]; !ok {
			delete(tgt.Properties, k)
		}
	}
	return nil
}

func (s *schemaBuilder) buildAllOf(tpe types.Type, schema *spec.Schema) error {
	debugLog("allOf %s", tpe.Underlying())
	switch ftpe := tpe.(type) {
	case *types.Pointer:
		return s.buildAllOf(ftpe.Elem(), schema)
	case *types.Named:
		switch utpe := ftpe.Underlying().(type) {
		case *types.Struct:
			decl, found := s.ctx.FindModel(ftpe.Obj().Pkg().Path(), ftpe.Obj().Name())
			if found {
				if ftpe.Obj().Pkg().Path() == "time" && ftpe.Obj().Name() == "Time" {
					schema.Typed("string", "date-time")
					return nil
				}
				if sfnm, isf := strfmtName(decl.Comments); isf {
					schema.Typed("string", sfnm)
					return nil
				}
				if decl.HasModelAnnotation() {
					return s.makeRef(decl, schemaTypable{schema, 0})
				}
				return s.buildFromStruct(decl, utpe, schema, make(map[string]string))
			}
			return fmt.Errorf("can't find source file for struct: %s", ftpe.String())
		case *types.Interface:
			decl, found := s.ctx.FindModel(ftpe.Obj().Pkg().Path(), ftpe.Obj().Name())
			if found {
				if sfnm, isf := strfmtName(decl.Comments); isf {
					schema.Typed("string", sfnm)
					return nil
				}
				if decl.HasModelAnnotation() {
					return s.makeRef(decl, schemaTypable{schema, 0})
				}
				return s.buildFromInterface(decl, utpe, schema, make(map[string]string))
			}
			return fmt.Errorf("can't find source file for interface: %s", ftpe.String())
		default:
			log.Printf("WARNING: can't figure out object type for allOf named type (%T): %v", ftpe, ftpe.Underlying())
			return fmt.Errorf("unable to locate source file for allOf %s", utpe.String())
		}
	default:
		log.Printf("WARNING: Missing allOf parser for a %T, skipping field", ftpe)
		return fmt.Errorf("unable to resolve allOf member for: %v", ftpe)
	}
}

func (s *schemaBuilder) buildEmbedded(tpe types.Type, schema *spec.Schema, seen map[string]string) error {
	debugLog("embedded %s", tpe.Underlying())
	switch ftpe := tpe.(type) {
	case *types.Pointer:
		return s.buildEmbedded(ftpe.Elem(), schema, seen)
	case *types.Named:
		debugLog("embedded named type: %T", ftpe.Underlying())
		switch utpe := ftpe.Underlying().(type) {
		case *types.Struct:
			decl, found := s.ctx.FindModel(ftpe.Obj().Pkg().Path(), ftpe.Obj().Name())
			if found {
				return s.buildFromStruct(decl, utpe, schema, seen)
			}
			return fmt.Errorf("can't find source file for struct: %s", ftpe.String())
		case *types.Interface:
			decl, found := s.ctx.FindModel(ftpe.Obj().Pkg().Path(), ftpe.Obj().Name())
			if found {
				return s.buildFromInterface(decl, utpe, schema, seen)
			}
			return fmt.Errorf("can't find source file for struct: %s", ftpe.String())
		default:
			log.Printf("WARNING: can't figure out object type for embedded named type (%T): %v", ftpe, ftpe.Underlying())
		}
	default:
		log.Printf("WARNING: Missing embedded parser for a %T, skipping model\n", ftpe)
		return nil
	}
	return nil
}

func (s *schemaBuilder) makeRef(decl *entityDecl, prop swaggerTypable) error {
	nm, _ := decl.Names()
	ref, err := spec.NewRef("#/definitions/" + nm)
	if err != nil {
		return err
	}
	prop.SetRef(ref)
	s.postDecls = append(s.postDecls, decl)
	return nil
}

func (s *schemaBuilder) createParser(nm string, schema, ps *spec.Schema, fld *ast.Field) *sectionedParser {
	sp := new(sectionedParser)

	schemeType, err := ps.Type.MarshalJSON()
	if err != nil {
		return nil
	}

	if ps.Ref.String() == "" {
		sp.setDescription = func(lines []string) {
			ps.Description = joinDropLast(lines)
			enumDesc := getEnumDesc(ps.VendorExtensible.Extensions)
			if enumDesc != "" {
				ps.Description += "\n" + enumDesc
			}
		}
		sp.taggers = []tagParser{
			newSingleLineTagParser("maximum", &setMaximum{schemaValidations{ps}, rxf(rxMaximumFmt, "")}),
			newSingleLineTagParser("minimum", &setMinimum{schemaValidations{ps}, rxf(rxMinimumFmt, "")}),
			newSingleLineTagParser("multipleOf", &setMultipleOf{schemaValidations{ps}, rxf(rxMultipleOfFmt, "")}),
			newSingleLineTagParser("minLength", &setMinLength{schemaValidations{ps}, rxf(rxMinLengthFmt, "")}),
			newSingleLineTagParser("maxLength", &setMaxLength{schemaValidations{ps}, rxf(rxMaxLengthFmt, "")}),
			newSingleLineTagParser("pattern", &setPattern{schemaValidations{ps}, rxf(rxPatternFmt, "")}),
			newSingleLineTagParser("minItems", &setMinItems{schemaValidations{ps}, rxf(rxMinItemsFmt, "")}),
			newSingleLineTagParser("maxItems", &setMaxItems{schemaValidations{ps}, rxf(rxMaxItemsFmt, "")}),
			newSingleLineTagParser("unique", &setUnique{schemaValidations{ps}, rxf(rxUniqueFmt, "")}),
			newSingleLineTagParser("enum", &setEnum{schemaValidations{ps}, rxf(rxEnumFmt, "")}),
			newSingleLineTagParser("default", &setDefault{&spec.SimpleSchema{Type: string(schemeType)}, schemaValidations{ps}, rxf(rxDefaultFmt, "")}),
			newSingleLineTagParser("type", &setDefault{&spec.SimpleSchema{Type: string(schemeType)}, schemaValidations{ps}, rxf(rxDefaultFmt, "")}),
			newSingleLineTagParser("example", &setExample{&spec.SimpleSchema{Type: string(schemeType)}, schemaValidations{ps}, rxf(rxExampleFmt, "")}),
			newSingleLineTagParser("required", &setRequiredSchema{schema, nm}),
			newSingleLineTagParser("readOnly", &setReadOnlySchema{ps}),
			newSingleLineTagParser("discriminator", &setDiscriminator{schema, nm}),
			newMultiLineTagParser("YAMLExtensionsBlock", newYamlParser(rxExtensions, schemaVendorExtensibleSetter(ps)), true),
		}

		itemsTaggers := func(items *spec.Schema, level int) []tagParser {
			schemeType, err := items.Type.MarshalJSON()
			if err != nil {
				return nil
			}
			// the expression is 1-index based not 0-index
			itemsPrefix := fmt.Sprintf(rxItemsPrefixFmt, level+1)
			return []tagParser{
				newSingleLineTagParser(fmt.Sprintf("items%dMaximum", level), &setMaximum{schemaValidations{items}, rxf(rxMaximumFmt, itemsPrefix)}),
				newSingleLineTagParser(fmt.Sprintf("items%dMinimum", level), &setMinimum{schemaValidations{items}, rxf(rxMinimumFmt, itemsPrefix)}),
				newSingleLineTagParser(fmt.Sprintf("items%dMultipleOf", level), &setMultipleOf{schemaValidations{items}, rxf(rxMultipleOfFmt, itemsPrefix)}),
				newSingleLineTagParser(fmt.Sprintf("items%dMinLength", level), &setMinLength{schemaValidations{items}, rxf(rxMinLengthFmt, itemsPrefix)}),
				newSingleLineTagParser(fmt.Sprintf("items%dMaxLength", level), &setMaxLength{schemaValidations{items}, rxf(rxMaxLengthFmt, itemsPrefix)}),
				newSingleLineTagParser(fmt.Sprintf("items%dPattern", level), &setPattern{schemaValidations{items}, rxf(rxPatternFmt, itemsPrefix)}),
				newSingleLineTagParser(fmt.Sprintf("items%dMinItems", level), &setMinItems{schemaValidations{items}, rxf(rxMinItemsFmt, itemsPrefix)}),
				newSingleLineTagParser(fmt.Sprintf("items%dMaxItems", level), &setMaxItems{schemaValidations{items}, rxf(rxMaxItemsFmt, itemsPrefix)}),
				newSingleLineTagParser(fmt.Sprintf("items%dUnique", level), &setUnique{schemaValidations{items}, rxf(rxUniqueFmt, itemsPrefix)}),
				newSingleLineTagParser(fmt.Sprintf("items%dEnum", level), &setEnum{schemaValidations{items}, rxf(rxEnumFmt, itemsPrefix)}),
				newSingleLineTagParser(fmt.Sprintf("items%dDefault", level), &setDefault{&spec.SimpleSchema{Type: string(schemeType)}, schemaValidations{items}, rxf(rxDefaultFmt, itemsPrefix)}),
				newSingleLineTagParser(fmt.Sprintf("items%dExample", level), &setExample{&spec.SimpleSchema{Type: string(schemeType)}, schemaValidations{items}, rxf(rxExampleFmt, itemsPrefix)}),
			}
		}

		var parseArrayTypes func(expr ast.Expr, items *spec.SchemaOrArray, level int) ([]tagParser, error)
		parseArrayTypes = func(expr ast.Expr, items *spec.SchemaOrArray, level int) ([]tagParser, error) {
			if items == nil || items.Schema == nil {
				return []tagParser{}, nil
			}
			switch iftpe := expr.(type) {
			case *ast.ArrayType:
				eleTaggers := itemsTaggers(items.Schema, level)
				sp.taggers = append(eleTaggers, sp.taggers...)
				otherTaggers, err := parseArrayTypes(iftpe.Elt, items.Schema.Items, level+1)
				if err != nil {
					return nil, err
				}
				return otherTaggers, nil
			case *ast.Ident:
				taggers := []tagParser{}
				if iftpe.Obj == nil {
					taggers = itemsTaggers(items.Schema, level)
				}
				otherTaggers, err := parseArrayTypes(expr, items.Schema.Items, level+1)
				if err != nil {
					return nil, err
				}
				return append(taggers, otherTaggers...), nil
			case *ast.StarExpr:
				otherTaggers, err := parseArrayTypes(iftpe.X, items, level)
				if err != nil {
					return nil, err
				}
				return otherTaggers, nil
			default:
				return nil, fmt.Errorf("unknown field type ele for %q", nm)
			}
		}
		// check if this is a primitive, if so parse the validations from the
		// doc comments of the slice declaration.
		if ftped, ok := fld.Type.(*ast.ArrayType); ok {
			taggers, err := parseArrayTypes(ftped.Elt, ps.Items, 0)
			if err != nil {
				return sp
			}
			sp.taggers = append(taggers, sp.taggers...)
		}

	} else {
		sp.taggers = []tagParser{
			newSingleLineTagParser("required", &setRequiredSchema{schema, nm}),
		}
	}
	return sp
}

func schemaVendorExtensibleSetter(meta *spec.Schema) func(json.RawMessage) error {
	return func(jsonValue json.RawMessage) error {
		var jsonData spec.Extensions
		err := json.Unmarshal(jsonValue, &jsonData)
		if err != nil {
			return err
		}
		for k := range jsonData {
			if !rxAllowedExtensions.MatchString(k) {
				return fmt.Errorf("invalid schema extension name, should start from `x-`: %s", k)
			}
		}
		meta.Extensions = jsonData
		return nil
	}
}

type tagOptions []string

func (t tagOptions) Contain(option string) bool {
	for i := 1; i < len(t); i++ {
		if t[i] == option {
			return true
		}
	}
	return false
}

func (t tagOptions) Name() string {
	return t[0]
}

func parseJSONTag(field *ast.Field) (name string, ignore, isString, omitEmpty bool, err error) {
	if len(field.Names) > 0 {
		name = field.Names[0].Name
	}
	if field.Tag == nil || len(strings.TrimSpace(field.Tag.Value)) == 0 {
		return name, false, false, false, nil
	}

	tv, err := strconv.Unquote(field.Tag.Value)
	if err != nil {
		return name, false, false, false, err
	}

	if strings.TrimSpace(tv) != "" {
		st := reflect.StructTag(tv)
		jsonParts := tagOptions(strings.Split(st.Get("json"), ","))

		if jsonParts.Contain("string") {
			// Need to check if the field type is a scalar. Otherwise, the
			// ",string" directive doesn't apply.
			isString = isFieldStringable(field.Type)
		}

		omitEmpty = jsonParts.Contain("omitempty")

		switch jsonParts.Name() {
		case "-":
			return name, true, isString, omitEmpty, nil
		case "":
			return name, false, isString, omitEmpty, nil
		default:
			return jsonParts.Name(), false, isString, omitEmpty, nil
		}
	}
	return name, false, false, false, nil
}

// isFieldStringable check if the field type is a scalar. If the field type is
// *ast.StarExpr and is pointer type, check if it refers to a scalar.
// Otherwise, the ",string" directive doesn't apply.
func isFieldStringable(tpe ast.Expr) bool {
	if ident, ok := tpe.(*ast.Ident); ok {
		switch ident.Name {
		case "int", "int8", "int16", "int32", "int64",
			"uint", "uint8", "uint16", "uint32", "uint64",
			"float64", "string", "bool":
			return true
		}
	} else if starExpr, ok := tpe.(*ast.StarExpr); ok {
		return isFieldStringable(starExpr.X)
	} else {
		return false
	}
	return false
}
