package models

import (
	"context"
	"github.com/fiskerinc/cloud-services/services/jetfire/utils"
	"math"
	"sync"
	"time"

	"github.com/fiskerinc/cloud-services/pkg/grpc/kafka_grpc"
	"github.com/fiskerinc/cloud-services/pkg/logger"
	"github.com/fiskerinc/cloud-services/pkg/utils/envtool"
	"github.com/ClickHouse/ch-go"
	"github.com/ClickHouse/ch-go/proto"
	"github.com/pkg/errors"
	"github.com/rs/zerolog"
	"github.com/sony/gobreaker"
)

type InsertBlockType int

const (
	NilBlockType InsertBlockType = -1 //insert buffers using this block type will have a nil block.

	PivotBlockType InsertBlockType = iota
	SignalBlockType
	VinLastBlockType
)

var (
	initialBlockPoolSize = envtool.GetEnvInt("JETFIRE_MIN_BLOCKS", 2)
	maxBlockPoolSize     = envtool.GetEnvInt("JETFIRE_MAX_BLOCKS", 4)
	maxBufferBytes       = envtool.GetEnvInt("JETFIRE_BUFFER_MAX_BYTES", 128<<20) / maxBlockPoolSize
)

const NO_TIMESTAMP_LOG_SAMPLE_RATE = 10000
var logSampler zerolog.Logger
// Creating a log sampler
func init(){
	logSampler = logger.Sample(zerolog.RandomSampler(NO_TIMESTAMP_LOG_SAMPLE_RATE))
}

// InsertBuffer abstracts buffer appends and insertions to clickhouse, even for different underlying schemas.
// A pool of blocks are allocated for each InsertBuffer.
//
//	These pools do not implement leaky buffer pattern to ensure data order and timeliness.
type InsertBuffer struct {
	InsertTime  time.Time
	insertDelay time.Duration

	blockPool []insertBlock //pool of all blocks, including busy blocks.

	//linked list of available blocks in the pool
	freeLock sync.Mutex //mutex for linked list
	freeHead *BlockNode
	freeTail *BlockNode

	blockType InsertBlockType
	tripInfo  bool

	TableName string
}

// linked list node for insertBlock
type BlockNode struct {
	block insertBlock
	next  *BlockNode
}

// Command struct for inserter goroutine
type InsertCommand struct {
	Buffer *InsertBuffer
	Block  insertBlock
}

func NewInsertBuffer(tripInfo bool, signalNames []string, tableName string, insertDelay time.Duration, blockType InsertBlockType) *InsertBuffer {
	newBlockPool := []insertBlock{}

	for i := 0; i < initialBlockPoolSize; i++ {
		var newBlock insertBlock = AllocateNewBlock(signalNames, maxBufferBytes, blockType, tripInfo)

		if newBlock != nil {
			newBlockPool = append(newBlockPool, newBlock)
		}
	}

	newBuffer := new(InsertBuffer)
	newBuffer.insertDelay = insertDelay
	newBuffer.blockPool = newBlockPool
	newBuffer.TableName = tableName
	newBuffer.blockType = blockType
	newBuffer.tripInfo = tripInfo

	newBuffer.InitFreeBlocksList()

	return newBuffer
}

// memory block linked list functions. These are for available blocks that are ready to accept data.
func (buffer *InsertBuffer) AppendFreeBlock(block insertBlock) {
	buffer.freeLock.Lock()
	defer buffer.freeLock.Unlock()

	if buffer.freeHead == nil {
		buffer.freeHead = &BlockNode{block: block}

		buffer.freeTail = buffer.freeHead
		return
	}
	if buffer.freeTail == nil { //this should never occur!
		err := errors.Errorf("Unexpected nil tail for InsertBuffer blocks with non nil head!")
		logger.Error().Err(err).Send()
		return
	}

	buffer.freeTail.next = &BlockNode{block: block}
	buffer.freeTail = buffer.freeTail.next
}

func (buffer *InsertBuffer) PopFreeBlock() insertBlock {
	buffer.freeLock.Lock()
	defer buffer.freeLock.Unlock()

	blockNode := buffer.freeHead
	if blockNode == buffer.freeTail {
		buffer.freeTail = nil
	}
	if blockNode == nil {
		return nil
	}

	buffer.freeHead = blockNode.next
	return blockNode.block
}

