<?php
/**
 * Base Paginator Class
 * 
 * @package ZenoChatbot\Framework\Database
 */

namespace ZenoChatbot\Framework\Database;

use ZenoChatbot\Framework\Support\Collection;

// Prevent direct access
if (!defined('ABSPATH')) {
    exit;
}

/**
 * Abstract Base Paginator Class
 */
abstract class Paginator
{
    /**
     * The items being paginated
     *
     * @var Collection
     */
    protected $items;

    /**
     * The number of items to be shown per page
     *
     * @var int
     */
    protected $perPage;

    /**
     * The current page being viewed
     *
     * @var int
     */
    protected $currentPage;

    /**
     * The path to assign to all URLs
     *
     * @var string
     */
    protected $path;

    /**
     * The query parameters to add to all URLs
     *
     * @var array
     */
    protected $query = [];

    /**
     * The URL fragment to add to all URLs
     *
     * @var string|null
     */
    protected $fragment;

    /**
     * The paginator options
     *
     * @var array
     */
    protected $options;

    /**
     * Create a new paginator instance
     *
     * @param mixed $items
     * @param int $perPage
     * @param int|null $currentPage
     * @param array $options
     */
    public function __construct($items, $perPage, $currentPage = null, array $options = [])
    {
        $this->items = $items instanceof Collection ? $items : new Collection($items);
        $this->perPage = $perPage;
        $this->currentPage = $this->setCurrentPage($currentPage);
        $this->path = $this->resolveCurrentPath();
        $this->options = $options;

        foreach ($options as $key => $value) {
            if (property_exists($this, $key)) {
                $this->{$key} = $value;
            }
        }
    }

    /**
     * Get the current page for the request
     *
     * @param int|null $currentPage
     * @return int
     */
    protected function setCurrentPage($currentPage)
    {
        $currentPage = $currentPage ?: static::resolveCurrentPage();
        
        return $this->isValidPageNumber($currentPage) ? (int) $currentPage : 1;
    }

    /**
     * Resolve the current page or return the default value
     *
     * @param string $pageName
     * @return int
     */
    public static function resolveCurrentPage($pageName = 'page')
    {
        if (isset($_GET[$pageName])) {
            $page = (int) $_GET[$pageName];
            return $page > 0 ? $page : 1;
        }
        
        return 1;
    }

    /**
     * Determine if the given value is a valid page number
     *
     * @param int $page
     * @return bool
     */
    protected function isValidPageNumber($page)
    {
        return $page >= 1 && filter_var($page, FILTER_VALIDATE_INT) !== false;
    }

    /**
     * Get the URL for a given page number
     *
     * @param int $page
     * @return string
     */
    public function url($page)
    {
        if ($page <= 0) {
            $page = 1;
        }

        $parameters = [$this->pageName() => $page];

        if (count($this->query) > 0) {
            $parameters = array_merge($this->query, $parameters);
        }

        $url = $this->path . '?' . http_build_query($parameters, '', '&');

        return $url . $this->buildFragment();
    }

    /**
     * Get the URL for the previous page
     *
     * @return string|null
     */
    public function previousPageUrl()
    {
        if ($this->currentPage() > 1) {
            return $this->url($this->currentPage() - 1);
        }

        return null;
    }

    /**
     * Get the URL for the next page
     *
     * @return string|null
     */
    abstract public function nextPageUrl();

    /**
     * Get all of the items being paginated
     *
     * @return Collection
     */
    public function items()
    {
        return $this->items;
    }

    /**
     * Get the "index" of the first item being paginated
     *
     * @return int
     */
    public function firstItem()
    {
        return count($this->items) > 0 ? ($this->currentPage - 1) * $this->perPage + 1 : null;
    }

    /**
     * Get the "index" of the last item being paginated
     *
     * @return int
     */
    public function lastItem()
    {
        return count($this->items) > 0 ? $this->firstItem() + count($this->items) - 1 : null;
    }

    /**
     * Get the number of items shown per page
     *
     * @return int
     */
    public function perPage()
    {
        return $this->perPage;
    }

    /**
     * Get the current page
     *
     * @return int
     */
    public function currentPage()
    {
        return $this->currentPage;
    }

    /**
     * Determine if there are enough items to split into multiple pages
     *
     * @return bool
     */
    public function hasPages()
    {
        return $this->currentPage() != 1 || $this->hasMorePages();
    }

