<?php

/*
 * This file is part of the Neos.Neos package.
 *
 * (c) Contributors of the Neos Project - www.neos.io
 *
 * This package is Open Source Software. For the full copyright and license
 * information, please view the LICENSE file which was distributed with this
 * source code.
 */

declare(strict_types=1);

namespace Neos\ContentRepository\NodeAccess\FlowQueryOperations;

use Neos\ContentRepository\Core\Projection\ContentGraph\Node;
use Neos\Eel\FlowQuery\FlowQuery;
use Neos\Eel\FlowQuery\FlowQueryException;
use Neos\Eel\FlowQuery\Operations\AbstractOperation;

/**
 * "sort" operation working on ContentRepository nodes.
 * Sorts nodes by specified node properties.
 */
class SortOperation extends AbstractOperation
{
    /**
     * {@inheritdoc}
     *
     * @var string
     */
    protected static $shortName = 'sort';

    /**
     * {@inheritdoc}
     *
     * We can only handle ContentRepository Nodes.
     *
     * @param mixed $context
     * @return boolean
     */
    public function canEvaluate($context)
    {
        return count($context) === 0 || (is_array($context) === true && (current($context) instanceof Node));
    }

    /**
     * First argument is the node property to sort by.
     * Second argument is the sort direction (ASC or DESC).
     * Third optional argument are the sort options (see https://www.php.net/manual/en/function.sort):
     *  - 'SORT_REGULAR'
     *  - 'SORT_NUMERIC'
     *  - 'SORT_STRING'
     *  - 'SORT_LOCALE_STRING'
     *  - 'SORT_NATURAL'
     *  - 'SORT_FLAG_CASE' (use as last option with SORT_STRING, SORT_LOCALE_STRING or SORT_NATURAL)
     * A single sort option can be supplied as string. Multiple sort options are supplied as array.
     * Other than the above listed sort options throw an error.
     * Omitting the third parameter leaves FlowQuery sort() in SORT_REGULAR sort mode.
     * Example usages:
     *      sort("title", "ASC", ["SORT_NATURAL", "SORT_FLAG_CASE"])
     *      sort("risk", "DESC", "SORT_NUMERIC")
     *
     *
     * @param FlowQuery $flowQuery the FlowQuery object
     * @param array<int,mixed> $arguments the arguments for this operation.
     * @return void
     * @throws FlowQueryException
     */
    public function evaluate(FlowQuery $flowQuery, array $arguments)
    {
        /** @var array|Node[] $nodes */
        $nodes = $flowQuery->getContext();

        // Check sort property
        if (isset($arguments[0]) && !empty($arguments[0])) {
            $sortProperty = $arguments[0];
        } else {
            throw new FlowQueryException('Please provide a node property to sort by.', 1467881104);
        }

        // Check sort direction
        if (isset($arguments[1]) && !empty($arguments[1]) && in_array(strtoupper($arguments[1]), ['ASC', 'DESC'])) {
            $sortOrder = strtoupper($arguments[1]);
        } else {
            throw new FlowQueryException('Please provide a valid sort direction (ASC or DESC)', 1467881105);
        }

        // Check sort options
        $sortOptions = [];
        $availableSortingMethods
            = ['SORT_REGULAR', 'SORT_NUMERIC', 'SORT_STRING', 'SORT_LOCALE_STRING', 'SORT_NATURAL', 'SORT_FLAG_CASE'];
        if (isset($arguments[2]) && !empty($arguments[2])) {
            $args = is_array($arguments[2]) ? $arguments[2] : [$arguments[2]];
            foreach ($args as $arg) {
                if (!in_array(strtoupper($arg), $availableSortingMethods, true)) {
                    throw new FlowQueryException(
                        'Please provide a valid sort option (see https://www.php.net/manual/en/function.sort)',
                        1591107722
                    );
                } else {
                    $sortOptions[] = $arg;
                }
            }
        }

        $sortedNodes = [];
        $sortSequence = [];
        $nodesByIdentifier = [];

        // Determine the property value to sort by
        foreach ($nodes as $node) {
            $propertyValue = $node->getProperty($sortProperty);

            $sortSequence[$node->aggregateId->value] = $propertyValue;
            $nodesByIdentifier[$node->aggregateId->value] = $node;
        }

        // Create the sort sequence
        $sortFlags = (int)array_sum(array_map('constant', $sortOptions));
        $sortFlags = $sortFlags === 0 ? SORT_REGULAR : $sortFlags;
        if ($sortOrder === 'DESC') {
            arsort($sortSequence, $sortFlags);
        } elseif ($sortOrder === 'ASC') {
            asort($sortSequence, $sortFlags);
        }

        // Build the sorted context that is returned
        foreach ($sortSequence as $nodeIdentifier => $value) {
            $sortedNodes[] = $nodesByIdentifier[$nodeIdentifier];
        }

        $flowQuery->setContext($sortedNodes);
    }
}
