// Copyright 2020 The Xorm Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package tags

import (
	"encoding/gob"
	"errors"
	"fmt"
	"reflect"
	"strings"
	"sync"
	"time"
	"unicode"

	"xorm.io/xorm/caches"
	"xorm.io/xorm/convert"
	"xorm.io/xorm/dialects"
	"xorm.io/xorm/names"
	"xorm.io/xorm/schemas"
)

// ErrUnsupportedType represents an unsupported type error
var ErrUnsupportedType = errors.New("unsupported type")

// TableIndices is an interface that describes structs that provide additional index information above that which is automatically parsed
type TableIndices interface {
	TableIndices() []*schemas.Index
}

var tpTableIndices = reflect.TypeOf((*TableIndices)(nil)).Elem()

type TableCollations interface {
	TableCollations() []*schemas.Collation
}

var tpTableCollations = reflect.TypeOf((*TableCollations)(nil)).Elem()

// Parser represents a parser for xorm tag
type Parser struct {
	identifier   string
	dialect      dialects.Dialect
	columnMapper names.Mapper
	tableMapper  names.Mapper
	handlers     map[string]Handler
	cacherMgr    *caches.Manager
	tableCache   sync.Map // map[reflect.Type]*schemas.Table
}

// NewParser creates a tag parser
func NewParser(identifier string, dialect dialects.Dialect, tableMapper, columnMapper names.Mapper, cacherMgr *caches.Manager) *Parser {
	return &Parser{
		identifier:   identifier,
		dialect:      dialect,
		tableMapper:  tableMapper,
		columnMapper: columnMapper,
		handlers:     defaultTagHandlers,
		cacherMgr:    cacherMgr,
	}
}

// GetTableMapper returns table mapper
func (parser *Parser) GetTableMapper() names.Mapper {
	return parser.tableMapper
}

// SetTableMapper sets table mapper
func (parser *Parser) SetTableMapper(mapper names.Mapper) {
	parser.ClearCaches()
	parser.tableMapper = mapper
}

// GetColumnMapper returns column mapper
func (parser *Parser) GetColumnMapper() names.Mapper {
	return parser.columnMapper
}

// SetColumnMapper sets column mapper
func (parser *Parser) SetColumnMapper(mapper names.Mapper) {
	parser.ClearCaches()
	parser.columnMapper = mapper
}

// SetIdentifier sets tag identifier
func (parser *Parser) SetIdentifier(identifier string) {
	parser.ClearCaches()
	parser.identifier = identifier
}

// ParseWithCache parse a struct with cache
func (parser *Parser) ParseWithCache(v reflect.Value) (*schemas.Table, error) {
	t := v.Type()
	tableI, ok := parser.tableCache.Load(t)
	if ok {
		return tableI.(*schemas.Table), nil
	}

	table, err := parser.Parse(v)
	if err != nil {
		return nil, err
	}

	parser.tableCache.Store(t, table)

	if parser.cacherMgr.GetDefaultCacher() != nil {
		if v.CanAddr() {
			gob.Register(v.Addr().Interface())
		} else {
			gob.Register(v.Interface())
		}
	}

	return table, nil
}

// ClearCacheTable removes the database mapper of a type from the cache
func (parser *Parser) ClearCacheTable(t reflect.Type) {
	parser.tableCache.Delete(t)
}

// ClearCaches removes all the cached table information parsed by structs
func (parser *Parser) ClearCaches() {
	parser.tableCache = sync.Map{}
}

func addIndex(indexName string, table *schemas.Table, col *schemas.Column, indexType int) {
	if index, ok := table.Indexes[indexName]; ok {
		index.AddColumn(col.Name)
		col.Indexes[index.Name] = indexType
	} else {
		index := schemas.NewIndex(indexName, indexType)
		index.AddColumn(col.Name)
		table.AddIndex(index)
		col.Indexes[index.Name] = indexType
	}
}

// ErrIgnoreField represents an error to ignore field
var ErrIgnoreField = errors.New("field will be ignored")