    /**
     * Determine if there are more items in the data source
     *
     * @return bool
     */
    abstract public function hasMorePages();

    /**
     * Get the instance as an array
     *
     * @return array
     */
    public function toArray()
    {
        return [
            'current_page' => $this->currentPage(),
            'data' => $this->items->toArray(),
            'first_page_url' => $this->url(1),
            'from' => $this->firstItem(),
            'next_page_url' => $this->nextPageUrl(),
            'path' => $this->path(),
            'per_page' => $this->perPage(),
            'prev_page_url' => $this->previousPageUrl(),
            'to' => $this->lastItem(),
        ];
    }

    /**
     * Convert the object to its JSON representation
     *
     * @param int $options
     * @return string
     */
    public function toJson($options = 0)
    {
        return json_encode($this->toArray(), $options);
    }

    /**
     * Get the paginator's underlying collection
     *
     * @return Collection
     */
    public function getCollection()
    {
        return $this->items;
    }

    /**
     * Set the paginator's underlying collection
     *
     * @param Collection $collection
     * @return $this
     */
    public function setCollection(Collection $collection)
    {
        $this->items = $collection;

        return $this;
    }

    /**
     * Get the query string variable used to store the page
     *
     * @return string
     */
    public function getPageName()
    {
        return $this->pageName();
    }

    /**
     * Get the query string variable used to store the page
     *
     * @return string
     */
    public function pageName()
    {
        return $this->options['pageName'] ?? 'page';
    }

    /**
     * Set the query string variable used to store the page
     *
     * @param string $name
     * @return $this
     */
    public function setPageName($name)
    {
        $this->options['pageName'] = $name;

        return $this;
    }

    /**
     * Set the base path to assign to all URLs
     *
     * @param string $path
     * @return $this
     */
    public function withPath($path)
    {
        return $this->setPath($path);
    }

    /**
     * Set the base path to assign to all URLs
     *
     * @param string $path
     * @return $this
     */
    public function setPath($path)
    {
        $this->path = $path;

        return $this;
    }

    /**
     * Get the base path for paginator generated URLs
     *
     * @return string|null
     */
    public function path()
    {
        return $this->path;
    }

    /**
     * Set the query parameters to add to all URLs
     *
     * @param array|string $key
     * @param string|null $value
     * @return $this
     */
    public function appends($key, $value = null)
    {
        if (is_null($key)) {
            return $this;
        }

        if (is_array($key)) {
            return $this->appendArray($key);
        }

        return $this->addQuery($key, $value);
    }

    /**
     * Add an array of query string values
     *
     * @param array $keys
     * @return $this
     */
    protected function appendArray(array $keys)
    {
        foreach ($keys as $key => $value) {
            $this->addQuery($key, $value);
        }

        return $this;
    }

    /**
     * Add a query string value to the paginator
     *
     * @param string $key
     * @param string $value
     * @return $this
     */
    protected function addQuery($key, $value)
    {
        if ($key !== $this->pageName()) {
            $this->query[$key] = $value;
        }

        return $this;
    }

    /**
     * Build the full fragment portion of a URL
     *
     * @return string
     */
    protected function buildFragment()
    {
        return $this->fragment ? '#' . $this->fragment : '';
    }

    /**
     * Add a fragment identifier to the paginator
     *
     * @param string|null $fragment
     * @return $this
     */
    public function fragment($fragment = null)
    {
        $this->fragment = $fragment;

        return $this;
    }

    /**
     * Resolve the current request path or return the default value
     *
     * @return string
     */
    public static function resolveCurrentPath()
    {
        return $_SERVER['REQUEST_URI'] ?? '/';
    }

    /**
     * Get an iterator for the items
     *
     * @return \ArrayIterator
     */
    public function getIterator()
    {
        return $this->items->getIterator();
    }

    /**
     * Determine if the list of items is empty
     *
     * @return bool
     */
    public function isEmpty()
    {
        return $this->items->isEmpty();
    }

    /**
     * Determine if the list of items is not empty
     *
     * @return bool
     */
    public function isNotEmpty()
    {
        return $this->items->isNotEmpty();
    }

    /**
     * Get the number of items for the current page
     *
     * @return int
     */
    public function count()
    {
        return $this->items->count();
    }

    /**
     * Convert the paginator to string
     *
     * @return string
     */
    public function __toString()
    {
        return $this->toJson();
    }
}