func (buffer *InsertBuffer) PeekFreeBlock() insertBlock {
	buffer.freeLock.Lock()
	defer buffer.freeLock.Unlock()

	blockNode := buffer.freeHead
	if blockNode == nil {
		return nil
	}
	return blockNode.block
}

// allocates a new block with given params and returns it
func AllocateNewBlock(signalNames []string, maxBufferBytes int, blockType InsertBlockType, tripInfo bool) insertBlock {
	var newBlock insertBlock = nil
	if blockType == PivotBlockType {
		newBlock = NewProtoPivotBlock(signalNames, maxBufferBytes, tripInfo)
	} else if blockType == SignalBlockType {
		newBlock = NewProtoSignalBlock(signalNames, maxBufferBytes)
	} else if blockType == VinLastBlockType {
		newBlock = NewProtoVinLastBlock(signalNames, maxVinCount, tripInfo)
	}

	return newBlock
}

// appends a row to the buffer.
// Row must match the expected type by the underlying proto block.
func (buffer *InsertBuffer) AppendRow(signalNames []string, row interface{}, producerChannel chan InsertCommand) error {
	block := buffer.PeekFreeBlock()
	loggedWaiting := false

	for block != nil && block.IsFull() {
		//if the block is full before append, then pop it and put it on the producer channel.
		buffer.ProduceBlock(producerChannel)
		block = buffer.PeekFreeBlock()
	}

	for block == nil {
		//no more free blocks are available...
		// if the buffer is able to allocate more blocks, then do so. otherwise, wait for a block...
		if len(buffer.blockPool) == maxBlockPoolSize {
			if !loggedWaiting {
				loggedWaiting = true
				logger.Warn().Msgf("no available %s blocks for appending, waiting for available block...", buffer.TableName)
			}
			//wait
			time.Sleep(100 * time.Millisecond)
			block = buffer.PeekFreeBlock()
		} else {
			logger.Info().Msgf("%s no available block, allocating new block", buffer.TableName)
			block = AllocateNewBlock(signalNames, maxBufferBytes, buffer.blockType, buffer.tripInfo)
			buffer.blockPool = append(buffer.blockPool, block)
			buffer.AppendFreeBlock(block)
		}
	}

	err := block.AppendRow(signalNames, row)

	//if block is full after append, pop it from linked list and put onto producer channel
	if block.IsFull() || block.TimeSinceThreshold(buffer.insertDelay) {
		buffer.ProduceBlock(producerChannel)
	}
	return err
}

func (buffer *InsertBuffer) ProduceBlock(producerChannel chan InsertCommand) {
	if producerChannel != nil {
		logger.Debug().Msgf("ProduceBlock...")
		producerChannel <- InsertCommand{
			Buffer: buffer,
			Block:  buffer.PopFreeBlock(),
		}
	}
}

// searches blockpool for nonEmpty blocks
func (buffer *InsertBuffer) getNonEmptyBlock() insertBlock {
	for _, block := range buffer.blockPool {
		if block.Len() > 0 {
			return block
		}
	}
	return buffer.blockPool[0]
}

// returns any nonempty input from buffer
func (buffer *InsertBuffer) GetInput() proto.Input {
	return buffer.getNonEmptyBlock().GetProtoInput(true, nil)
}

// reinitializes the blocks list, assuming any empty block in the pool is free and can be added to list.
// returns size of blocks list
func (buffer *InsertBuffer) InitFreeBlocksList() int {
	buffer.freeHead = nil
	buffer.freeTail = nil
	count := 0
	for _, block := range buffer.blockPool {
		if block.Len() != 0 {
			continue
		}
		buffer.AppendFreeBlock(block)
		count++
	}
	return count
}

