package mysql

import (
	"strconv"

	"github.com/pingcap/errors"
	"github.com/starainrt/go-mysql/utils"
)

type RowData []byte

func (p RowData) Parse(f []*Field, binary bool, dst []FieldValue) ([]FieldValue, error) {
	if binary {
		return p.ParseBinary(f, dst)
	} else {
		return p.ParseText(f, dst)
	}
}

func (p RowData) ParseText(f []*Field, dst []FieldValue) ([]FieldValue, error) {
	for len(dst) < len(f) {
		dst = append(dst, FieldValue{})
	}
	data := dst[:len(f)]

	var err error
	var v []byte
	var isNull bool
	var pos, n int

	for i := range f {
		v, isNull, n, err = LengthEncodedString(p[pos:])
		if err != nil {
			return nil, errors.Trace(err)
		}

		pos += n

		if isNull {
			data[i].Type = FieldValueTypeNull
		} else {
			isUnsigned := f[i].Flag&UNSIGNED_FLAG != 0

			switch f[i].Type {
			case MYSQL_TYPE_TINY, MYSQL_TYPE_SHORT, MYSQL_TYPE_INT24,
				MYSQL_TYPE_LONGLONG, MYSQL_TYPE_LONG, MYSQL_TYPE_YEAR:
				if isUnsigned {
					var val uint64
					data[i].Type = FieldValueTypeUnsigned
					val, err = strconv.ParseUint(utils.ByteSliceToString(v), 10, 64)
					data[i].value = val
				} else {
					var val int64
					data[i].Type = FieldValueTypeSigned
					val, err = strconv.ParseInt(utils.ByteSliceToString(v), 10, 64)
					data[i].value = utils.Int64ToUint64(val)
				}
			case MYSQL_TYPE_FLOAT, MYSQL_TYPE_DOUBLE:
				var val float64
				data[i].Type = FieldValueTypeFloat
				val, err = strconv.ParseFloat(utils.ByteSliceToString(v), 64)
				data[i].value = utils.Float64ToUint64(val)
			default:
				data[i].Type = FieldValueTypeString
				data[i].str = append(data[i].str[:0], v...)
			}

			if err != nil {
				return nil, errors.Trace(err)
			}
		}
	}

	return data, nil
}