func (parser *Parser) getSQLTypeByType(t reflect.Type) (schemas.SQLType, error) {
	if t.Kind() == reflect.Ptr {
		t = t.Elem()
	}
	if t.Kind() == reflect.Struct {
		v, ok := parser.tableCache.Load(t)
		if ok {
			pkCols := v.(*schemas.Table).PKColumns()
			if len(pkCols) == 1 {
				return pkCols[0].SQLType, nil
			}
			if len(pkCols) > 1 {
				return schemas.SQLType{}, fmt.Errorf("unsupported mulitiple primary key on cascade")
			}
		}
	}
	return schemas.Type2SQLType(t), nil
}

func (parser *Parser) parseFieldWithNoTag(fieldIndex int, field reflect.StructField, fieldValue reflect.Value) (*schemas.Column, error) {
	var sqlType schemas.SQLType
	if fieldValue.CanAddr() {
		if _, ok := fieldValue.Addr().Interface().(convert.Conversion); ok {
			sqlType = schemas.SQLType{Name: schemas.Text}
		}
	}
	if _, ok := fieldValue.Interface().(convert.Conversion); ok {
		sqlType = schemas.SQLType{Name: schemas.Text}
	} else {
		var err error
		sqlType, err = parser.getSQLTypeByType(field.Type)
		if err != nil {
			return nil, err
		}
	}
	col := schemas.NewColumn(parser.columnMapper.Obj2Table(field.Name),
		field.Name, sqlType, sqlType.DefaultLength,
		sqlType.DefaultLength2, true)
	col.FieldIndex = []int{fieldIndex}

	if field.Type.Kind() == reflect.Int64 && (strings.ToUpper(col.FieldName) == "ID" || strings.HasSuffix(strings.ToUpper(col.FieldName), ".ID")) {
		col.IsAutoIncrement = true
		col.IsPrimaryKey = true
		col.Nullable = false
	}
	return col, nil
}

func (parser *Parser) parseFieldWithTags(table *schemas.Table, fieldIndex int, field reflect.StructField, fieldValue reflect.Value, tags []tag) (*schemas.Column, error) {
	col := &schemas.Column{
		FieldName:       field.Name,
		FieldIndex:      []int{fieldIndex},
		Nullable:        true,
		IsPrimaryKey:    false,
		IsAutoIncrement: false,
		MapType:         schemas.TWOSIDES,
		Indexes:         make(map[string]int),
		DefaultIsEmpty:  true,
	}

	ctx := Context{
		table:      table,
		col:        col,
		fieldValue: fieldValue,
		indexNames: make(map[string]int),
		parser:     parser,
	}

	for j, tag := range tags {
		if ctx.ignoreNext {
			ctx.ignoreNext = false
			continue
		}

		ctx.tag = tag
		ctx.tagUname = strings.ToUpper(tag.name)

		if j > 0 {
			ctx.preTag = strings.ToUpper(tags[j-1].name)
		}
		if j < len(tags)-1 {
			ctx.nextTag = tags[j+1].name
		} else {
			ctx.nextTag = ""
		}

		if h, ok := parser.handlers[ctx.tagUname]; ok {
			if err := h(&ctx); err != nil {
				return nil, err
			}
		} else {
			if strings.HasPrefix(ctx.tag.name, "'") && strings.HasSuffix(ctx.tag.name, "'") {
				col.Name = ctx.tag.name[1 : len(ctx.tag.name)-1]
			} else {
				col.Name = ctx.tag.name
			}
		}

		if ctx.hasCacheTag {
			if parser.cacherMgr.GetDefaultCacher() != nil {
				parser.cacherMgr.SetCacher(table.Name, parser.cacherMgr.GetDefaultCacher())
			} else {
				parser.cacherMgr.SetCacher(table.Name, caches.NewLRUCacher2(caches.NewMemoryStore(), time.Hour, 10000))
			}
		}
		if ctx.hasNoCacheTag {
			parser.cacherMgr.SetCacher(table.Name, nil)
		}
	}

	if col.SQLType.Name == "" {
		var err error
		col.SQLType, err = parser.getSQLTypeByType(field.Type)
		if err != nil {
			return nil, err
		}
	}
	if ctx.isUnsigned && col.SQLType.IsNumeric() && !strings.HasPrefix(col.SQLType.Name, "UNSIGNED") {
		col.SQLType.Name = "UNSIGNED " + col.SQLType.Name
	}

	parser.dialect.SQLType(col)
	if col.Length == 0 {
		col.Length = col.SQLType.DefaultLength
	}
	if col.Length2 == 0 {
		col.Length2 = col.SQLType.DefaultLength2
	}
	if col.Name == "" {
		col.Name = parser.columnMapper.Obj2Table(field.Name)
	}

	if ctx.isUnique {
		ctx.indexNames[col.Name] = schemas.UniqueType
	} else if ctx.isIndex {
		ctx.indexNames[col.Name] = schemas.IndexType
	}

	for indexName, indexType := range ctx.indexNames {
		addIndex(indexName, table, col, indexType)
	}

	return col, nil
}

