package datagen

import (
	"math"
	"strconv"
	"strings"
	"unsafe"
	_ "unsafe" // required by go:linkname

	"github.com/pingcap/log"

	"github.com/pingcap/tidb/parser/mysql"
	"github.com/pingcap/tidb/parser/types"
	"github.com/pingcap/tidb/parser/ast"
	"go.uber.org/zap"
)

// TODO generate null value
// TODO  json,bool data generate

func init() {
	gens = make(map[types.EvalType]func(def *ast.ColumnDef) string)
	gens[types.ETInt] = etIntGen
	gens[types.ETReal] = etRealAndDecimalGen
	gens[types.ETDecimal] = etRealAndDecimalGen
	gens[types.ETString] = etStringGen
	gens[types.ETDatetime] = etDatetimeGen
	gens[types.ETTimestamp] = etTimestampGen
	gens[types.ETDuration] = etDurationGen
}

const letterBytes = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
const (
	letterIdxBits = 6                    // 6 bits to represent a letter index
	letterIdxMask = 1<<letterIdxBits - 1 // All 1-bits, as many as letterIdxBits
	letterIdxMax  = 32 / letterIdxBits   // # of letter indices fitting in 63 bits
)
const blobLen = 1<<8 - 1
const pkProducerCap = 1000 * 1000

var gens map[types.EvalType]func(def *ast.ColumnDef) string
var months = []uint32{31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}

//go:linkname RandUint32 runtime.fastrand
func RandUint32() uint32

func remainder(v1, v2 uint32) uint32 {
	return uint32(uint64(v1) * uint64(v2) >> 32)
}

func randomValue(def *ast.ColumnDef) string {
	if f, ok := gens[def.Tp.EvalType()]; ok {
		return f(def)
	}
	log.Fatal("eval type generator not found",
		zap.String("eval type", def.Tp.InfoSchemaStr()))
	return ""
}

/*
mysql.TypeDate, mysql.TypeDatetime
A date. The supported range is '1000-01-01' to '9999-12-31'. MySQL displays DATE values in 'YYYY-MM-DD' format,
but permits assignment of values to DATE columns using either strings or numbers.

A date and time combination. The supported range is '1000-01-01 00:00:00.000000' to '9999-12-31 23:59:59.999999'.
MySQL displays DATETIME values in 'YYYY-MM-DD hh:mm:ss[.fraction]' format, but permits assignment of values to DATETIME columns using either strings or numbers.
*/
func etDatetimeGen(g *ast.ColumnDef) string {
	r := RandUint32()
	year := remainder(r, 9999-1000+1) + 1000
	month := remainder(r, 12) + 1
	days := months[month-1]
	if month == 2 && isLeap(year) {
		days += 1
	}
	days = remainder(r, days) + 1
	res := float64(year*100*100 + month*100 + days)
	if g.Tp.Tp == mysql.TypeDatetime {
		hour := remainder(r, 24)
		minute := remainder(r, 60)
		second := remainder(r, 60)
		res = res*100*100*100 + float64(100*100*hour+100*minute+second)
		if g.Tp.Decimal >= 0 {
			res = res + float64(remainder(r, uint32(math.Pow10(g.Tp.Decimal))))*math.Pow10(-g.Tp.Decimal)
		}
	}
	return strconv.FormatFloat(res, 'f', -1, 64)
}

func isLeap(year uint32) bool {
	return remainder(year, 400) == 0 || (remainder(year, 4) == 0 && remainder(year, 100) != 0)
}

/*
mysql.TypeTimestamp
A timestamp. The range is '1970-01-01 00:00:01.000000' UTC to '2038-01-19 03:14:07.999999' UTC.
TIMESTAMP values are stored as the number of seconds since the epoch ('1970-01-01 00:00:00' UTC).
A TIMESTAMP cannot represent the value '1970-01-01 00:00:00' because that is equivalent to 0 seconds from the epoch and the value 0 is reserved for representing '0000-00-00 00:00:00', the “zero” TIMESTAMP value.
*/
func etTimestampGen(g *ast.ColumnDef) string {
	r := RandUint32()
	// avoid overflow and time zone differs, year from 1971 ~ 2037
	year := remainder(r, 2037-1971+1) + 1971
	month := remainder(r, 12) + 1
	days := months[month-1]
	if month == 2 && isLeap(year) {
		days += 1
	}
	days = remainder(r, days) + 1
	res := float64(year*100*100 + month*100 + days)
	hour := remainder(r, 24)
	minute := remainder(r, 60)
	second := remainder(r, 60)
	// 19700101000000
	if year == 1970 && month == 1 && days == 1 && hour+minute+second == 0 {
		second = remainder(r, 60-1) + 1
	}
	res = res*100*100*100 + float64(100*100*hour+100*minute+second)
	if g.Tp.Decimal >= 0 {
		res = res + float64(remainder(r, uint32(math.Pow10(g.Tp.Decimal))))*math.Pow10(-g.Tp.Decimal)
	}
	return strconv.FormatFloat(res, 'f', -1, 64)
}

/*
mysql.TypeDuration
A time. The range is '-838:59:59.000000' to '838:59:59.000000'.
MySQL displays TIME values in 'hh:mm:ss[.fraction]' format, but permits assignment of values to TIME columns using either strings or numbers.
*/
func etDurationGen(g *ast.ColumnDef) string {
	r := RandUint32()
	hour := remainder(r, 839)
	minute := remainder(r, 60)
	second := remainder(r, 60)
	res := float64(hour*100*100 + minute*100 + second)
	if g.Tp.Decimal > 0 {
		fraction := float64(remainder(r, uint32(math.Pow10(g.Tp.Decimal)))) * math.Pow10(-g.Tp.Decimal)
		if hour == 838 && minute == 59 && second == 59 {
			fraction = 0
		}
		res += fraction
	}
	if r&1 == 0 {
		res = 0 - res
	}
	return strconv.FormatFloat(res, 'f', -1, 64)
}

