<?php

declare(strict_types=1);

namespace Grav\Plugin\FlexObjects\Table;

use Grav\Common\Debugger;
use Grav\Common\Grav;
use Grav\Framework\Collection\CollectionInterface;
use Grav\Framework\Flex\Interfaces\FlexAuthorizeInterface;
use Grav\Framework\Flex\Interfaces\FlexCollectionInterface;
use Grav\Framework\Flex\Interfaces\FlexObjectInterface;
use JsonSerializable;
use Throwable;
use Twig\Environment;
use Twig\Error\LoaderError;
use Twig\Error\RuntimeError;
use Twig\Error\SyntaxError;
use function is_array;
use function is_string;

/**
 * Class DataTable
 * @package Grav\Plugin\Gitea
 *
 * https://github.com/ratiw/vuetable-2/wiki/Data-Format-(JSON)
 * https://github.com/ratiw/vuetable-2/wiki/Sorting
 */
class DataTable implements JsonSerializable
{
    /** @var string */
    private $url;
    /** @var int */
    private $limit;
    /** @var int */
    private $page;
    /** @var array */
    private $sort;
    /** @var string */
    private $search;
    /** @var FlexCollectionInterface */
    private $collection;
    /** @var FlexCollectionInterface */
    private $filteredCollection;
    /** @var array */
    private $columns;
    /** @var Environment */
    private $twig;
    /** @var array */
    private $twig_context;

    /**
     * DataTable constructor.
     * @param array $params
     */
    public function __construct(array $params)
    {
        $this->setUrl($params['url'] ?? '');
        $this->setLimit((int)($params['limit'] ?? 10));
        $this->setPage((int)($params['page'] ?? 1));
        $this->setSort($params['sort'] ?? ['id' => 'asc']);
        $this->setSearch($params['search'] ?? '');
    }

    /**
     * @param string $url
     * @return void
     */
    public function setUrl(string $url): void
    {
        $this->url = $url;
    }

    /**
     * @param int $limit
     * @return void
     */
    public function setLimit(int $limit): void
    {
        $this->limit = max(1, $limit);
    }

    /**
     * @param int $page
     * @return void
     */
    public function setPage(int $page): void
    {
        $this->page = max(1, $page);
    }

    /**
     * @param string|string[] $sort
     * @return void
     */
    public function setSort($sort): void
    {
        if (is_string($sort)) {
            $sort = $this->decodeSort($sort);
        } elseif (!is_array($sort)) {
            $sort = [];
        }

        $this->sort = $sort;
    }

    /**
     * @param string $search
     * @return void
     */
    public function setSearch(string $search): void
    {
        $this->search = $search;
    }

    /**
     * @param CollectionInterface $collection
     * @return void
     */
    public function setCollection(CollectionInterface $collection): void
    {
        $this->collection = $collection;
        $this->filteredCollection = null;
    }

    /**
     * @return int
     */
    public function getLimit(): int
    {
        return $this->limit;
    }

    /**
     * @return int
     */
    public function getPage(): int
    {
        return $this->page;
    }

    /**
     * @return int
     */
    public function getLastPage(): int
    {
        return 1 + (int)floor(max(0, $this->getTotal()-1) / $this->getLimit());
    }

    /**
     * @return int
     */
    public function getTotal(): int
    {
        $collection = $this->filteredCollection ?? $this->getCollection();

        return $collection ? $collection->count() : 0;
    }

    /**
     * @return array
     */
    public function getSort(): array
    {
        return $this->sort;
    }

    /**
     * @return FlexCollectionInterface|null
     */
    public function getCollection(): ?FlexCollectionInterface
    {
        return $this->collection;
    }

    /**
     * @param int $page
     * @return string|null
     */
    public function getUrl(int $page): ?string
    {
        if ($page < 1 || $page > $this->getLastPage()) {
            return null;
        }

        return "{$this->url}.json?page={$page}&per_page={$this->getLimit()}&sort={$this->encodeSort()}";
    }

    /**
     * @return array
     */
    public function getColumns(): array
    {
        if (null === $this->columns) {
            $collection = $this->getCollection();
            if (!$collection) {
                return [];
            }

            $blueprint = $collection->getFlexDirectory()->getBlueprint();
            $schema = $blueprint->schema();
            $columns = $blueprint->get('config/admin/views/list/fields') ?? $blueprint->get('config/admin/list/fields', []);

            $list = [];
            foreach ($columns as $key => $options) {
                if (!isset($options['field'])) {
                    $options['field'] = $schema->get($options['alias'] ?? $key);
                }
                if (!$options['field'] || !empty($options['field']['ignore'])) {
                    continue;
                }
                $list[$key] = $options;
            }

            $this->columns = $list;
        }

        return $this->columns;
    }