func (parser *Parser) parseField(table *schemas.Table, fieldIndex int, field reflect.StructField, fieldValue reflect.Value) (*schemas.Column, error) {
	if isNotTitle(field.Name) {
		return nil, ErrIgnoreField
	}

	var (
		tag       = field.Tag
		ormTagStr = strings.TrimSpace(tag.Get(parser.identifier))
	)
	if ormTagStr == "-" {
		return nil, ErrIgnoreField
	}
	if ormTagStr == "" {
		return parser.parseFieldWithNoTag(fieldIndex, field, fieldValue)
	}
	tags, err := splitTag(ormTagStr)
	if err != nil {
		return nil, err
	}
	return parser.parseFieldWithTags(table, fieldIndex, field, fieldValue, tags)
}

func isNotTitle(n string) bool {
	for _, c := range n {
		return unicode.IsLower(c)
	}
	return true
}

// Parse parses a struct as a table information
func (parser *Parser) Parse(v reflect.Value) (*schemas.Table, error) {
	t := v.Type()
	if t.Kind() == reflect.Ptr {
		t = t.Elem()
		v = v.Elem()
	}
	if t.Kind() != reflect.Struct {
		return nil, ErrUnsupportedType
	}

	table := schemas.NewEmptyTable()
	table.Type = t
	table.Name = names.GetTableName(parser.tableMapper, v)
	table.Comment = names.GetTableComment(v)

	for i := 0; i < t.NumField(); i++ {
		col, err := parser.parseField(table, i, t.Field(i), v.Field(i))
		if err == ErrIgnoreField {
			continue
		} else if err != nil {
			return nil, err
		}

		table.AddColumn(col)
	} // end for

	indices := tableIndices(v)
	for _, index := range indices {
		// Override old information
		if oldIndex, ok := table.Indexes[index.Name]; ok {
			for _, colName := range oldIndex.Cols {
				col := table.GetColumn(colName)
				if col == nil {
					return nil, ErrUnsupportedType
				}
				delete(col.Indexes, index.Name)
			}
		}
		table.AddIndex(index)
		for _, colName := range index.Cols {
			col := table.GetColumn(colName)
			if col == nil {
				return nil, ErrUnsupportedType
			}
			col.Indexes[index.Name] = index.Type
		}
	}

	collations := tableCollations(v)
	for _, collation := range collations {
		if collation.Name == "" {
			continue
		}
		if collation.Column == "" {
			table.Collation = collation.Name
		} else {
			col := table.GetColumn(collation.Column)
			if col == nil {
				return nil, ErrUnsupportedType
			}
			col.Collation = collation.Name // this may override definition in struct tag
		}
	}

	return table, nil
}

func tableIndices(v reflect.Value) []*schemas.Index {
	if v.Type().Implements(tpTableIndices) {
		return v.Interface().(TableIndices).TableIndices()
	}

	if v.Kind() == reflect.Ptr {
		v = v.Elem()
		if v.Type().Implements(tpTableIndices) {
			return v.Interface().(TableIndices).TableIndices()
		}
	} else if v.CanAddr() {
		v1 := v.Addr()
		if v1.Type().Implements(tpTableIndices) {
			return v1.Interface().(TableIndices).TableIndices()
		}
	}
	return nil
}

func tableCollations(v reflect.Value) []*schemas.Collation {
	if v.Type().Implements(tpTableCollations) {
		return v.Interface().(TableCollations).TableCollations()
	}

	if v.Kind() == reflect.Ptr {
		v = v.Elem()
		if v.Type().Implements(tpTableCollations) {
			return v.Interface().(TableCollations).TableCollations()
		}
	} else if v.CanAddr() {
		v1 := v.Addr()
		if v1.Type().Implements(tpTableCollations) {
			return v1.Interface().(TableCollations).TableCollations()
		}
	}
	return nil
}
