package data

import (
	"fmt"
	"math/rand"
	"strconv"
	"strings"
	"time"

	"github.com/ngaut/log"
)

func init() {
	rand.Seed(time.Now().UnixNano())
}

// DDLType is ddl type
const (
	// create table
	CreateTable = iota
	DropTable

	// column
	AddColumn
	DeletColumn
	AlterColumn

	// AddAndAlterColumn generates two consecutive ddls to simulate incompatible ddls
	// changing varchar to shorter length
	AddAndAlterColumn

	// AddWithIndexAndDeleteColumn adds ad column, add index on it and then delete the column
	// to simulate incompatible ddls
	AddWithIndexAndDeleteColumn

	// index
	CreateIndex
	DropIndex

	// db
	CreateDatabase
	DropDatatbase

	// some builtin skipped ddl type
	Flush
	OptimizeTable
	CreateView
)

// DML
const (
	Insert = iota
	Update
	Delete
)

// index category
const (
	NormalIndex = iota
	PrimaryIndex
	UniqueIndex
	FulltextIndex
)

var (
	typeSet = []string{
		// "BIT3","BIT64",
		// "TINYINT","TINYINTUNSIGNED",
		// "SMALLINT", "SMALLINTUNSIGNED",
		// "MEDIUMINT","MEDIUMINTUNSIGNED",
		"INT",
		"INTUNSIGNED",
		"BIGINT",
		"BIGINTUNSIGNED",
		"BOOLEAN",
		"DOUBLE",
		"DOUBLEUNSIGNED",
		// "FLOAT","FLOAT24","FLOAT53",
		"DECIMAL",
		"DATE",
		"DATETIME",
		"TIMESTAMP",
		"TIME",
		"CHAR",
		"VARCHAR",
		"BLOB",
		"YEAR",
		"TEXT",
		"SET",
		"ENUM",
		// "SET64",
		"ENUM257",
		// "TIMESTAMPONUPDATE",
	}

	uniqSet = map[string]string{
		"INTUNSIGNED":       "INT UNSIGNED",
		"BIGINTUNSIGNED":    "BIGINT UNSIGNED",
		"DOUBLEUNSIGNED":    "DOUBLE UNSIGNED",
		"DECIMAL":           "DECIMAL(38,19)",
		"VARCHAR":           "VARCHAR(10)",
		"ENUM":              "ENUM('enum1', 'enum2', 'enum3')",
		"SET":               "SET('a', 'b', 'c')",
		"SET64":             fmt.Sprintf("SET(%s)", genFixStr(64, "set")),
		"ENUM257":           fmt.Sprintf("ENUM(%s)", genFixStr(257, "enum")),
		"TIMESTAMPONUPDATE": "TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP",
	}

	unsupportIndex = []string{
		"TEXT",
		"BLOB",
	}
)

func supportIndex(index string) bool {
	for _, i := range unsupportIndex {
		if index == i {
			return false
		}
	}
	return true
}

func genFixStr(length int, prefix string) string {
	var sets []string
	for i := 0; i < length; i++ {
		set := fmt.Sprintf("'%s%d'", prefix, i)
		sets = append(sets, set)
	}
	return strings.Join(sets, ",")
}

// usage: genAddColumnSQL("tb1", "c1", "int")
func genAddColumnSQL(schemaName, tableName, columnName, columnDef string) string {
	def, ok := uniqSet[columnDef]
	if ok {
		columnDef = def
	}
	if columnDef == "TIMESTAMP" {
		return fmt.Sprintf("ALTER TABLE %s  ADD COLUMN %s %s NOT NULL DEFAULT '0000-00-00 00:00:00';", tableName, columnName, columnDef)
	}
	return fmt.Sprintf("ALTER TABLE `%s`.`%s` ADD COLUMN %s %s;", schemaName, tableName, columnName, columnDef)
}