/*
For integer data types, M indicates the maximum display width.
The maximum display width is 255. Display width is unrelated to the range of values a type can store.
A type of BIT(M) enables storage of M-bit values. M can range from 1 to 64.
As 4-digit strings in the range '1901' to '2155'.
As 4-digit numbers in the range 1901 to 2155.
skip 2-digit format year

mysql.TypeTiny, mysql.TypeShort, mysql.TypeInt24, mysql.TypeLong, mysql.TypeLonglong, mysql.TypeBit, mysql.TypeYear
TINYINT(TypeTiny)		1
SMALLINT(TypeShort)		2
MEDIUMINT(TypeInt24)	3
INT(TypeLong)			4
BIGINT(TypeLonglong)	8
*/
func etIntGen(g *ast.ColumnDef) string {
	var res int64
	r1 := RandUint32()
	r := uint64(r1)<<32 + uint64(r1) + 1

	switch g.Tp.Tp {
	case mysql.TypeTiny, mysql.TypeShort, mysql.TypeInt24, mysql.TypeLong, mysql.TypeLonglong:
		l := mysql.DefaultLengthOfMysqlTypes[g.Tp.Tp]
		res = int64(r >> (64 - 8*l))
		if !mysql.HasUnsignedFlag(g.Tp.Flag) {
			if r&1 == 0 {
				res = -((res >> 1) + 1)
			} else {
				res = res >> 1
			}
		}
	case mysql.TypeBit:
		res = int64(r & (1<<g.Tp.Flen - 1))
	case mysql.TypeYear:
		res = int64(remainder(r1, 2155-1901+1) + 1901)
	}
	return strconv.FormatInt(res, 10)
}

/*
没有 Double(M) 形式
FLOAT(p) -- parser 可以自动识别
A floating-point number. p represents the precision in bits,
but MySQL uses this value only to determine whether to use FLOAT or DOUBLE for the resulting data type.
If p is from 0 to 24, the data type becomes FLOAT with no M or D values.
If p is from 25 to 53, the data type becomes DOUBLE with no M or D values.

FLOAT[(M,D)] DOUBLE[(M,D)] DECIMAL[(M[,D])]
M is the total number of digits and D is the number of digits following the decimal point.
for example
Standard SQL requires that DECIMAL(5,2) be able to store any value with five digits and two decimals,
so values that can be stored in the salary column range from -999.99 to 999.99.
*/
func etRealAndDecimalGen(g *ast.ColumnDef) string {
	r1 := RandUint32()
	r := uint64(r1)<<32 + uint64(r1) + 1
	// float/double/decimal(M,D), decimal(M)
	if g.Tp.Flen != types.UnspecifiedLength {
		var value strings.Builder
		precision := g.Tp.Flen
		scale := g.Tp.Decimal
		v1 := r % uint64(math.Pow10(precision-scale))
		v2 := r % uint64(math.Pow10(scale))
		if !mysql.HasUnsignedFlag(g.Tp.Flag) && r&1 == 0 {
			value.WriteString("-")
		}
		value.WriteString(strconv.FormatUint(v1, 10))
		value.WriteString(".")
		value.WriteString(strconv.FormatUint(v2, 10))
		return value.String()
	}
	// float,double,decimal,float(M)
	var res int64
	l := mysql.DefaultLengthOfMysqlTypes[g.Tp.Tp]
	res = int64(r >> (64 - 8*l))
	if !mysql.HasUnsignedFlag(g.Tp.Flag) {
		if r&1 == 0 {
			res = -((res >> 1) + 1)
		} else {
			res = res >> 1
		}
	}
	return strconv.FormatInt(res, 10)
}

/*
TypeVarchar,TypeTinyBlob,TypeMediumBlob,TypeLongBlob,TypeBlob,TypeVarString,TypeString
TypeEnum,TypeSet
*/
func etStringGen(g *ast.ColumnDef) string {
	r := RandUint32()
	if g.Tp.Tp == mysql.TypeEnum || g.Tp.Tp == mysql.TypeSet {
		return g.Tp.Elems[remainder(r, uint32(len(g.Tp.Elems)))]
	}
	flen := g.Tp.Flen
	if flen == types.UnspecifiedLength && g.Tp.Tp == mysql.TypeString {
		flen = 1
	}
	/*
		TINYBLOB, TINYTEXT	L + 1 bytes, where L < 2^8
		BLOB, TEXT	L + 2 bytes, where L < 2^16
		MEDIUMBLOB, MEDIUMTEXT	L + 3 bytes, where L < 2^24
		LONGBLOB, LONGTEXT	L + 4 bytes, where L < 2^32
		simple rule
	*/
	switch g.Tp.Tp {
	case mysql.TypeTinyBlob:
		flen = blobLen
	case mysql.TypeBlob:
		flen = blobLen << 1
	case mysql.TypeMediumBlob:
		flen = blobLen << 2
	case mysql.TypeLongBlob:
		flen = blobLen << 3
	}
	return randStringGen(flen)
}

func randStringGen(n int) string {
	b := make([]byte, n)
	for i, cache, remain := n-1, RandUint32(), letterIdxMax; i >= 0; {
		if remain == 0 {
			cache, remain = RandUint32(), letterIdxMax
		}
		if idx := int(cache & letterIdxMask); idx < len(letterBytes) {
			b[i] = letterBytes[idx]
			i--
		}
		cache >>= letterIdxBits
		remain--
	}
	return *(*string)(unsafe.Pointer(&b))
}