package iterator

import (
	"sync"

	"go.uber.org/atomic"

	"github.com/milvus-io/milvus-proto/go-api/v2/schemapb"
	"github.com/milvus-io/milvus/internal/storage"
	"github.com/milvus-io/milvus/pkg/common"
	"github.com/milvus-io/milvus/pkg/util/typeutil"
)

type BinlogIterator struct {
	disposed     atomic.Bool
	disposedCh   chan struct{}
	disposedOnce sync.Once

	data      *storage.InsertData
	label     *Label
	pkFieldID int64
	pkType    schemapb.DataType
	pos       int
}

var _ Iterator = (*BinlogIterator)(nil)

// NewInsertBinlogIterator creates a new iterator
func NewInsertBinlogIterator(v [][]byte, pkFieldID typeutil.UniqueID, pkType schemapb.DataType, label *Label) (*BinlogIterator, error) {
	blobs := make([]*storage.Blob, len(v))
	for i := range blobs {
		blobs[i] = &storage.Blob{Value: v[i]}
	}

	reader := storage.NewInsertCodec()
	_, _, iData, err := reader.Deserialize(blobs)
	if err != nil {
		return nil, err
	}

	return &BinlogIterator{
		disposedCh: make(chan struct{}),
		data:       iData,
		pkFieldID:  pkFieldID,
		pkType:     pkType,
		label:      label,
	}, nil
}

// HasNext returns true if the iterator have unread record
func (i *BinlogIterator) HasNext() bool {
	return !i.isDisposed() && i.hasNext()
}

func (i *BinlogIterator) Next() (*LabeledRowData, error) {
	if i.isDisposed() {
		return nil, ErrDisposed
	}

	if !i.hasNext() {
		return nil, ErrNoMoreRecord
	}

	fields := make(map[int64]interface{})
	for fieldID, fieldData := range i.data.Data {
		fields[fieldID] = fieldData.GetRow(i.pos)
	}

	pk, err := storage.GenPrimaryKeyByRawData(i.data.Data[i.pkFieldID].GetRow(i.pos), i.pkType)
	if err != nil {
		return nil, err
	}

	row := &InsertRow{
		ID:        i.data.Data[common.RowIDField].GetRow(i.pos).(int64),
		Timestamp: uint64(i.data.Data[common.TimeStampField].GetRow(i.pos).(int64)),
		Pk:        pk,
		Value:     fields,
	}
	i.pos++
	return NewLabeledRowData(row, i.label), nil
}

// Dispose disposes the iterator
func (i *BinlogIterator) Dispose() {
	i.disposed.CompareAndSwap(false, true)
	i.disposedOnce.Do(func() {
		close(i.disposedCh)
	})
}

func (i *BinlogIterator) hasNext() bool {
	return i.pos < i.data.GetRowNum()
}

func (i *BinlogIterator) isDisposed() bool {
	return i.disposed.Load()
}

// Disposed wait forever for the iterator to dispose
func (i *BinlogIterator) WaitForDisposed() {
	<-i.disposedCh
}