// usage: genDelColumnSQL("tb1", "c1")
func genDelColumnSQL(schemaName, tableName, columnName string) string {
	return fmt.Sprintf("ALTER TABLE `%s`.`%s` DROP COLUMN %s;", schemaName, tableName, columnName)
}

func genAlterColumnDefSQL(schemaName, tableName, columnName, newDef string) string {
	def, ok := uniqSet[newDef]
	if ok {
		newDef = def
	}
	if newDef == "TIMESTAMP" {
		return fmt.Sprintf("ALTER TABLE `%s`.`%s`  MODIFY COLUMN %s %s NOT NULL DEFAULT '0000-00-00 00:00:00';", schemaName, tableName, columnName, newDef)
	}
	return fmt.Sprintf("ALTER TABLE `%s`.`%s`  MODIFY COLUMN %s %s;", schemaName, tableName, columnName, newDef)
}

// can extend this
func genCreateIndex(schemaName, tableName, columnName, indexName string, indexCategory int, indexType string) string {
	switch indexCategory {
	case NormalIndex:
		return fmt.Sprintf("CREATE INDEX %s USING %s ON `%s`.`%s` (%s);", indexName, indexType, schemaName, tableName, columnName)
	case PrimaryIndex:
		return fmt.Sprintf("CREATE PRIMARY INDEX %s USING %s ON `%s`.`%s` (%s);", indexName, indexType, schemaName, tableName, columnName)
	case UniqueIndex:
		return fmt.Sprintf("CREATE UNIQUE INDEX %s USING %s ON `%s`.`%s` (%s);", indexName, indexType, schemaName, tableName, columnName)
	case FulltextIndex:
		return fmt.Sprintf("CREATE FULLTEXT INDEX %s USING %s ON `%s`.`%s` (%s);", indexName, indexType, schemaName, tableName, columnName)
	}
	return fmt.Sprintf("")
}

func genDropIndex(schemaName, tableName, indexName string) string {
	return fmt.Sprintf("DROP INDEX %s ON `%s`.`%s`;", indexName, schemaName, tableName)
}

func genFlushSQL(schemaName, tableName string, options ...string) string {
	switch options[0] {
	case "TABLES":
		return fmt.Sprintf("FLUSH TABLES `%s`.`%s`;", schemaName, tableName)
	default:
		return fmt.Sprintf("FLUSH %s;", strings.Join(options, " "))
	}
}

func genOptimizeSQL(schemaName, tableName string) string {
	return fmt.Sprintf("OPTIMIZE TABLE `%s`.`%s`;", schemaName, tableName)
}

func genCreateViewSQL(schemaName, tableName, viewName string) string {
	return fmt.Sprintf("CREATE VIEW %s AS SELECT * FROM `%s`.`%s`", viewName, schemaName, tableName)
}

// genInitTable gens init table
func genInitTable(schemaName, tableName string) string {
	return fmt.Sprintf("CREATE TABLE IF NOT EXISTS `%s`.`%s` (id bigint NOT NULL AUTO_INCREMENT, primary key(id)) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin;", schemaName, tableName)
}

func dropTable(schemaName, tableName string) string {
	return fmt.Sprintf("DROP TABLE IF EXISTS `%s`.`%s`;", schemaName, tableName)
}

func genRandomColumnDef() string {
	return typeSet[rand.Intn(len(typeSet))]
}

func getMapKey(mymap map[string]string) []string {
	keys := make([]string, 0, len(mymap))
	for k := range mymap {
		keys = append(keys, k)
	}
	return keys
}

func getMapKeyForIndex(mymap map[string]*Index) []string {
	keys := make([]string, 0, len(mymap))
	for k := range mymap {
		keys = append(keys, k)
	}
	return keys
}

