//go:build go1.18
// +build go1.18

/*
Until go version 2, we can't really apply the type alias feature on a generic type or function,
so keep it separated on x/pagination.

import "github.com/kataras/iris/v12/context"

type ListResponse[T any] = context.ListResponse[T]
OR
type ListResponse = context.ListResponse doesn't work.

The only workable thing for generic aliases is when you know the type e.g.
type ListResponse = context.ListResponse[any] but that doesn't fit us.
*/

package pagination

import (
	"math"
	"net/http"
	"strconv"
)

var (
	// MaxSize defines the max size of items to display.
	MaxSize = 100000
	// DefaultSize defines the default size when ListOptions.Size is zero.
	DefaultSize = MaxSize
)

// ListOptions is the list request object which should be provided by the client through
// URL Query. Then the server passes that options to a database query,
// including any custom filters may be given from the request body and,
// then the server responds back with a `Context.JSON(NewList(...))` response based
// on the database query's results.
type ListOptions struct {
	// Current page number.
	// If Page > 0 then:
	// Limit = DefaultLimit
	// Offset = DefaultLimit * Page
	// If Page == 0 then no actual data is return,
	// internally we must check for this value
	// because in postgres LIMIT 0 returns the columns but with an empty set.
	Page int `json:"page" url:"page"`
	// The elements to get, this modifies the LIMIT clause,
	// this Size can't be higher than the MaxSize.
	// If Size is zero then size is set to DefaultSize.
	Size int `json:"size" url:"size"`
}

// GetLimit returns the LIMIT value of a query.
func (opts ListOptions) GetLimit() int {
	if opts.Size > 0 && opts.Size < MaxSize {
		return opts.Size
	}

	return DefaultSize
}

// GetLimit returns the OFFSET value of a query.
func (opts ListOptions) GetOffset() int {
	if opts.Page > 1 {
		return (opts.Page - 1) * opts.GetLimit()
	}

	return 0
}

// GetCurrentPage returns the Page or 1.
func (opts ListOptions) GetCurrentPage() int {
	current := opts.Page
	if current == 0 {
		current = 1
	}

	return current
}

// GetNextPage returns the next page, current page + 1.
func (opts ListOptions) GetNextPage() int {
	return opts.GetCurrentPage() + 1
}

// Bind binds the ListOptions values to a request value.
// It should be used as an x/client.RequestOption to fire requests
// on a server that supports pagination.
func (opts ListOptions) Bind(r *http.Request) error {
	page := strconv.Itoa(opts.GetCurrentPage())
	size := strconv.Itoa(opts.GetLimit())

	q := r.URL.Query()
	q.Set("page", page)
	q.Set("size", size)
	return nil
}

// List is the http response of a server handler which should render
// items with pagination support.
type List[T any] struct {
	CurrentPage int   `json:"current_page"`  // the current page.
	PageSize    int   `json:"page_size"`     // the total amount of the entities return.
	TotalPages  int   `json:"total_pages"`   // the total number of pages based on page, size and total count.
	TotalItems  int64 `json:"total_items"`   // the total number of rows.
	HasNextPage bool  `json:"has_next_page"` // true if more data can be fetched, depending on the current page * page size and total pages.
	Filter      any   `json:"filter"`        // if any filter data.
	Items       []T   `json:"items"`         // Items is empty array if no objects returned. Do NOT modify from outside.
}

// NewList returns a new List response which holds
// the current page, page size, total pages, total items count, any custom filter
// and the items array.
//
// Example Code:
//
//	import "github.com/kataras/iris/v12/x/pagination"
//	...more code
//
//	type User struct {
//		Firstname string `json:"firstname"`
//		Lastname  string `json:"lastname"`
//	}
//
//	type ExtraUser struct {
//		User
//		ExtraData string
//	}
//
//	func main() {
//		users := []User{
//			{"Gerasimos", "Maropoulos"},
//			{"Efi", "Kwfidou"},
//		}
//
//		t := pagination.NewList(users, 100, nil, pagination.ListOptions{
//			Page: 1,
//			Size: 50,
//		})
//
//		// Optionally, transform a T list of objects to a V list of objects.
//		v, err := pagination.TransformList(t, func(u User) (ExtraUser, error) {
//			return ExtraUser{
//				User:      u,
//				ExtraData: "test extra data",
//			}, nil
//		})
//		if err != nil { panic(err) }
//
//		paginationJSON, err := json.MarshalIndent(v, "", "    ")
//		if err!=nil { panic(err) }
//		fmt.Println(paginationJSON)
//	}
func NewList[T any](items []T, totalCount int64, filter any, opts ListOptions) *List[T] {
	pageSize := opts.GetLimit()

	n := len(items)
	if n == 0 || pageSize <= 0 {
		return &List[T]{
			CurrentPage: 1,
			PageSize:    0,
			TotalItems:  0,
			TotalPages:  0,
			Filter:      filter,
			Items:       make([]T, 0),
		}
	}

	numberOfPages := int(roundUp(float64(totalCount)/float64(pageSize), 0))
	if numberOfPages <= 0 {
		numberOfPages = 1
	}

	var hasNextPage bool

	currentPage := opts.GetCurrentPage()
	if totalCount == 0 {
		currentPage = 1
	}

	if n > 0 {
		hasNextPage = currentPage < numberOfPages
	}

	return &List[T]{
		CurrentPage: currentPage,
		PageSize:    n,
		TotalPages:  numberOfPages,
		TotalItems:  totalCount,
		HasNextPage: hasNextPage,
		Filter:      filter,
		Items:       items,
	}
}

// TransformList accepts a List response and converts to a list of V items.
// T => from
// V => to
//
// Example Code:
//
//	listOfUsers := pagination.NewList(...)
//	newListOfExtraUsers, err := pagination.TransformList(listOfUsers, func(u User) (ExtraUser, error) {
//		return ExtraUser{
//			User:      u,
//			ExtraData: "test extra data",
//		}, nil
//	})
func TransformList[T any, V any](list *List[T], transform func(T) (V, error)) (*List[V], error) {
	if list == nil {
		return &List[V]{
			CurrentPage: 1,
			PageSize:    0,
			TotalItems:  0,
			TotalPages:  0,
			Filter:      nil,
			Items:       make([]V, 0),
		}, nil
	}

	items := list.Items

	toItems := make([]V, 0, len(items))
	for _, fromItem := range items {
		toItem, err := transform(fromItem)
		if err != nil {
			return nil, err
		}

		toItems = append(toItems, toItem)
	}

	newList := &List[V]{
		CurrentPage: list.CurrentPage,
		PageSize:    list.PageSize,
		TotalItems:  list.TotalItems,
		TotalPages:  list.TotalPages,
		Filter:      list.Filter,
		Items:       toItems,
	}
	return newList, nil
}

func roundUp(input float64, places float64) float64 {
	pow := math.Pow(10, places)
	return math.Ceil(pow*input) / pow
}