package engine

import (
	"sort"

	"github.com/nspcc-dev/neofs-node/pkg/local_object_storage/shard"
	addressSDK "github.com/nspcc-dev/neofs-sdk-go/object/address"
)

// ErrEndOfListing is returned from an object listing with cursor
// when the storage can't return any more objects after the provided
// cursor. Use nil cursor object to start listing again.
var ErrEndOfListing = shard.ErrEndOfListing

// Cursor is a type for continuous object listing.
type Cursor struct {
	shardID     string
	shardCursor *shard.Cursor
}

// ListWithCursorPrm contains parameters for ListWithCursor operation.
type ListWithCursorPrm struct {
	count  uint32
	cursor *Cursor
}

// WithCount sets the maximum amount of addresses that ListWithCursor should return.
func (p *ListWithCursorPrm) WithCount(count uint32) *ListWithCursorPrm {
	p.count = count
	return p
}

// WithCursor sets a cursor for ListWithCursor operation. For initial request
// ignore this param or use nil value. For consecutive requests, use value
// from ListWithCursorRes.
func (p *ListWithCursorPrm) WithCursor(cursor *Cursor) *ListWithCursorPrm {
	p.cursor = cursor
	return p
}

// ListWithCursorRes contains values returned from ListWithCursor operation.
type ListWithCursorRes struct {
	addrList []*addressSDK.Address
	cursor   *Cursor
}

// AddressList returns addresses selected by ListWithCursor operation.
func (l ListWithCursorRes) AddressList() []*addressSDK.Address {
	return l.addrList
}

// Cursor returns cursor for consecutive listing requests.
func (l ListWithCursorRes) Cursor() *Cursor {
	return l.cursor
}

// ListWithCursor lists physical objects available in the engine starting
// from the cursor. It includes regular, tombstone and storage group objects.
// Does not include inhumed objects. Use cursor value from the response
// for consecutive requests.
//
// Returns ErrEndOfListing if there are no more objects to return or count
// parameter set to zero.
func (e *StorageEngine) ListWithCursor(prm *ListWithCursorPrm) (*ListWithCursorRes, error) {
	result := make([]*addressSDK.Address, 0, prm.count)

	// 1. Get available shards and sort them.
	e.mtx.RLock()
	shardIDs := make([]string, 0, len(e.shards))
	for id := range e.shards {
		shardIDs = append(shardIDs, id)
	}
	e.mtx.RUnlock()

	if len(shardIDs) == 0 {
		return nil, ErrEndOfListing
	}

	sort.Slice(shardIDs, func(i, j int) bool {
		return shardIDs[i] < shardIDs[j]
	})

	// 2. Prepare cursor object.
	cursor := prm.cursor
	if cursor == nil {
		cursor = &Cursor{shardID: shardIDs[0]}
	}

	// 3. Iterate over available shards. Skip unavailable shards.
	for i := range shardIDs {
		if len(result) >= int(prm.count) {
			break
		}

		if shardIDs[i] < cursor.shardID {
			continue
		}

		e.mtx.RLock()
		shardInstance, ok := e.shards[shardIDs[i]]
		e.mtx.RUnlock()
		if !ok {
			continue
		}

		count := uint32(int(prm.count) - len(result))
		shardPrm := new(shard.ListWithCursorPrm).WithCount(count)
		if shardIDs[i] == cursor.shardID {
			shardPrm.WithCursor(cursor.shardCursor)
		}

		res, err := shardInstance.ListWithCursor(shardPrm)
		if err != nil {
			continue
		}

		result = append(result, res.AddressList()...)
		cursor.shardCursor = res.Cursor()
		cursor.shardID = shardIDs[i]
	}

	if len(result) == 0 {
		return nil, ErrEndOfListing
	}

	return &ListWithCursorRes{
		addrList: result,
		cursor:   cursor,
	}, nil
}