package gen

import (
	"fmt"
	"log"
	"regexp"
	"strings"

	"github.com/dave/jennifer/jen"
	"github.com/fatih/structtag"

	"github.com/thanhpd56/dbml-go/core"
	"github.com/thanhpd56/dbml-go/internal/gen-go-model/genutil"
)

const (
	GoTypeAnnotation       = "gotype"
	GoPointerAnnotation    = "gopointer"
	GoTagAnnotation        = "gotag"
	GoTagFromRefAnnotation = "gotagref"
	GoTagToRefAnnotation   = "gotagreversedref"
)

type generator struct {
	dbml             *core.DBML
	out              string
	gopackage        string
	fieldtags        []string
	types            map[string]jen.Code
	shouldGenTblName bool
	excludeTables    []string
}

func newgen() *generator {
	return &generator{
		types: make(map[string]jen.Code),
	}
}

func (g *generator) reset(rememberAlias bool) {
	g.dbml = nil
	if !rememberAlias {
		g.types = make(map[string]jen.Code)
	}
}

func (g *generator) file() *jen.File {
	return jen.NewFilePathName(g.out, g.gopackage)
}

func (g *generator) generate() error {
	if err := g.genEnums(); err != nil {
		return err
	}
	return nil
}

func (g *generator) genEnums() error {
	for _, enum := range g.dbml.Enums {
		if err := g.genEnum(enum); err != nil {
			return err
		}
	}

	toColumnNameToRelationships, fromColumnNameToRelationships := g.getFullRelationShips()

	for _, table := range g.dbml.Tables {
		if err := g.genTable(table, toColumnNameToRelationships, fromColumnNameToRelationships); err != nil {
			return err
		}
	}

	return nil
}

func (g *generator) genEnum(enum core.Enum) error {
	f := jen.NewFilePathName(g.out, g.gopackage)

	enumOriginName := genutil.NormalizeTypeName(enum.Name)
	enumGoTypeName := genutil.NormalizeGoTypeName(enum.Name)

	f.PackageComment("Code generated by dbml-gen-go-model. DO NOT EDIT.")
	f.PackageComment("Supported by thanhpd56@2020")
	f.Commentf("%s is generated type for enum '%s'", enumGoTypeName, enumOriginName)
	f.Type().Id(enumGoTypeName).String()

	f.Const().DefsFunc(func(group *jen.Group) {
		for _, value := range enum.Values {
			enumName := fmt.Sprintf("%s_%s", enumOriginName, strings.ToLower(value.Name))
			enumName = genutil.NormalizeGoTypeName(enumName)
			v := group.Id(enumName).Op("=").Id(enumGoTypeName).Parens(jen.Lit(value.Name))
			if value.Note != "" {
				v.Comment(value.Note)
			}
		}
	})

	g.types[enum.Name] = jen.Id(enumGoTypeName)

	return f.Save(fmt.Sprintf("%s/%s.enum.go", g.out, genutil.Normalize(enum.Name)))
}

type fieldDefinition struct {
	name      string
	fieldType jen.Code
	nullable  bool
}

func containsString(arr []string, value string) bool {
	for _, str := range arr {
		if strings.ToLower(str) == strings.ToLower(value) {
			return true
		}
	}
	return false
}

