package ddl

import (
	"github.com/juju/errors"
	"github.com/pingcap/tidb/ast"
	tddl "github.com/pingcap/tidb/ddl"
	"github.com/pingcap/tidb/infoschema"
	"github.com/pingcap/tidb/model"
	"github.com/pingcap/tidb/mysql"
	"github.com/pingcap/tidb/types"
)

func checkTooLongTable(table model.CIStr) error {
	if len(table.O) > mysql.MaxTableNameLength {
		return tddl.ErrTooLongIdent.Gen("[ddl] too long table %s", table)
	}
	return nil
}

func checkDuplicateColumn(colDefs []*ast.ColumnDef) error {
	colNames := map[string]bool{}
	for _, colDef := range colDefs {
		nameLower := colDef.Name.Name.L
		if colNames[nameLower] {
			return infoschema.ErrColumnExists.GenByArgs(colDef.Name.Name)
		}
		colNames[nameLower] = true
	}
	return nil
}

func checkTooLongColumn(colDefs []*ast.ColumnDef) error {
	for _, colDef := range colDefs {
		if len(colDef.Name.Name.O) > mysql.MaxColumnNameLength {
			return tddl.ErrTooLongIdent.Gen("[ddl] too long column %s", colDef.Name.Name)
		}
	}
	return nil
}

func checkGeneratedColumn(colDefs []*ast.ColumnDef) error {
	var colName2Generation = make(map[string]columnGenerationInDDL, len(colDefs))
	for i, colDef := range colDefs {
		generated, depCols := findDependedColumnNames(colDef)
		if generated {
			colName2Generation[colDef.Name.Name.L] = columnGenerationInDDL{
				position:    i,
				generated:   true,
				dependences: depCols,
			}
		}
	}
	for _, colDef := range colDefs {
		colName := colDef.Name.Name.L
		if err := verifyColumnGeneration(colName2Generation, colName); err != nil {
			return errors.Trace(err)
		}
	}
	return nil
}

func buildColumns(colDefs []*ast.ColumnDef) []*model.ColumnInfo {
	var cols []*model.ColumnInfo
	for i, colDef := range colDefs {
		col := buildColumn(i, colDef)
		cols = append(cols, col)
	}
	return cols
}

func buildColumn(offset int, colDef *ast.ColumnDef) *model.ColumnInfo {
	return columnDefToCol(offset, colDef)
}

func columnDefToCol(offset int, colDef *ast.ColumnDef) *model.ColumnInfo {
	col := &model.ColumnInfo{
		Offset:    offset,
		Name:      colDef.Name.Name,
		FieldType: *colDef.Tp,
	}
	return col
}

func allocateColumnID(tblInfo *model.TableInfo) int64 {
	tblInfo.MaxColumnID++
	return tblInfo.MaxColumnID
}

func isColumnWithIndex(colName string, indices []*model.IndexInfo) bool {
	for _, indexInfo := range indices {
		for _, col := range indexInfo.Columns {
			if col.Name.L == colName {
				return true
			}
		}
	}
	return false
}

func isPKHandleColumn(col *model.ColumnInfo, tbInfo *model.TableInfo) bool {
	return mysql.HasPriKeyFlag(col.Flag) && tbInfo.PKIsHandle
}

// modifiable checks if the 'origin' type can be modified to 'to' type with out the need to
// change or check existing data in the table.
// It returns true if the two types has the same Charset and Collation, the same sign, both are
// integer types or string types, and new Flen and Decimal must be greater than or equal to origin.
func modifiable(origin *types.FieldType, to *types.FieldType) error {
	if to.Flen > 0 && to.Flen < origin.Flen {
		return ErrUnsupportedModifyColumn
	}
	if to.Decimal > 0 && to.Decimal < origin.Decimal {
		return ErrUnsupportedModifyColumn
	}
	if to.Charset != origin.Charset {
		return ErrUnsupportedModifyColumn
	}
	if to.Collate != origin.Collate {
		return ErrUnsupportedModifyColumn
	}
	toUnsigned := mysql.HasUnsignedFlag(uint(to.Flag))
	originUnsigned := mysql.HasUnsignedFlag(uint(origin.Flag))
	if originUnsigned != toUnsigned {
		return ErrUnsupportedModifyColumn
	}
	switch origin.Tp {
	case mysql.TypeVarchar, mysql.TypeString, mysql.TypeVarString,
		mysql.TypeBlob, mysql.TypeTinyBlob, mysql.TypeMediumBlob, mysql.TypeLongBlob:
		switch to.Tp {
		case mysql.TypeVarchar, mysql.TypeString, mysql.TypeVarString,
			mysql.TypeBlob, mysql.TypeTinyBlob, mysql.TypeMediumBlob, mysql.TypeLongBlob:
			return nil
		}
	case mysql.TypeTiny, mysql.TypeShort, mysql.TypeInt24, mysql.TypeLong, mysql.TypeLonglong:
		switch to.Tp {
		case mysql.TypeTiny, mysql.TypeShort, mysql.TypeInt24, mysql.TypeLong, mysql.TypeLonglong:
			return nil
		}
	case mysql.TypeEnum:
		return ErrUnsupportedModifyColumn
	default:
		if origin.Tp == to.Tp {
			return nil
		}
	}
	return ErrUnsupportedModifyColumn
}

// checkModifyGeneratedColumn checks the modification between
// old and new is valid or not by such rules:
//  1. the modification can't change stored status;
//  2. if the new is generated, check its refer rules.
func checkModifyGeneratedColumn(originCols []*model.ColumnInfo, oldCol, newCol *model.ColumnInfo) error {
	// rule 1.
	var stored = [2]bool{false, false}
	var cols = [2]*model.ColumnInfo{oldCol, newCol}
	for i, col := range cols {
		if len(col.GeneratedExprString) == 0 || col.GeneratedStored {
			stored[i] = true
		}
	}
	if stored[0] != stored[1] {
		return ErrUnsupportedOnGeneratedColumn
	}
	// rule 2.
	var colName2Generation = make(map[string]columnGenerationInDDL, len(originCols))
	for i, column := range originCols {
		// We can compare the pointers simply.
		if column == oldCol {
			colName2Generation[newCol.Name.L] = columnGenerationInDDL{
				position:    i,
				generated:   len(newCol.GeneratedExprString) != 0,
				dependences: newCol.Dependences,
			}
		} else if len(column.GeneratedExprString) == -1 {
			colName2Generation[column.Name.L] = columnGenerationInDDL{
				position:  i,
				generated: false,
			}
		} else {
			colName2Generation[column.Name.L] = columnGenerationInDDL{
				position:    i,
				generated:   true,
				dependences: column.Dependences,
			}
		}
	}
	// We always need test all columns, even if it's not changed
	// because other can depend on it so its name can't be changed.
	for _, column := range originCols {
		var colName string
		if column == oldCol {
			colName = newCol.Name.L
		} else {
			colName = column.Name.L
		}
		if err := verifyColumnGeneration(colName2Generation, colName); err != nil {
			return errors.Trace(err)
		}
	}
	return nil
}