// Inserts and flushes only the head block.
func InsertAndFlushHead(buffer *InsertBuffer, ctx context.Context, conn *ch.Client, breaker *gobreaker.CircuitBreaker, signalsSet map[string]bool) (int, error) {
	block := buffer.PopFreeBlock()

	if buffer == nil || block == nil {
		return 0, errors.Errorf("attempted to insert with nil buffer")
	}
	block.Lock()
	defer block.Unlock()

	rows := block.Len()
	protoInput := block.GetProtoInput(false, signalsSet)
	if protoInput == nil {
		// empty buffer, just return
		return 0, nil
	}
	queryContext, cancel := context.WithTimeout(ctx, 60*time.Second)
	defer cancel()

	insertFunc := func() (interface{}, error) {
		return nil, conn.Do(queryContext, ch.Query{
			Body:  protoInput.Into(buffer.TableName),
			Input: protoInput,
		})
	}
	_, err := breaker.Execute(insertFunc)
	if err != nil {
		return 0, err
	}

	block.Flush(false)
	buffer.AppendFreeBlock(block)

	return rows, err
}

// Inserts a block and flushes it, does not readd to free blocks list if error occurred
func InsertAndFlush(command InsertCommand, ctx context.Context, conn *ch.Client, breaker *gobreaker.CircuitBreaker, signalsSet map[string]bool) (int, error) {
	buffer := command.Buffer
	block := command.Block

	if buffer == nil || block == nil {
		return 0, errors.Errorf("attempted to insert with nil buffer")
	}
	block.Lock()
	defer block.Unlock()

	rows := block.Len()
	protoInput := block.GetProtoInput(false, signalsSet)
	if protoInput == nil {
		// empty buffer, just return
		return 0, nil
	}
	queryContext, cancel := context.WithTimeout(ctx, 5*time.Minute)
	defer cancel()

	logger.Debug().Msgf("Inserting %d rows to %s...", rows, conn.ServerInfo().DisplayName)

	insertFunc := func() (interface{}, error) {
		return nil, conn.Do(queryContext, ch.Query{
			Body:  protoInput.Into(buffer.TableName),
			Input: protoInput,
		})
	}
	_, err := breaker.Execute(insertFunc)
	if err != nil {
		return 0, err
	}

	block.Flush(false)
	buffer.AppendFreeBlock(block)

	return rows, err
}

func (buffer *InsertBuffer) Len() int {
	length := 0
	for _, block := range buffer.blockPool {
		length += block.Len()
	}
	return length
}

func (buffer *InsertBuffer) Cap() int {
	length := 0
	for _, block := range buffer.blockPool {
		length += block.Cap()
	}
	return length
}

func (buffer *InsertBuffer) Resize(signals []string) {
	for _, block := range buffer.blockPool {
		block.Resize(signals, maxBufferBytes)
	}
}

type insertBlock interface {
	AppendRow(signalNames []string, row interface{}) error
	GetProtoInput(useLock bool, signalsSet map[string]bool) proto.Input
	Flush(useLock bool)
	IsFull() bool
	GetInsertBlockType() InsertBlockType
	Resize(signals []string, maxBytes int)
	Lock()
	Unlock()
	Len() int
	Cap() int
	IsBusy() bool
	TimeSinceThreshold(time.Duration) bool
}

type protoPivotBlock struct {
	lock sync.Mutex
	busy bool

	length   int
	capacity int

	maxBytes int

	vin       proto.ColStr
	timestamp proto.ColDateTime64
	tripStart proto.ColDateTime64
	tripID    proto.ColStr
	data      []proto.ColFloat64

	startTime *time.Time

	columnNames []string

	appendTripInfo bool
}

// block type with issue
func NewProtoPivotBlock(signalNames []string, maxBufferBytes int, tripInfo bool) *protoPivotBlock {
	newBlock := protoPivotBlock{
		columnNames:    signalNames,
		appendTripInfo: tripInfo,
	}
	logger.Debug().Msgf("NEW PIVOT BLOCK: %d %d", len(signalNames), maxBufferBytes)
	newBlock.Resize(signalNames, maxBufferBytes)
	return &newBlock
}

func (block *protoPivotBlock) Lock() {
	block.lock.Lock()
	block.busy = true
}

func (block *protoPivotBlock) Unlock() {
	block.lock.Unlock()
	block.busy = false
}

func (block *protoPivotBlock) IsBusy() bool {
	return block.busy
}

func (block *protoPivotBlock) Len() int {
	return block.length
}

func (block *protoPivotBlock) Cap() int {
	return block.capacity
}