// ParseBinary parses the binary format of data
// see https://dev.mysql.com/doc/internals/en/binary-protocol-value.html
func (p RowData) ParseBinary(f []*Field, dst []FieldValue) ([]FieldValue, error) {
	for len(dst) < len(f) {
		dst = append(dst, FieldValue{})
	}
	data := dst[:len(f)]

	if p[0] != OK_HEADER {
		return nil, ErrMalformPacket
	}

	pos := 1 + ((len(f) + 7 + 2) >> 3)

	nullBitmap := p[1:pos]

	var isNull bool
	var n int
	var err error
	var v []byte
	for i := range data {
		if nullBitmap[(i+2)/8]&(1<<(uint(i+2)%8)) > 0 {
			data[i].Type = FieldValueTypeNull
			continue
		}

		isUnsigned := f[i].Flag&UNSIGNED_FLAG != 0

		switch f[i].Type {
		case MYSQL_TYPE_NULL:
			data[i].Type = FieldValueTypeNull
			continue

		case MYSQL_TYPE_TINY:
			if isUnsigned {
				v := ParseBinaryUint8(p[pos : pos+1])
				data[i].Type = FieldValueTypeUnsigned
				data[i].value = uint64(v)
			} else {
				v := ParseBinaryInt8(p[pos : pos+1])
				data[i].Type = FieldValueTypeSigned
				data[i].value = utils.Int64ToUint64(int64(v))
			}
			pos++
			continue

		case MYSQL_TYPE_SHORT, MYSQL_TYPE_YEAR:
			if isUnsigned {
				v := ParseBinaryUint16(p[pos : pos+2])
				data[i].Type = FieldValueTypeUnsigned
				data[i].value = uint64(v)
			} else {
				v := ParseBinaryInt16(p[pos : pos+2])
				data[i].Type = FieldValueTypeSigned
				data[i].value = utils.Int64ToUint64(int64(v))
			}
			pos += 2
			continue

		case MYSQL_TYPE_INT24, MYSQL_TYPE_LONG:
			if isUnsigned {
				v := ParseBinaryUint32(p[pos : pos+4])
				data[i].Type = FieldValueTypeUnsigned
				data[i].value = uint64(v)
			} else {
				v := ParseBinaryInt32(p[pos : pos+4])
				data[i].Type = FieldValueTypeSigned
				data[i].value = utils.Int64ToUint64(int64(v))
			}
			pos += 4
			continue

		case MYSQL_TYPE_LONGLONG:
			if isUnsigned {
				v := ParseBinaryUint64(p[pos : pos+8])
				data[i].Type = FieldValueTypeUnsigned
				data[i].value = v
			} else {
				v := ParseBinaryInt64(p[pos : pos+8])
				data[i].Type = FieldValueTypeSigned
				data[i].value = utils.Int64ToUint64(v)
			}
			pos += 8
			continue

		case MYSQL_TYPE_FLOAT:
			v := ParseBinaryFloat32(p[pos : pos+4])
			data[i].Type = FieldValueTypeFloat
			data[i].value = utils.Float64ToUint64(float64(v))
			pos += 4
			continue

		case MYSQL_TYPE_DOUBLE:
			v := ParseBinaryFloat64(p[pos : pos+8])
			data[i].Type = FieldValueTypeFloat
			data[i].value = utils.Float64ToUint64(v)
			pos += 8
			continue

		case MYSQL_TYPE_DECIMAL, MYSQL_TYPE_NEWDECIMAL, MYSQL_TYPE_VARCHAR,
			MYSQL_TYPE_BIT, MYSQL_TYPE_ENUM, MYSQL_TYPE_SET, MYSQL_TYPE_TINY_BLOB,
			MYSQL_TYPE_MEDIUM_BLOB, MYSQL_TYPE_LONG_BLOB, MYSQL_TYPE_BLOB,
			MYSQL_TYPE_VAR_STRING, MYSQL_TYPE_STRING, MYSQL_TYPE_GEOMETRY, MYSQL_TYPE_JSON:
			v, isNull, n, err = LengthEncodedString(p[pos:])
			pos += n
			if err != nil {
				return nil, errors.Trace(err)
			}

			if !isNull {
				data[i].Type = FieldValueTypeString
				data[i].str = append(data[i].str[:0], v...)
				continue
			} else {
				data[i].Type = FieldValueTypeNull
				continue
			}

		case MYSQL_TYPE_DATE, MYSQL_TYPE_NEWDATE:
			var num uint64
			num, isNull, n = LengthEncodedInt(p[pos:])

			pos += n

			if isNull {
				data[i].Type = FieldValueTypeNull
				continue
			}

			data[i].Type = FieldValueTypeString
			data[i].str, err = FormatBinaryDate(int(num), p[pos:])
			pos += int(num)

			if err != nil {
				return nil, errors.Trace(err)
			}

		case MYSQL_TYPE_TIMESTAMP, MYSQL_TYPE_DATETIME:
			var num uint64
			num, isNull, n = LengthEncodedInt(p[pos:])

			pos += n

			if isNull {
				data[i].Type = FieldValueTypeNull
				continue
			}

			data[i].Type = FieldValueTypeString
			data[i].str, err = FormatBinaryDateTime(int(num), p[pos:])
			pos += int(num)

			if err != nil {
				return nil, errors.Trace(err)
			}

		case MYSQL_TYPE_TIME:
			var num uint64
			num, isNull, n = LengthEncodedInt(p[pos:])

			pos += n

			if isNull {
				data[i].Type = FieldValueTypeNull
				continue
			}

			data[i].Type = FieldValueTypeString
			data[i].str, err = FormatBinaryTime(int(num), p[pos:])
			pos += int(num)

			if err != nil {
				return nil, errors.Trace(err)
			}

		default:
			return nil, errors.Errorf("Stmt Unknown FieldType %d %s", f[i].Type, f[i].Name)
		}
	}

	return data, nil
}