func (g *generator) genTable(table core.Table, toColumnNameToRelationships map[string][]core.Relationship, fromColumnNameToRelationships map[string][]core.Relationship) error {
	if containsString(g.excludeTables, table.Name) {
		log.Printf("ignore table %s", table.Name)
		return nil
	}
	f := jen.NewFilePathName(g.out, g.gopackage)

	tableOriginName := genutil.Normalize(table.Name)
	tableGoTypeName := genutil.NormalizeGoTypeName(table.Name)

	f.PackageComment("Code generated by dbml-gen-go-model. DO NOT EDIT.")
	f.PackageComment("Supported by thanhpd56@2020")
	f.Commentf("%s is generated type for table '%s'", tableGoTypeName, tableOriginName)

	var genColumnErr error

	cols := make([]string, 0)
	fields := []fieldDefinition{}

	f.Type().Id(tableGoTypeName).StructFunc(func(group *jen.Group) {

		for _, column := range table.Columns {
			columnName := genutil.NormalLizeGoName(column.Name)
			columnOriginName := genutil.Normalize(column.Name)
			t, ok := g.getJenType(column)
			if !ok {
				genColumnErr = fmt.Errorf("type '%s' is not support", column.Type)
			}

			customTag, found := column.Annotations[GoTagAnnotation]
			tags := structtag.Tags{}
			if found {
				t, err := structtag.Parse(customTag)
				if err != nil {
					fmt.Errorf("error why parse tag %s", customTag)
				}
				tags = *t
			}

			if column.Settings.Note != "" {
				group.Comment(column.Settings.Note)
			}

			for _, t := range g.fieldtags {
				_ = tags.Set(&structtag.Tag{
					Key:     strings.TrimSpace(t),
					Name:    columnOriginName,
					Options: nil,
				})
			}

			tagValues := tagToMap(tags)

			if column.Settings.Null && column.Settings.Default == "" {
				fields = append(fields, fieldDefinition{
					name:      columnName,
					fieldType: t,
					nullable:  true,
				})
				group.Id(columnName).Add(jen.Op("*")).Add(t).Tag(tagValues)
			} else {
				fields = append(fields, fieldDefinition{
					name:      columnName,
					fieldType: t,
					nullable:  false,
				})
				group.Id(columnName).Add(t).Tag(tagValues)
			}
			cols = append(cols, columnOriginName)

			fullColumnID := fmt.Sprintf("%s.%s", table.Name, column.Name)

			toRelationships := toColumnNameToRelationships[fullColumnID]

			keyGorm := "gorm"
			for _, relationship := range toRelationships {

				relationName := strings.Split(column.Name, "_id")[0]

				fromTypeName := strings.Split(relationship.From, ".")[0]

				fromGoTypeName := genutil.NormalizeGoTypeName(fromTypeName)

				toTypeName := strings.Split(relationship.To, ".")[0]

				toGoTypeName := genutil.NormalizeGoTypeName(toTypeName)

				fieldName := genutil.NormalizeGoTypeName(relationName)
				goTags := appendTagKey(relationship.Tags, keyGorm, fmt.Sprintf("foreignkey:%s", column.Name))
				tags := tagToMap(goTags)

				if relationship.Type == core.OneToMany {
					group.Id(fieldName).Id(fromGoTypeName).Tag(tags)
				} else if relationship.Type == core.OneToOne {
					group.Id(fieldName).Op("*").Id(fromGoTypeName).Tag(tags)
				} else if relationship.Type == core.ManyToOne {
					group.Id(fieldName).Op("*").Id(toGoTypeName).Tag(tags)
				}
			}

			fromRelationships := fromColumnNameToRelationships[fullColumnID]

			for _, relationship := range fromRelationships {

				toTypeName := strings.Split(relationship.To, ".")[0]
				toColumnName := strings.Split(relationship.To, ".")[1]

				toGoTypeName := genutil.NormalizeGoTypeName(toTypeName)

				fromTypeName := strings.Split(relationship.From, ".")[0]

				goTags := appendTagKey(relationship.Tags, keyGorm, fmt.Sprintf("foreignkey:%s", toColumnName))
				tags := tagToMap(goTags)

				fromGoTypeName := genutil.NormalizeGoTypeName(fromTypeName)
				if relationship.Type == core.OneToMany {

					name := genutil.GoInitialismCamelCase(toTypeName)

					group.Id(name).Index().Id(toGoTypeName).Tag(tags)

				} else if relationship.Type == core.OneToOne {

					name := genutil.NormalizeGoTypeName(toTypeName)

					group.Id(name).Op("*").Id(toGoTypeName).Tag(tags)
				} else if relationship.Type == core.ManyToOne {

					name := genutil.GoInitialismCamelCase(fromTypeName)

					group.Id(name).Index().Id(fromGoTypeName).Tag(tags)
				}
			}
		}
	})

	if genColumnErr != nil {
		return genColumnErr
	}

	tableMetadataType := "__tbl_" + tableOriginName
	tableMetadataColumnsType := tableMetadataType + "_columns"

	f.Commentf("// table '%s' columns list struct", tableOriginName)
	f.Type().Id(tableMetadataColumnsType).StructFunc(func(group *jen.Group) {
		for _, column := range table.Columns {
			name := genutil.NormalLizeGoName(column.Name)
			group.Id(name).String()
		}
	})

	f.Commentf("// table '%s' metadata struct", tableOriginName)
	f.Type().Id("__tbl_"+tableOriginName).Struct(
		jen.Id("Name").String(),
		jen.Id("Columns").Id(tableMetadataColumnsType),
	)

	tableMetadataVar := "_tbl_" + tableOriginName

	f.Commentf("// table '%s' metadata info", tableOriginName)
	f.Var().Id(tableMetadataVar).Op("=").Id(tableMetadataType).Values(jen.DictFunc(func(d jen.Dict) {
		d[jen.Id("Name")] = jen.Lit(tableOriginName)
		d[jen.Id("Columns")] = jen.Id(tableMetadataColumnsType).Values(jen.DictFunc(func(d jen.Dict) {
			for _, column := range table.Columns {
				columnName := genutil.NormalLizeGoName(column.Name)
				columnOriginName := genutil.Normalize(column.Name)
				d[jen.Id(columnName)] = jen.Lit(columnOriginName)
			}
		}))
	}))

	f.Commentf("GetColumns return list columns name for table '%s'", tableOriginName)
	f.Func().Params(
		jen.Op("*").Id(tableMetadataType),
	).Id("GetColumns").Params().Index().String().Block(
		jen.Return(jen.Index().String().ValuesFunc(func(g *jen.Group) {
			for _, col := range cols {
				g.Lit(col)
			}
		})),
	)

	f.Commentf("T return metadata info for table '%s'", tableOriginName)
	f.Func().Params(
		jen.Op("*").Id(tableGoTypeName),
	).Id("T").Params().Op("*").Id(tableMetadataType).Block(
		jen.Return().Op("&").Id(tableMetadataVar),
	)

	for _, field := range fields {
		getterName := fmt.Sprintf("Get%s", field.name)
		f.Commentf(getterName)
		if field.nullable {
			f.Func().Params(
				jen.Id("e").Op("*").Id(tableGoTypeName),
			).Id(getterName).Params().Op("*").Add(field.fieldType).Block(
				jen.Return().Id("e").Dot(field.name),
			)
		} else {
			f.Func().Params(
				jen.Id("e").Op("*").Id(tableGoTypeName),
			).Id(getterName).Params().Add(field.fieldType).Block(
				jen.Return().Id("e").Dot(field.name),
			)
		}

	}

	if g.shouldGenTblName {
		f.Commentf("TableName return table name")
		f.Func().Params(
			jen.Id(tableGoTypeName),
		).Id("TableName").Params().Id("string").Block(
			jen.Return(jen.Lit(tableOriginName)),
		)
	}

	return f.Save(fmt.Sprintf("%s/%s.table.go", g.out, genutil.Normalize(table.Name)))
}