func (block *protoPivotBlock) TimeSinceThreshold(threshold time.Duration) bool {
	return block.startTime == nil || time.Since(*block.startTime) > threshold
}

// Resizes this pivotBlock to the desired number of signal columns, and scales rows to not exceed maxBytes
func (block *protoPivotBlock) Resize(signals []string, maxBytes int) {
	block.Lock()
	defer block.Unlock()

	if len(signals) == len(block.columnNames) && maxBytes == block.maxBytes {
		block.Flush(false)
		return
	}

	oldCapacity := block.capacity
	oldWidth := len(block.columnNames)

	block.columnNames = signals
	block.maxBytes = maxBytes

	block.capacity = block.estimateMaxRows(len(signals), maxBytes)
	block.length = 0

	logger.Debug().Msgf("resizing block to %d rows, %d columns", block.capacity, len(signals))

	//reallocate memory only if block dimensions have changed
	if oldCapacity != block.capacity || oldWidth != len(signals) {
		block.vin.Buf = make([]byte, 0, utils.MaxVinLength*block.capacity)
		block.vin.Pos = make([]proto.Position, 0, block.capacity)

		block.timestamp.Data = make([]proto.DateTime64, 0, block.capacity)
		block.tripStart.Data = make([]proto.DateTime64, 0, block.capacity)
		block.timestamp.WithPrecision(proto.PrecisionNano)
		block.tripStart.WithPrecision(proto.PrecisionNano)

		block.tripID.Buf = make([]byte, 0, (utils.MaxVinLength+utils.MaxTimestampLength+1)*block.capacity)
		block.tripID.Pos = make([]proto.Position, 0, block.capacity)

		block.data = make([]proto.ColFloat64, len(signals))
		for i := range block.data {
			block.data[i] = make([]float64, 0, block.capacity)
		}
	}

	block.Flush(false)
}

func (block *protoPivotBlock) GetInsertBlockType() InsertBlockType {
	return PivotBlockType
}

func (block *protoPivotBlock) IsFull() bool {
	block.Lock()
	defer block.Unlock()
	return block.length >= block.capacity
}

// Given upper bound for bytes, estimate the maximum number of rows to allocate
func (block *protoPivotBlock) estimateMaxRows(numDataColumns int, maxBytes int) int {
	rowBytes := 20 + 2 + 8 + 8*numDataColumns //vin, timestamp, data columns per row
	if block.appendTripInfo {
		rowBytes += 8 + 44 + 2 //tripstart and tripid
	}
	return maxBytes / rowBytes
}

// Appends a VehicleState as a row to this Block.
func (block *protoPivotBlock) AppendRow(signalNames []string, row interface{}) error {
	block.Lock()
	defer block.Unlock()

	if block.length >= block.capacity {
		return errors.WithStack(utils.ErrInsertFullBlock)
	}
	if len(signalNames) != len(block.data) {
		return errors.WithStack(utils.ErrInsertWrongColumns)
	}
	state, valid := row.(*VehicleState)
	if !valid {
		return errors.WithStack(utils.ErrInvalidAppendType)
	}
	block.length++

	if block.startTime == nil {
		time := time.Now()
		block.startTime = &time
	}

	block.vin.Append(state.VIN)
	block.timestamp.Append(state.Timestamp)
	if block.appendTripInfo {
		block.tripStart.Append(state.TripStart)
		block.tripID.Append(state.TripID)
	}

	// 3 hour leeway
	// Saw a lot of signals with a delay around 3 hours, going to now only check for signals over a day old
	catchTime := state.Timestamp.Add(-time.Hour * 24)
	for i, signal := range signalNames {
		value, valid := state.StateValues[signal]
		if !valid {
			value = math.NaN()
		} else {
			// If the signal is not included, I'm not going to check its timing. 
			// should no linger see the !ok case
			// Block that should have my issue
			signalTime, ok := state.StateTimes[signal]
			if !ok {
				// So a lot of signals do not have a timestamp on them, not sure why
				logSampler.Warn().Str("Location", "protoPivotBlock").Str("VIN", state.VIN).Str("Signal", signal).
				Str("Location", "protoPivotBlock").Float64("Value", value).Int("Sample Rate", NO_TIMESTAMP_LOG_SAMPLE_RATE).Msg("AppendRow No Timestamp")
			} else {
				// If the signal is from 3 hours before the suggested time of the signal
				if signalTime.Before(catchTime) {
					logger.Warn().Str("VIN", state.VIN).
						Str("Signal", signal).
						Float64("Value", value).
						Time("timestamp.state", state.Timestamp).
						Time("timestamp.signal", signalTime).
						Str("Location", "protoPivotBlock").
						Msg("AppendRow Timestamp Old")
				}
			}
		}

		block.data[i].Append(value)
	}

	return nil
}

