package pub

import (
	"errors"
	"fmt"
	"golang.org/x/exp/slices"
	"servitor/client"
	"servitor/object"
	"net/url"
	"sync"
)

/*
	Methods are:
	Category
	Kind
	Identifier
	Next
	Size
	Items (returns list)
	String // maybe just show this page, and Next can be a button
		// the infiniscroll will be provided by the View package
*/

// Should probably take in a constructor, actor gives NewActivity
// and Post gives NewPost, but not exactly, they can wrap them
// in a function which also checks whether the element is
// valid in the given context

type Collection struct {
	kind string
	id   *url.URL

	elements    []any
	elementsErr error
	next        any
	nextErr     error

	size    uint64
	sizeErr error

	construct func(any, *url.URL,) Tangible
}

func NewCollection(input any, source *url.URL, construct func(any, *url.URL) Tangible) (*Collection, error) {
	o, id, err := client.FetchUnknown(input, source)
	if err != nil {
		return nil, err
	}
	return NewCollectionFromObject(o, id, construct)
}

func NewCollectionFromObject(o object.Object, id *url.URL, construct func(any, *url.URL) Tangible) (*Collection, error) {
	c := &Collection{}
	c.id = id
	var err error
	if c.kind, err = o.GetString("type"); err != nil {
		return nil, err
	}

	if !slices.Contains([]string{
		"Collection", "OrderedCollection", "CollectionPage", "OrderedCollectionPage",
	}, c.kind) {
		return nil, fmt.Errorf("%w: %s is not a Collection", ErrWrongType, c.kind)
	}

	c.construct = construct

	if c.kind == "Collection" || c.kind == "CollectionPage" {
		c.elements, c.elementsErr = o.GetList("items")
	} else {
		c.elements, c.elementsErr = o.GetList("orderedItems")
	}

	if c.kind == "Collection" || c.kind == "OrderedCollection" {
		c.next, c.nextErr = o.GetAny("first")
	} else {
		c.next, c.nextErr = o.GetAny("next")
	}

	c.size, c.sizeErr = o.GetNumber("totalItems")

	return c, nil
}

func (c *Collection) Size() (uint64, error) {
	return c.size, c.sizeErr
}

func (c *Collection) Harvest(amount uint, startingPoint uint) ([]Tangible, Container, uint) {
	return c.harvestWithEmptyCount(amount, startingPoint, 0)
}

func (c *Collection) harvestWithEmptyCount(amount uint, startingPoint uint, emptyCount int) ([]Tangible, Container, uint) {
	if c == nil {
		panic("can't harvest nil collection")
	}

	if c.elementsErr != nil && !errors.Is(c.elementsErr, object.ErrKeyNotPresent) {
		return []Tangible{NewFailure(c.elementsErr)}, nil, 0
	}

	var length uint
	if errors.Is(c.elementsErr, object.ErrKeyNotPresent) {
		length = 0
	} else {
		length = uint(len(c.elements))
	}

	if length == 0 {
		emptyCount += 1
	}

	/*
		This is set at 3 because 3 seems to be the maximum amount that servers send, besides cases of infinite loops.
		Mastodon sends 3 in the following case:
			- the first is the Collection itself, which has no items because the items are in CollectionPages
			- the next page (the first CollectionPage) is empty because it only holds self-replies and there are none
			- the next page (the second CollectionPage) is empty because it holds replies from others and there are none
	*/
	if emptyCount > 3 {
		return []Tangible{NewFailure(errors.New("refusing to read the next collection because >3 consecutive empty collections have been encountered"))}, nil, 0
	}

	var amountFromThisPage uint
	if startingPoint >= length {
		amountFromThisPage = 0
	} else if length > amount+startingPoint {
		amountFromThisPage = amount
	} else {
		amountFromThisPage = length - startingPoint
	}

	fromThisPage := make([]Tangible, amountFromThisPage)
	var fromLaterPages []Tangible
	var nextCollection Container
	var nextStartingPoint uint

	var wg sync.WaitGroup
	for i := uint(0); i < amountFromThisPage; i++ {
		i := i
		wg.Add(1)
		go func() {
			fromThisPage[i] = c.construct(c.elements[i+startingPoint], c.id)
			wg.Done()
		}()
	}

	wg.Add(1)
	go func() {
		if length > amount+startingPoint {
			fromLaterPages, nextCollection, nextStartingPoint = []Tangible{}, c, amount+startingPoint
		} else if errors.Is(c.nextErr, object.ErrKeyNotPresent) {
			fromLaterPages, nextCollection, nextStartingPoint = []Tangible{}, nil, 0
		} else if c.nextErr != nil {
			fromLaterPages, nextCollection, nextStartingPoint = []Tangible{NewFailure(c.nextErr)}, nil, 0
		} else if next, err := NewCollection(c.next, c.id, c.construct); err != nil {
			fromLaterPages, nextCollection, nextStartingPoint = []Tangible{NewFailure(err)}, nil, 0
		} else {
			fromLaterPages, nextCollection, nextStartingPoint = next.harvestWithEmptyCount(amount-amountFromThisPage, 0, emptyCount)
		}

		wg.Done()
	}()

	wg.Wait()

	return append(fromThisPage, fromLaterPages...), nextCollection, nextStartingPoint
}
