/*
Package nns provide some RPC wrappers for the non-native NNS contract.

It's not yet a complete interface because there are different NNS versions
available, yet it provides the most widely used ones that were available from
the old RPC client API.
*/
package nns

import (
	"fmt"

	"github.com/google/uuid"
	"github.com/nspcc-dev/neo-go/pkg/neorpc/result"
	"github.com/nspcc-dev/neo-go/pkg/rpcclient/nep11"
	"github.com/nspcc-dev/neo-go/pkg/rpcclient/unwrap"
	"github.com/nspcc-dev/neo-go/pkg/util"
	"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
)

// Invoker is used by ContractReader to call various methods.
type Invoker interface {
	nep11.Invoker
}

// ContractReader provides an interface to call read-only NNS contract methods.
type ContractReader struct {
	nep11.NonDivisibleReader

	invoker Invoker
	hash    util.Uint160
}

// RecordIterator is used for iterating over GetAllRecords results.
type RecordIterator struct {
	client   Invoker
	session  uuid.UUID
	iterator result.Iterator
}

// NewReader creates an instance of ContractReader that can be used to read
// data from the contract.
func NewReader(invoker Invoker, hash util.Uint160) *ContractReader {
	return &ContractReader{*nep11.NewNonDivisibleReader(invoker, hash), invoker, hash}
}

// GetPrice returns current domain registration price in GAS.
func (c *ContractReader) GetPrice() (int64, error) {
	return unwrap.Int64(c.invoker.Call(c.hash, "getPrice"))
}

// IsAvailable checks whether the domain given is available for registration.
func (c *ContractReader) IsAvailable(name string) (bool, error) {
	return unwrap.Bool(c.invoker.Call(c.hash, "isAvailable", name))
}

// Resolve resolves the given record type for the given domain (with no more
// than three redirects).
func (c *ContractReader) Resolve(name string, typ RecordType) (string, error) {
	return unwrap.UTF8String(c.invoker.Call(c.hash, "resolve", name, int64(typ)))
}

// GetAllRecords returns an iterator that allows to retrieve all RecordState
// items for the given domain name. It depends on the server to provide proper
// session-based iterator, but can also work with expanded one.
func (c *ContractReader) GetAllRecords(name string) (*RecordIterator, error) {
	sess, iter, err := unwrap.SessionIterator(c.invoker.Call(c.hash, "getAllRecords", name))
	if err != nil {
		return nil, err
	}

	return &RecordIterator{
		client:   c.invoker,
		iterator: iter,
		session:  sess,
	}, nil
}

// Next returns the next set of elements from the iterator (up to num of them).
// It can return less than num elements in case iterator doesn't have that many
// or zero elements if the iterator has no more elements or the session is
// expired.
func (r *RecordIterator) Next(num int) ([]RecordState, error) {
	items, err := r.client.TraverseIterator(r.session, &r.iterator, num)
	if err != nil {
		return nil, err
	}
	return itemsToRecords(items)
}

// Terminate closes the iterator session used by RecordIterator (if it's
// session-based).
func (r *RecordIterator) Terminate() error {
	if r.iterator.ID == nil {
		return nil
	}
	return r.client.TerminateSession(r.session)
}

// GetAllRecordsExpanded is similar to GetAllRecords (uses the same NNS
// method), but can be useful if the server used doesn't support sessions and
// doesn't expand iterators. It creates a script that will get num of result
// items from the iterator right in the VM and return them to you. It's only
// limited by VM stack and GAS available for RPC invocations.
func (c *ContractReader) GetAllRecordsExpanded(name string, num int) ([]RecordState, error) {
	arr, err := unwrap.Array(c.invoker.CallAndExpandIterator(c.hash, "getAllRecords", num, name))
	if err != nil {
		return nil, err
	}
	return itemsToRecords(arr)
}

func itemsToRecords(arr []stackitem.Item) ([]RecordState, error) {
	res := make([]RecordState, len(arr))
	for i := range arr {
		err := res[i].FromStackItem(arr[i])
		if err != nil {
			return nil, fmt.Errorf("item #%d: %w", i, err)
		}
	}
	return res, nil
}