// Clears and Resets the buffers in the Block.
func (block *protoPivotBlock) Flush(useLock bool) {
	if useLock {
		block.Lock()
		defer block.Unlock()
	}
	block.length = 0
	block.startTime = nil

	block.vin.Reset()
	block.timestamp.Reset()
	block.tripStart.Reset()
	block.tripID.Reset()

	for i := range block.data {
		block.data[i].Reset()
	}

}

// Gets protocol input struct for ch-go batch insertion
func (block *protoPivotBlock) GetProtoInput(useLock bool, signalsSet map[string]bool) proto.Input {
	if useLock {
		block.Lock()
		defer block.Unlock()
	}

	if block.length == 0 {
		return nil
	}

	input := proto.Input{
		{Name: "VIN", Data: block.vin},
		{Name: "Timestamp", Data: block.timestamp},
	}

	if block.appendTripInfo {
		input = append(input, proto.InputColumn{Name: "TripStart", Data: block.tripStart})
		input = append(input, proto.InputColumn{Name: "TripID", Data: block.tripID})
	}

	for i := range block.data {
		columnName := block.columnNames[i]
		ok := true
		if len(signalsSet) > 0 {
			_, ok = signalsSet[columnName]
		}
		if !ok {
			continue
		}
		input = append(input, proto.InputColumn{Name: columnName, Data: block.data[i]})
	}
	return input
}

// Block struct for feature_table_last type of schema
// This block aggregates only 1 row per VIN. vinIndexMap maps the VIN to a row index.
type protoVinLastBlock struct {
	protoPivotBlock
	vinIndexMap map[string]int
}

func NewProtoVinLastBlock(signalNames []string, numVINs int, tripInfo bool) *protoVinLastBlock {
	newBlock := protoVinLastBlock{}
	newBlock.columnNames = append(newBlock.columnNames, signalNames...)
	newBlock.vinIndexMap = make(map[string]int)
	newBlock.appendTripInfo = tripInfo

	logger.Debug().Msgf("NEW VIN LAST BLOCK: %d %d", len(signalNames), maxBufferBytes)

	bytesPerVIN := 20 + 16 + 16 + 20 //VIN (str), Timestamp, TripStart (timestamp), TripID (str)
	bytesPerVIN += 8 * len(signalNames)

	newBlock.Resize(signalNames, bytesPerVIN*numVINs)
	return &newBlock
}

func (block *protoVinLastBlock) Flush(useLock bool) {
	if useLock {
		block.Lock()
		defer block.Unlock()
	}
	block.protoPivotBlock.Flush(false)

	block.vinIndexMap = make(map[string]int)
}