func appendTagKey(goTags structtag.Tags, key string, value string) structtag.Tags {
	_, err := goTags.Get(key)
	if err != nil { 
		_ = goTags.Set(&structtag.Tag{
			Key:  key,
			Name: value,
		})
	} else {
		goTags.AddOptions(key, value)
	}
	return goTags
}

func tagToMap(tags structtag.Tags) map[string]string {
	tagValues := map[string]string{}
	for _, tag := range tags.Tags() {
		tagValues[tag.Key] = tag.Value()
	}
	return tagValues
}

const primeTypePattern = `^(\w+)(\(d+\))?`

var (
	regexType    = regexp.MustCompile(primeTypePattern)
	builtinTypes = map[string]jen.Code{
		"int":       jen.Int(),
		"int8":      jen.Int8(),
		"int16":     jen.Int16(),
		"int32":     jen.Int32(),
		"int64":     jen.Int64(),
		"bigint":    jen.Int64(),
		"uint":      jen.Uint(),
		"uint8":     jen.Uint8(),
		"uint16":    jen.Uint16(),
		"uint32":    jen.Uint32(),
		"uint64":    jen.Uint64(),
		"float":     jen.Float64(),
		"float32":   jen.Float32(),
		"float64":   jen.Float64(),
		"bool":      jen.Bool(),
		"text":      jen.String(),
		"varchar":   jen.String(),
		"char":      jen.String(),
		"byte":      jen.Byte(),
		"rune":      jen.Rune(),
		"timestamp": jen.Int(),
		"json":      jen.Qual("gorm.io/datatypes", "JSON"),
		"datetime":  jen.Qual("time", "Time"),
	}
)