    /**
     * @return array
     */
    public function getData(): array
    {
        $grav = Grav::instance();

        /** @var Debugger $debugger */
        $debugger = $grav['debugger'];
        $debugger->startTimer('datatable', 'Data Table');

        $collection = $this->getCollection();
        if (!$collection) {
            return [];
        }
        if ($this->search !== '') {
            $collection = $collection->search($this->search);
        }

        $columns = $this->getColumns();

        $collection = $collection->sort($this->getSort());

        $this->filteredCollection = $collection;

        $limit = $this->getLimit();
        $page = $this->getPage();
        $to = $page * $limit;
        $from = $to - $limit + 1;

        if ($from < 1 || $from > $this->getTotal()) {
            $debugger->stopTimer('datatable');
            return [];
        }

        $array = $collection->slice($from-1, $limit);

        $twig = $grav['twig'];
        $grav->fireEvent('onTwigSiteVariables');

        $this->twig = $twig->twig;
        $this->twig_context = $twig->twig_vars;

        $list = [];
        /** @var FlexObjectInterface $object */
        foreach ($array as $object) {
            $item = [
                'id' => $object->getKey(),
                'timestamp' => $object->getTimestamp()
            ];
            foreach ($columns as $name => $column) {
                $item[str_replace('.', '_', $name)] = $this->renderColumn($name, $column, $object);
            }
            $item['_actions_'] = $this->renderActions($object);

            $list[] = $item;
        }

        $debugger->stopTimer('datatable');

        return $list;
    }

    /**
     * @return array
     */
    public function jsonSerialize(): array
    {
        $data = $this->getData();
        $total = $this->getTotal();
        $limit = $this->getLimit();
        $page = $this->getPage();
        $to = $page * $limit;
        $from = $to - $limit + 1;

        $empty = empty($data);

        return [
            'links' => [
                'pagination' => [
                    'total' => $total,
                    'per_page' => $limit,
                    'current_page' => $page,
                    'last_page' => $this->getLastPage(),
                    'next_page_url' => $this->getUrl($page+1),
                    'prev_page_url' => $this->getUrl($page-1),
                    'from' => $empty ? null : $from,
                    'to' => $empty ? null : min($to, $total),
                ]
            ],
            'data' => $data
        ];
    }

    /**
     * @param string $name
     * @param array $column
     * @param FlexObjectInterface $object
     * @return false|string
     * @throws Throwable
     * @throws LoaderError
     * @throws RuntimeError
     * @throws SyntaxError
     */
    protected function renderColumn(string $name, array $column, FlexObjectInterface $object)
    {
        $grav = Grav::instance();
        $flex = $grav['flex_objects'];

        $value = $object->getFormValue($name) ?? $object->getNestedProperty($name, $column['field']['default'] ?? null);
        $type = $column['field']['type'] ?? 'text';
        $hasLink = $column['link'] ?? null;
        $link = null;

        $authorized = $object instanceof FlexAuthorizeInterface
            ? ($object->isAuthorized('read') || $object->isAuthorized('update')) : true;

        if ($hasLink && $authorized) {
            $route = $grav['route']->withExtension('');
            $link = $route->withAddedPath($object->getKey())->withoutParams()->getUri();
        }

        $template = $this->twig->resolveTemplate(["forms/fields/{$type}/edit_list.html.twig", 'forms/fields/text/edit_list.html.twig']);

        return $this->twig->load($template)->render([
            'value' => $value,
            'link' => $link,
            'field' => $column['field'],
            'object' => $object,
            'flex' => $flex,
            'route' => $grav['route']->withExtension('')
        ] + $this->twig_context);
    }

    /**
     * @param FlexObjectInterface $object
     * @return false|string
     * @throws Throwable
     * @throws LoaderError
     * @throws RuntimeError
     * @throws SyntaxError
     */
    protected function renderActions(FlexObjectInterface $object)
    {
        $grav = Grav::instance();
        $type = $object->getFlexType();
        $template = $this->twig->resolveTemplate(["flex-objects/types/{$type}/list/list_actions.html.twig", 'flex-objects/types/default/list/list_actions.html.twig']);

        return $this->twig->load($template)->render([
            'object' => $object,
            'flex' => $grav['flex_objects'],
            'route' => $grav['route']->withExtension('')
        ] + $this->twig_context);
    }

    /**
     * @param string $sort
     * @param string $fieldSeparator
     * @param string $orderSeparator
     * @return array
     */
    protected function decodeSort(string $sort, string $fieldSeparator = ',', string $orderSeparator = '|'): array
    {
        $strings = explode($fieldSeparator, $sort);
        $list = [];
        foreach ($strings as $string) {
            $item = explode($orderSeparator, $string, 2);
            $key = array_shift($item);
            $order = array_shift($item) === 'desc' ? 'desc' : 'asc';
            $list[$key] = $order;
        }

        return $list;
    }

    /**
     * @param string $fieldSeparator
     * @param string $orderSeparator
     * @return string
     */
    protected function encodeSort(string $fieldSeparator = ',', string $orderSeparator = '|'): string
    {
        $list = [];
        foreach ($this->getSort() as $key => $order) {
            $list[] = $key . $orderSeparator . ($order ?: 'asc');
        }

        return implode($fieldSeparator, $list);
    }
}