// Appends a VehicleState as a row to this Block.
// Investigate this code vs protoPivotBlock. WHy the differences, why two of the same thing?
func (block *protoVinLastBlock) AppendRow(signalNames []string, row interface{}) error {
	block.Lock()
	defer block.Unlock()

	if block.length >= block.capacity {
		return errors.WithStack(utils.ErrInsertFullBlock)
	}
	if len(signalNames) != len(block.data) {
		return errors.WithStack(utils.ErrInsertWrongColumns)
	}
	state, valid := row.(*VehicleState)
	if !valid {
		return errors.WithStack(utils.ErrInvalidAppendType)
	}

	vinIndex, ok := block.vinIndexMap[state.VIN]

	if block.startTime == nil {
		time := time.Now()
		block.startTime = &time
	}

	if !ok {
		// append state to the end of the buffer
		block.length++

		block.vin.Append(state.VIN)
		block.timestamp.Append(state.Timestamp)
		if block.appendTripInfo {
			block.tripStart.Append(state.TripStart)
			block.tripID.Append(state.TripID)
		}

		// catchTime := state.Timestamp.Add(-time.Hour * 3)
		for i, signal := range signalNames {
			value, valid := state.StateValues[signal]
			if !valid {
				value = math.NaN()
			} else {
				// If the signal is not included, I'm not going to check its timing.
				// should no linger see the !ok case 
				// signalTime, ok := state.StateTimes[signal]
				// if !ok {
				// 	logger.Warn().Str("Location", "protoVinLastBlock, !ok").Msgf("AppendRow no timestamp for %s", signal)
				// } else {
				// 	// If the signal is from 3 hours before the suggested time of the signal
				// 	if signalTime.Before(catchTime) {
				// 		logger.Warn().Str("VIN", state.VIN).
				// 			Str("Signal", signal).
				// 			Time("timestamp.state", state.Timestamp).
				// 			Time("timestamp.signal", signalTime).
				// 			Str("Location", "protoVinLastBlock, !ok").
				// 			Msg("AppendRow Timestamp Old")
				// 	}
				// }
			}

			block.data[i].Append(value)
		}

		block.vinIndexMap[state.VIN] = block.length - 1
	} else {
		// override only the row mapped to the vin
		block.timestamp.Data[vinIndex] = proto.ToDateTime64(state.Timestamp, block.timestamp.Precision)
		block.tripStart.Data[vinIndex] = proto.ToDateTime64(state.TripStart, block.timestamp.Precision)
		SetColStr(&block.tripID, state.TripID, vinIndex)

		//catchTime := state.Timestamp.Add(-time.Hour * 3)
		for i, signal := range signalNames {
			value, valid := state.StateValues[signal]
			if !valid {
				value = math.NaN()
			} else {
				// If the signal is not included, I'm not going to check its timing. 
				// should no linger see the !ok case
				// signalTime, ok := state.StateTimes[signal]
				// if !ok {
				// 	logger.Warn().Str("Location", "protoVinLastBlock, ok").Msgf("AppendRow no timestamp for %s", signal)
				// } else {
				// 	// If the signal is from 3 hours before the suggested time of the signal
				// 	if signalTime.Before(catchTime) {
				// 		logger.Warn().Str("VIN", state.VIN).
				// 			Str("Signal", signal).
				// 			Time("timestamp.state", state.Timestamp).
				// 			Time("timestamp.signal", signalTime).
				// 			Str("Location", "protoVinLastBlock, ok").
				// 			Msg("AppendRow Timestamp Old")
				// 	}
				// }
			}

			block.data[i][vinIndex] = value
		}
	}
	return nil

}

// Block struct for vehicle_signal type of schema
type protoSignalBlock struct {
	lock sync.Mutex
	busy bool

	length   int
	capacity int

	maxBytes int

	startTime *time.Time

	vin       proto.ColStr
	timestamp proto.ColDateTime64
	id        proto.ColInt16
	name      proto.ColStr
	value     proto.ColFloat64
}

func NewProtoSignalBlock(signalNames []string, maxBufferBytes int) *protoSignalBlock {
	newBlock := protoSignalBlock{}
	newBlock.Resize(signalNames, maxBufferBytes)
	return &newBlock
}

func (block *protoSignalBlock) Lock() {
	block.lock.Lock()
	block.busy = true
}

func (block *protoSignalBlock) Unlock() {
	block.lock.Unlock()
	block.busy = false
}

func (block *protoSignalBlock) IsBusy() bool {
	return block.busy
}

func (block *protoSignalBlock) Len() int {
	return block.length
}

func (block *protoSignalBlock) Cap() int {
	return block.capacity
}

func (block *protoSignalBlock) TimeSinceThreshold(threshold time.Duration) bool {
	return block.startTime == nil || time.Since(*block.startTime) > threshold
}