func (g *generator) getJenType(col core.Column) (jen.Code, bool) {

	t, ok := getJenTypeFromAnnotation(col.Annotations[GoTypeAnnotation])
	if ok {
		return t, ok
	}

	m := regexType.FindStringSubmatch(col.Type)
	if len(m) >= 2 {

		if t, ok := builtinTypes[m[1]]; ok {
			return t, ok
		}
	}
	t, ok = g.types[col.Type]
	return t, ok
}

func getJenTypeFromAnnotation(s string) (jen.Code, bool) {
	if len(s) == 0 {
		return nil, false
	}

	parts := strings.Split(s, ":")
	if len(parts) >= 2 {
		return jen.Qual(parts[0], parts[1]), true
	}
	return jen.Id(parts[0]), true
}

func (g *generator) getFullRelationShips() (toColumnNameToRelationships map[string][]core.Relationship, fromColumnNameToRelationships map[string][]core.Relationship) {
	toColumnNameToRelationships = map[string][]core.Relationship{}
	fromColumnNameToRelationships = map[string][]core.Relationship{}
	for _, ref := range g.dbml.Refs {
		for _, relationship := range ref.Relationships {
			reverseRelationship := core.Relationship{
				From: relationship.To,
				To:   relationship.From,
				Type: reverseRefType(relationship.Type),
				Tags: relationship.Tags,
			}
			fromColumnNameToRelationships[relationship.To] = append(toColumnNameToRelationships[relationship.To], reverseRelationship)

			toColumnNameToRelationships[relationship.From] = append(fromColumnNameToRelationships[relationship.From], relationship)
		}
	}
	for _, table := range g.dbml.Tables {

		for _, column := range table.Columns {

			fullColumnID := getFullColumnName(table.Name, column.Name)
			toRelationships := toColumnNameToRelationships[fullColumnID]
			fromRelationships := fromColumnNameToRelationships[fullColumnID]
			fromRefTagStr, found := column.Annotations[GoTagFromRefAnnotation]
			fromTags := structtag.Tags{}
			if found {
				parse, err := structtag.Parse(fromRefTagStr)
				if err != nil {
					log.Fatalf("error when parse tag %s", fromRefTagStr)
				}
				fromTags = *parse
			}
			toRefTagStr, found := column.Annotations[GoTagToRefAnnotation]
			toTags := structtag.Tags{}
			if found {
				parse, err := structtag.Parse(toRefTagStr)
				if err != nil {
					log.Fatalf("error when parse tag %s", toRefTagStr)
				}
				toTags = *parse
			}

			if column.Settings.Ref.To != "" {
				refType := column.Settings.Ref.Type
				toRelationships = append(toRelationships, core.Relationship{
					From: fullColumnID,
					To:   column.Settings.Ref.To,
					Type: refType,
					Tags: fromTags,
				})

				reverseRefType := reverseRefType(refType)
				fromRelationships = append(fromRelationships, core.Relationship{
					From: column.Settings.Ref.To,
					To:   fullColumnID,
					Type: reverseRefType,
					Tags: toTags,
				})

				toColumnNameToRelationships[fullColumnID] = toRelationships
				fromColumnNameToRelationships[column.Settings.Ref.To] = fromRelationships
			}
		}
	}

	return
}

func getFullColumnName(tableName string, columnName string) string {
	return fmt.Sprintf("%s.%s", tableName, columnName)
}

func reverseRefType(refType core.RelationshipType) core.RelationshipType {
	var reverseRefType core.RelationshipType = core.OneToOne
	if refType == core.OneToMany {
		reverseRefType = core.ManyToOne
	}
	if refType == core.ManyToOne {
		reverseRefType = core.OneToMany
	}
	return reverseRefType
}
package gen

import (
	"fmt"
	"log"
	"regexp"
	"strings"

	"github.com/dave/jennifer/jen"
	"github.com/fatih/structtag"

	"github.com/thanhpd56/dbml-go/core"
	"github.com/thanhpd56/dbml-go/internal/gen-go-model/genutil"
)