// usage: genRandomValue("INT")
func genRandomValue(columnType string) string {
	log.Debugf("type is %s", columnType)
	booleans := []string{"TRUE", "FALSE"}
	enums := []string{"'enum1'", "'enum2'", "'enum3'"}
	sets := []string{"''", "'a'", "'b'", "'c'", "'a,b'", "'b,c'", "'a,c'", "'a,b,c'"}
	upper := strings.ToUpper(columnType)
	switch upper {
	case "INT":
		return strconv.FormatInt(int64(rand.Int31()), 10)
	case "INTUNSIGNED":
		return strconv.FormatInt(int64(rand.Int31()), 10)
	case "BOOLEAN":
		return booleans[rand.Intn(len(booleans))]
	case "BIGINT":
		return strconv.FormatInt(rand.Int63(), 10)
	case "BIGINTUNSIGNED":
		return strconv.FormatInt(rand.Int63(), 10)
	case "DOUBLE":
		return strconv.FormatFloat(rand.ExpFloat64(), 'f', 5, 64)
	case "DOUBLEUNSIGNED":
		return strconv.FormatFloat(rand.ExpFloat64(), 'f', 5, 64)
	case "DECIMAL":
		return strconv.FormatFloat(rand.ExpFloat64(), 'f', 5, 64) // FIXME
	case "DATE":
		t := genRandomTime()
		return fmt.Sprintf("'%.4d-%.2d-%.2d'", t.Year(), t.Month(), t.Day())
	case "DATETIME", "TIMESTAMP", "TIMESTAMPONUPDATE":
		t := genRandomTime()
		return fmt.Sprintf("'%.4d-%.2d-%.2d %.2d:%.2d:%.2d'", t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second())
	case "TIME":
		t := genRandomTime()
		return fmt.Sprintf("'%.2d:%.2d:%.2d'", t.Hour(), t.Minute(), t.Second())
	case "YEAR":
		t := genRandomTime()
		return fmt.Sprintf("'%.4d'", t.Year())
	case "CHAR":
		return genRandomUnicodeString(1)
	case "VARCHAR":
		return genRandomUnicodeString(5) // update length if needed
	case "BLOB":
		return genRandomByteString(10)
	case "TEXT":
		return genRandomUnicodeString(20)
	case "ENUM":
		return enums[rand.Intn(len(enums))]
	case "ENUM257":
		return fmt.Sprintf("'%s%d'", "enum", rand.Intn(257))
	case "SET":
		return sets[rand.Intn(len(sets))]
	case "SET64":
		return fmt.Sprintf("'%s%d'", "set", rand.Intn(64))
	}
	return "''"
}

func genRandomTime() time.Time {
	min := time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC).Unix()
	max := time.Date(2037, 12, 31, 0, 0, 0, 0, time.UTC).Unix()
	delta := max - min
	sec := rand.Int63n(delta) + min
	return time.Unix(sec, 0)
}

func genRandomString(n int) string {
	var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
	b := make([]rune, n)
	for i := range b {
		b[i] = letterRunes[rand.Intn(len(letterRunes))]
	}
	return string(b)
}

func genRandomUnicodeString(n int) string {
	var builder strings.Builder
	builder.Grow(2 + 3*n)
	builder.WriteByte('\'')
	for i := 0; i < n; i++ {
		// 50% chance generating ASCII string, 50% chance generating Unicode string
		var r rune
		switch rand.Intn(2) {
		case 0:
			r = rune(rand.Intn(0x80))
		case 1:
			r = rune(rand.Intn(0xd800))
		}
		switch r {
		case '\'':
			builder.WriteString("''")
		case '\\':
			builder.WriteString(`\\`)
		default:
			builder.WriteRune(r)
		}
	}
	builder.WriteByte('\'')
	return builder.String()
}

func genRandomByteString(n int) string {
	var builder strings.Builder
	builder.Grow(3 + 2*n)
	builder.WriteString("x'")
	for i := 0; i < n; i++ {
		fmt.Fprintf(&builder, "%02X", rand.Intn(256))
	}
	builder.WriteString("'")
	return builder.String()
}

func genRandomFlushOption() string {
	opts := []string{"ERROR LOGS", "RELAY LOGS", "SLOW LOGS", "QUERY CACHE"}
	return opts[rand.Intn(len(opts))]
}