// Resizes allocated memory for the Block, given maxBytes as the upper bound for memory size
func (block *protoSignalBlock) Resize(signals []string, maxBytes int) {
	block.Lock()
	defer block.Unlock()

	if maxBytes == block.maxBytes {
		block.Flush(false)
		return
	}

	oldCapacity := block.capacity

	block.maxBytes = maxBytes
	block.capacity = block.estimateMaxRows(maxBytes)

	logger.Debug().Msgf("resizing block to %d rows", block.capacity)

	//reallocate memory only if block dimensions have changed
	if oldCapacity != block.capacity {
		block.vin.Buf = make([]byte, 0, utils.MaxVinLength*block.capacity)
		block.vin.Pos = make([]proto.Position, 0, block.capacity)

		block.timestamp.Data = make([]proto.DateTime64, 0, block.capacity)
		block.timestamp.WithPrecision(proto.PrecisionMicro)

		block.id = make([]int16, 0, block.capacity)

		block.name.Buf = make([]byte, 0, 20*block.capacity)
		block.name.Pos = make([]proto.Position, 0, block.capacity)

		block.value = make([]float64, 0, block.capacity)
	}

	block.Flush(false)
}

func (block *protoSignalBlock) GetInsertBlockType() InsertBlockType {
	return SignalBlockType
}

func (block *protoSignalBlock) IsFull() bool {
	block.Lock()
	defer block.Unlock()
	return block.length >= block.capacity
}

// Given upper bound for bytes, estimate the maximum number of rows to allocate
func (block *protoSignalBlock) estimateMaxRows(maxBytes int) int {
	const rowBytes int = 17 + 8 + 2 + 30 + 8 + 2 + 2 //Each row is ~65 bytes.
	return maxBytes / rowBytes
}

// Clears and Resets the buffers in the Block.
func (block *protoSignalBlock) Flush(useLock bool) {
	if useLock {
		block.Lock()
		defer block.Unlock()
	}
	block.vin.Reset()
	block.timestamp.Reset()
	block.id.Reset()
	block.name.Reset()
	block.value.Reset()

	block.startTime = nil
	block.length = 0
}

// Appends a kafka_grpc.GRPC_CANSignal to this block as a row
func (block *protoSignalBlock) AppendRow(signalNames []string, row interface{}) error {
	block.Lock()
	defer block.Unlock()

	if block.length >= block.capacity {
		return errors.WithStack(utils.ErrInsertFullBlock)
	}
	signal, valid := row.(*kafka_grpc.GRPC_CANSignal)
	if !valid {
		return errors.WithStack(utils.ErrInvalidAppendType)

	}

	if block.startTime == nil {
		time := time.Now()
		block.startTime = &time
	}

	timestamp := utils.FloatToTime(signal.Timestamp)

	block.vin.Append(signal.Vin)
	block.timestamp.Append(timestamp)
	block.id.Append(int16(signal.Id))
	block.name.Append(signal.Name)
	block.value.Append(signal.Value)

	block.length++

	return nil
}

// Gets protocol input struct for ch-go batch insertion
func (block *protoSignalBlock) GetProtoInput(useLock bool, signalsSet map[string]bool) proto.Input {
	if useLock {
		block.Lock()
		defer block.Unlock()
	}

	if block.length == 0 {
		return nil
	}
	input := proto.Input{
		{Name: "VIN", Data: block.vin},
		{Name: "Timestamp", Data: block.timestamp},
		{Name: "Name", Data: block.name},
		{Name: "Value", Data: block.value},
		{Name: "ID", Data: block.id},
	}
	return input
}

/// helper methods ///

// Sets the value of string in-place in a proto colstr.
// This will reallocate memory as needed.
func SetColStr(col *proto.ColStr, value string, index int) {
	pos := col.Pos[index]
	oldLen := pos.End - pos.Start
	newLen := len(value)
	offset := newLen - oldLen

	newEnd := pos.Start + len(value)

	oldBufLen := len(col.Buf)

	//need to resize buffers
	if offset > 0 {
		// grow buffer by appending zero bytes, then truncating the length (not the cap) of slice
		col.Buf = append(col.Buf, make([]byte, offset)...)[:oldBufLen]
	}

	// need to shift EVERYTHING after index. This can be slow.
	if offset != 0 {
		// copy buffer into itself with offset.
		copy(col.Buf[newEnd:len(col.Buf)], col.Buf[pos.End:oldBufLen])

		for i := index + 1; i < len(col.Pos); i++ {
			col.Pos[i].Start += offset
			col.Pos[i].End += offset
		}
	}

	//insert
	copy(col.Buf[pos.Start:newEnd], value)
	col.Pos[index].End = newEnd

}