const (
	GoTypeAnnotation       = "gotype"
	GoPointerAnnotation    = "gopointer"
	GoTagAnnotation        = "gotag"
	GoTagFromRefAnnotation = "gotagref"
	GoTagToRefAnnotation   = "gotagreversedref"
)

type generator struct {
	dbml             *core.DBML
	out              string
	gopackage        string
	fieldtags        []string
	types            map[string]jen.Code
	shouldGenTblName bool
	excludeTables    []string
}

func newgen() *generator {
	return &generator{
		types: make(map[string]jen.Code),
	}
}

func (g *generator) reset(rememberAlias bool) {
	g.dbml = nil
	if !rememberAlias {
		g.types = make(map[string]jen.Code)
	}
}

func (g *generator) file() *jen.File {
	return jen.NewFilePathName(g.out, g.gopackage)
}

func (g *generator) generate() error {
	if err := g.genEnums(); err != nil {
		return err
	}
	return nil
}

func (g *generator) genEnums() error {
	for _, enum := range g.dbml.Enums {
		if err := g.genEnum(enum); err != nil {
			return err
		}
	}

	toColumnNameToRelationships, fromColumnNameToRelationships := g.getFullRelationShips()

	for _, table := range g.dbml.Tables {
		if err := g.genTable(table, toColumnNameToRelationships, fromColumnNameToRelationships); err != nil {
			return err
		}
	}

	return nil
}

func (g *generator) genEnum(enum core.Enum) error {
	f := jen.NewFilePathName(g.out, g.gopackage)

	enumOriginName := genutil.NormalizeTypeName(enum.Name)
	enumGoTypeName := genutil.NormalizeGoTypeName(enum.Name)

	f.PackageComment("Code generated by dbml-gen-go-model. DO NOT EDIT.")
	f.PackageComment("Supported by thanhpd56@2020")
	f.Commentf("%s is generated type for enum '%s'", enumGoTypeName, enumOriginName)
	f.Type().Id(enumGoTypeName).String()

	f.Const().DefsFunc(func(group *jen.Group) {
		for _, value := range enum.Values {
			enumName := fmt.Sprintf("%s_%s", enumOriginName, strings.ToLower(value.Name))
			enumName = genutil.NormalizeGoTypeName(enumName)
			v := group.Id(enumName).Op("=").Id(enumGoTypeName).Parens(jen.Lit(value.Name))
			if value.Note != "" {
				v.Comment(value.Note)
			}
		}
	})

	g.types[enum.Name] = jen.Id(enumGoTypeName)

	return f.Save(fmt.Sprintf("%s/%s.enum.go", g.out, genutil.Normalize(enum.Name)))
}

type fieldDefinition struct {
	name      string
	fieldType jen.Code
	nullable  bool
}

func containsString(arr []string, value string) bool {
	for _, str := range arr {
		if strings.ToLower(str) == strings.ToLower(value) {
			return true
		}
	}
	return false
}

