// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package paginator

import (
	"fmt"

	"github.com/hashicorp/go-bexpr"
	"github.com/hashicorp/nomad/nomad/structs"
)

// Iterator is the interface that must be implemented to supply data to the
// Paginator.
type Iterator interface {
	// Next returns the next element to be considered for pagination.
	// The page will end if nil is returned.
	Next() interface{}
}

// Paginator wraps an iterator and returns only the expected number of pages.
type Paginator struct {
	iter           Iterator
	tokenizer      Tokenizer
	filters        []Filter
	perPage        int32
	itemCount      int32
	seekingToken   string
	nextToken      string
	reverse        bool
	nextTokenFound bool
	pageErr        error

	// appendFunc is the function the caller should use to append raw
	// entries to the results set. The object is guaranteed to be
	// non-nil.
	appendFunc func(interface{}) error
}

// NewPaginator returns a new Paginator. Any error creating the paginator is
// due to bad user filter input, RPC functions should therefore return a 400
// error code along with an appropriate message.
func NewPaginator(iter Iterator, tokenizer Tokenizer, filters []Filter,
	opts structs.QueryOptions, appendFunc func(interface{}) error) (*Paginator, error) {

	var evaluator *bexpr.Evaluator
	var err error

	if opts.Filter != "" {
		evaluator, err = bexpr.CreateEvaluator(opts.Filter)
		if err != nil {
			return nil, fmt.Errorf("failed to read filter expression: %v", err)
		}
		filters = append(filters, evaluator)
	}

	return &Paginator{
		iter:           iter,
		tokenizer:      tokenizer,
		filters:        filters,
		perPage:        opts.PerPage,
		seekingToken:   opts.NextToken,
		reverse:        opts.Reverse,
		nextTokenFound: opts.NextToken == "",
		appendFunc:     appendFunc,
	}, nil
}

// Page populates a page by running the append function
// over all results. Returns the next token.
func (p *Paginator) Page() (string, error) {
DONE:
	for {
		raw, andThen := p.next()
		switch andThen {
		case paginatorInclude:
			err := p.appendFunc(raw)
			if err != nil {
				p.pageErr = err
				break DONE
			}
		case paginatorSkip:
			continue
		case paginatorComplete:
			break DONE
		}
	}
	return p.nextToken, p.pageErr
}

func (p *Paginator) next() (interface{}, paginatorState) {
	raw := p.iter.Next()
	if raw == nil {
		p.nextToken = ""
		return nil, paginatorComplete
	}
	token := p.tokenizer.GetToken(raw)

	// have we found the token we're seeking (if any)?
	p.nextToken = token

	var passedToken bool

	if p.reverse {
		passedToken = token > p.seekingToken
	} else {
		passedToken = token < p.seekingToken
	}

	if !p.nextTokenFound && passedToken {
		return nil, paginatorSkip
	}

	// apply filters if defined
	for _, f := range p.filters {
		allow, err := f.Evaluate(raw)
		if err != nil {
			p.pageErr = err
			return nil, paginatorComplete
		}
		if !allow {
			return nil, paginatorSkip
		}
	}

	p.nextTokenFound = true

	// have we produced enough results for this page?
	p.itemCount++
	if p.perPage != 0 && p.itemCount > p.perPage {
		return raw, paginatorComplete
	}

	return raw, paginatorInclude
}

type paginatorState int

const (
	paginatorInclude paginatorState = iota
	paginatorSkip
	paginatorComplete
)