func (g *generator) genTable(table core.Table, toColumnNameToRelationships map[string][]core.Relationship, fromColumnNameToRelationships map[string][]core.Relationship) error {
	if containsString(g.excludeTables, table.Name) {
		log.Printf("ignore table %s", table.Name)
		return nil
	}
	f := jen.NewFilePathName(g.out, g.gopackage)

	tableOriginName := genutil.Normalize(table.Name)
	tableGoTypeName := genutil.NormalizeGoTypeName(table.Name)

	f.PackageComment("Code generated by dbml-gen-go-model. DO NOT EDIT.")
	f.PackageComment("Supported by thanhpd56@2020")
	f.Commentf("%s is generated type for table '%s'", tableGoTypeName, tableOriginName)

	var genColumnErr error

	cols := make([]string, 0)
	fields := []fieldDefinition{}

	f.Type().Id(tableGoTypeName).StructFunc(func(group *jen.Group) {

		for _, column := range table.Columns {
			columnName := genutil.NormalLizeGoName(column.Name)
			columnOriginName := genutil.Normalize(column.Name)
			t, ok := g.getJenType(column)
			if !ok {
				genColumnErr = fmt.Errorf("type '%s' is not support", column.Type)
			}

			customTag, found := column.Annotations[GoTagAnnotation]
			tags := structtag.Tags{}
			if found {
				t, err := structtag.Parse(customTag)
				if err != nil {
					fmt.Errorf("error why parse tag %s", customTag)
				}
				tags = *t
			}

			if column.Settings.Note != "" {
				group.Comment(column.Settings.Note)
			}

			for _, t := range g.fieldtags {
				_ = tags.Set(&structtag.Tag{
					Key:     strings.TrimSpace(t),
					Name:    columnOriginName,
					Options: nil,
				})
			}

			tagValues := tagToMap(tags)

			if column.Settings.Null && column.Settings.Default == "" {
				fields = append(fields, fieldDefinition{
					name:      columnName,
					fieldType: t,
					nullable:  true,
				})
				group.Id(columnName).Add(jen.Op("*")).Add(t).Tag(tagValues)
			} else {
				fields = append(fields, fieldDefinition{
					name:      columnName,
					fieldType: t,
					nullable:  false,
				})
				group.Id(columnName).Add(t).Tag(tagValues)
			}
			cols = append(cols, columnOriginName)

			// users.department_id > departments.id
			fullColumnID := fmt.Sprintf("%s.%s", table.Name, column.Name)
			// [{from: }]
			toRelationships := toColumnNameToRelationships[fullColumnID]

			keyGorm := "gorm"
			for _, relationship := range toRelationships {
				// split department_id => to get department
				relationName := strings.Split(column.Name, "_id")[0]
				// split departments.id => departments
				fromTypeName := strings.Split(relationship.From, ".")[0]
				// normalize to Department
				fromGoTypeName := genutil.NormalizeGoTypeName(fromTypeName)

				// split departments.id => departments
				toTypeName := strings.Split(relationship.To, ".")[0]
				// normalize to Department
				toGoTypeName := genutil.NormalizeGoTypeName(toTypeName)

				// normalize relationName department to Department
				fieldName := genutil.NormalizeGoTypeName(relationName)
				goTags := appendTagKey(relationship.Tags, keyGorm, fmt.Sprintf("foreignkey:%s", column.Name))
				tags := tagToMap(goTags)

				if relationship.Type == core.OneToMany {
					group.Id(fieldName).Id(fromGoTypeName).Tag(tags)
				} else if relationship.Type == core.OneToOne {
					group.Id(fieldName).Op("*").Id(fromGoTypeName).Tag(tags)
				} else if relationship.Type == core.ManyToOne {
					group.Id(fieldName).Op("*").Id(toGoTypeName).Tag(tags)
				}
			}
			// departments.id > users.department_id
			fromRelationships := fromColumnNameToRelationships[fullColumnID]

			for _, relationship := range fromRelationships {
				// users.id => users
				toTypeName := strings.Split(relationship.To, ".")[0]
				toColumnName := strings.Split(relationship.To, ".")[1]
				// users => User
				toGoTypeName := genutil.NormalizeGoTypeName(toTypeName)

				// users.id => users
				fromTypeName := strings.Split(relationship.From, ".")[0]
				// users => User
				goTags := appendTagKey(relationship.Tags, keyGorm, fmt.Sprintf("foreignkey:%s", toColumnName))
				tags := tagToMap(goTags)

				fromGoTypeName := genutil.NormalizeGoTypeName(fromTypeName)
				if relationship.Type == core.OneToMany {
					// Users
					name := genutil.GoInitialismCamelCase(toTypeName)
					// Users []User
					group.Id(name).Index().Id(toGoTypeName).Tag(tags)

				} else if relationship.Type == core.OneToOne {
					// User
					name := genutil.NormalizeGoTypeName(toTypeName)
					// User *User
					group.Id(name).Op("*").Id(toGoTypeName).Tag(tags)
				} else if relationship.Type == core.ManyToOne {
					// Users
					name := genutil.GoInitialismCamelCase(fromTypeName)
					// Users []User
					group.Id(name).Index().Id(fromGoTypeName).Tag(tags)
				}
			}
		}
	})

	if genColumnErr != nil {
		return genColumnErr
	}

	tableMetadataType := "__tbl_" + tableOriginName
	tableMetadataColumnsType := tableMetadataType + "_columns"

	f.Commentf("// table '%s' columns list struct", tableOriginName)
	f.Type().Id(tableMetadataColumnsType).StructFunc(func(group *jen.Group) {
		for _, column := range table.Columns {
			name := genutil.NormalLizeGoName(column.Name)
			group.Id(name).String()
		}
	})

	f.Commentf("// table '%s' metadata struct", tableOriginName)
	f.Type().Id("__tbl_"+tableOriginName).Struct(
		jen.Id("Name").String(),
		jen.Id("Columns").Id(tableMetadataColumnsType),
	)

	tableMetadataVar := "_tbl_" + tableOriginName

	f.Commentf("// table '%s' metadata info", tableOriginName)
	f.Var().Id(tableMetadataVar).Op("=").Id(tableMetadataType).Values(jen.DictFunc(func(d jen.Dict) {
		d[jen.Id("Name")] = jen.Lit(tableOriginName)
		d[jen.Id("Columns")] = jen.Id(tableMetadataColumnsType).Values(jen.DictFunc(func(d jen.Dict) {
			for _, column := range table.Columns {
				columnName := genutil.NormalLizeGoName(column.Name)
				columnOriginName := genutil.Normalize(column.Name)
				d[jen.Id(columnName)] = jen.Lit(columnOriginName)
			}
		}))
	}))

	f.Commentf("GetColumns return list columns name for table '%s'", tableOriginName)
	f.Func().Params(
		jen.Op("*").Id(tableMetadataType),
	).Id("GetColumns").Params().Index().String().Block(
		jen.Return(jen.Index().String().ValuesFunc(func(g *jen.Group) {
			for _, col := range cols {
				g.Lit(col)
			}
		})),
	)

	f.Commentf("T return metadata info for table '%s'", tableOriginName)
	f.Func().Params(
		jen.Op("*").Id(tableGoTypeName),
	).Id("T").Params().Op("*").Id(tableMetadataType).Block(
		jen.Return().Op("&").Id(tableMetadataVar),
	)

	for _, field := range fields {
		getterName := fmt.Sprintf("Get%s", field.name)
		f.Commentf(getterName)
		if field.nullable {
			f.Func().Params(
				jen.Id("e").Op("*").Id(tableGoTypeName),
			).Id(getterName).Params().Op("*").Add(field.fieldType).Block(
				jen.Return().Id("e").Dot(field.name),
			)
		} else {
			f.Func().Params(
				jen.Id("e").Op("*").Id(tableGoTypeName),
			).Id(getterName).Params().Add(field.fieldType).Block(
				jen.Return().Id("e").Dot(field.name),
			)
		}

	}

	if g.shouldGenTblName {
		f.Commentf("TableName return table name")
		f.Func().Params(
			jen.Id(tableGoTypeName),
		).Id("TableName").Params().Id("string").Block(
			jen.Return(jen.Lit(tableOriginName)),
		)
	}

	return f.Save(fmt.Sprintf("%s/%s.table.go", g.out, genutil.Normalize(table.Name)))
}

func appendTagKey(goTags structtag.Tags, key string, value string) structtag.Tags {
	_, err := goTags.Get(key)
	if err != nil { // not exit
		_ = goTags.Set(&structtag.Tag{
			Key:  key,
			Name: value,
		})
	} else {
		goTags.AddOptions(key, value)
	}
	return goTags
}

func tagToMap(tags structtag.Tags) map[string]string {
	tagValues := map[string]string{}
	for _, tag := range tags.Tags() {
		tagValues[tag.Key] = tag.Value()
	}
	return tagValues
}

const primeTypePattern = `^(\w+)(\(d+\))?`

var (
	regexType    = regexp.MustCompile(primeTypePattern)
	builtinTypes = map[string]jen.Code{
		"int":       jen.Int(),
		"int8":      jen.Int8(),
		"int16":     jen.Int16(),
		"int32":     jen.Int32(),
		"int64":     jen.Int64(),
		"bigint":    jen.Int64(),
		"uint":      jen.Uint(),
		"uint8":     jen.Uint8(),
		"uint16":    jen.Uint16(),
		"uint32":    jen.Uint32(),
		"uint64":    jen.Uint64(),
		"float":     jen.Float64(),
		"float32":   jen.Float32(),
		"float64":   jen.Float64(),
		"bool":      jen.Bool(),
		"text":      jen.String(),
		"varchar":   jen.String(),
		"char":      jen.String(),
		"byte":      jen.Byte(),
		"rune":      jen.Rune(),
		"timestamp": jen.Int(),
		"json":      jen.Qual("gorm.io/datatypes", "JSON"),
		"datetime":  jen.Qual("time", "Time"),
	}
)

func (g *generator) getJenType(col core.Column) (jen.Code, bool) {
	// generate from annotation
	t, ok := getJenTypeFromAnnotation(col.Annotations[GoTypeAnnotation])
	if ok {
		return t, ok
	}

	// generate from column type
	m := regexType.FindStringSubmatch(col.Type)
	if len(m) >= 2 {
		// lookup for builtin type
		if t, ok := builtinTypes[m[1]]; ok {
			return t, ok
		}
	}
	t, ok = g.types[col.Type]
	return t, ok
}

func getJenTypeFromAnnotation(s string) (jen.Code, bool) {
	if len(s) == 0 {
		return nil, false
	}

	parts := strings.Split(s, ":")
	if len(parts) >= 2 {
		return jen.Qual(parts[0], parts[1]), true
	}
	return jen.Id(parts[0]), true
}

func (g *generator) getFullRelationShips() (toColumnNameToRelationships map[string][]core.Relationship, fromColumnNameToRelationships map[string][]core.Relationship) {
	toColumnNameToRelationships = map[string][]core.Relationship{}
	fromColumnNameToRelationships = map[string][]core.Relationship{}
	for _, ref := range g.dbml.Refs {
		for _, relationship := range ref.Relationships {
			reverseRelationship := core.Relationship{
				From: relationship.To,
				To:   relationship.From,
				Type: reverseRefType(relationship.Type),
				Tags: relationship.Tags,
			}
			fromColumnNameToRelationships[relationship.To] = append(toColumnNameToRelationships[relationship.To], reverseRelationship)

			toColumnNameToRelationships[relationship.From] = append(fromColumnNameToRelationships[relationship.From], relationship)
		}
	}
	for _, table := range g.dbml.Tables {

		// inline relationships
		for _, column := range table.Columns {
			// support inline relationship
			fullColumnID := getFullColumnName(table.Name, column.Name)
			toRelationships := toColumnNameToRelationships[fullColumnID]
			fromRelationships := fromColumnNameToRelationships[fullColumnID]
			fromRefTagStr, found := column.Annotations[GoTagFromRefAnnotation]
			fromTags := structtag.Tags{}
			if found {
				parse, err := structtag.Parse(fromRefTagStr)
				if err != nil {
					log.Fatalf("error when parse tag %s", fromRefTagStr)
				}
				fromTags = *parse
			}
			toRefTagStr, found := column.Annotations[GoTagToRefAnnotation]
			toTags := structtag.Tags{}
			if found {
				parse, err := structtag.Parse(toRefTagStr)
				if err != nil {
					log.Fatalf("error when parse tag %s", toRefTagStr)
				}
				toTags = *parse
			}

			if column.Settings.Ref.To != "" {
				refType := column.Settings.Ref.Type
				toRelationships = append(toRelationships, core.Relationship{
					From: fullColumnID,
					To:   column.Settings.Ref.To,
					Type: refType,
					Tags: fromTags,
				})

				reverseRefType := reverseRefType(refType)
				fromRelationships = append(fromRelationships, core.Relationship{
					From: column.Settings.Ref.To,
					To:   fullColumnID,
					Type: reverseRefType,
					Tags: toTags,
				})

				toColumnNameToRelationships[fullColumnID] = toRelationships
				fromColumnNameToRelationships[column.Settings.Ref.To] = fromRelationships
			}
		}
	}

	return
}

func getFullColumnName(tableName string, columnName string) string {
	return fmt.Sprintf("%s.%s", tableName, columnName)
}

func reverseRefType(refType core.RelationshipType) core.RelationshipType {
	var reverseRefType core.RelationshipType = core.OneToOne
	if refType == core.OneToMany {
		reverseRefType = core.ManyToOne
	}
	if refType == core.ManyToOne {
		reverseRefType = core.OneToMany
	}
	return reverseRefType
}
