<?php declare(strict_types=1);
namespace Newland\Toubiz\Search\Neos\Indexer;

/*
 * This file is part of the "toubiz-search-neos" package.
 *
 * For the full copyright and license information, please read the
 * LICENSE.txt file that was distributed with this source code.
 */

use Neos\ContentRepository\Domain\Model\Node;
use Neos\ContentRepository\Domain\Model\NodeInterface;
use Neos\Eel\FlowQuery\FlowQuery;
use Neos\Flow\Annotations as Flow;
use Newland\Contracts\Neos\Search\IndexerInterface;
use Newland\Contracts\Neos\Search\SearchBackend;
use Newland\Contracts\Neos\Search\IndexRecordModification;
use Newland\Contracts\Neos\Search\ProgressHandler;

/**
 * Document node indexer.
 *
 * Indexes document nodes by building an index entry for the document node
 * with all its child nodes.
 *
 * @Flow\Scope("singleton")
 */
class DocumentNodeIndexer extends AbstractNodeIndexer implements IndexerInterface
{
    public const TYPE = 'page';

    /** @var int */
    private $total = 0;

    /** @var string[] */
    protected $indexed = [];

    public function index(array $scopesToIndex, SearchBackend  $backend, ProgressHandler $progressHandler): void
    {
        foreach ($scopesToIndex as $scope) {
            $progressTitle = sprintf('%s [%s]', static::TYPE, $scope);
            foreach ($this->extractDimensionSearchSets() as $searchSet) {
                foreach ($this->findNodes($searchSet, $scope) as $modification) {
                    $backend->createOrUpdateIndexEntry($modification);
                    $this->indexed[] = $modification->getIdentifier();
                    $progressHandler->informProgress($progressTitle, \count($this->indexed), $this->total);
                }
            }
        }
    }

    public function postIndex(SearchBackend $backend): void
    {
        $backend->deleteObsoleteIndexEntries($this->indexed);
    }

    protected function findNodes(array $dimensions, string $scope): \Generator
    {
        $context = $this->getContext(['dimensions' => $dimensions]);

        /** @var NodeInterface|null $siteNode */
        $siteNode = $context->getNode($scope);
        if ($siteNode === null) {
            return;
        }

        $documentNodes = (new FlowQuery([ $siteNode ]))->find('[instanceof Neos.Neos:Document]');
        $this->total += \count($documentNodes);
        foreach ($documentNodes as $node) {
            /** @var Node $node */
            $data = $this->indexNode($node) ?: '';
            $description = $this->getDescriptionForNode($node) ? $this->getDescriptionForNode($node) : '';

            if ($data !== null && $data !== '') {
                yield new IndexRecordModification(
                    $node->getIdentifier(),
                    static::TYPE,
                    (string) $this->getTitleForNode($node),
                    (string) $description,
                    $data,
                    [ $scope ],
                    $this->getLanguageForNode($node)
                );
            }
        }
    }

    protected function indexNode(Node $node): ?string
    {
        if (!$this->nodeShouldBeIndexed($node)) {
            return null;
        }

        $indexData = [];
        $this->buildIndexDataForNode($node, $indexData);

        $childNodes = (new FlowQuery([$node]))->findInPage('[instanceof Neos.Neos:Content]');
        foreach ($childNodes as $childNode) {
            /** @var Node $childNode */
            $this->buildIndexDataForNode($childNode, $indexData);
        }

        return trim(implode(' ', $indexData));
    }


    /**
     * Build index data for given node.
     *
     * @param Node $node
     * @param array $indexData
     * @return void
     */
    protected function buildIndexDataForNode(Node $node, array &$indexData): void
    {
        if (!$this->nodeShouldBeIndexed($node)) {
            return;
        }

        foreach ($node->getProperties() as $name => $value) {
            if (!$this->propertyShouldBeIndexed($node, $name, $value)) {
                continue;
            }

            $value = $this->stripIgnoredStrings($node, $value);
            $indexData[] = $value;
        }
    }

    /**
     * Checks if a property should be indexed.
     *
     * @param Node $node The node object
     * @param string $name Property name
     * @param mixed $value Property value
     * @return bool
     */
    protected function propertyShouldBeIndexed(Node $node, $name, $value): bool
    {
        if ($value === null) {
            return false;
        }

        if (\is_bool($value)) {
            // Boolean values do not provide any information.
            return false;
        }

        if (\is_numeric($value)) {
            // Simple numeric values do not provide useful information.
            return false;
        }

        if (\is_object($value)) {
            // Skip objects (like images, references, etc).
            return false;
        }

        if (\is_array($value)) {
            // Skip arrays as they contain mostly internal data.
            return false;
        }

        if (\is_string($value) && preg_match('/^(node:\/\/)?[\w]{8}-[\w]{4}-[\w]{4}-[\w]{4}-[\w]{12}$/i', $value)) {
            // Skip persistence object identifiers;
            return false;
        }

        // Checking given node agains node configurations.
        foreach ($this->configuration['nodeTypes'] ?? [] as $nodeType => $configuration) {
            if (!array_key_exists('ignoredProperties', $configuration)) {
                continue;
            }

            if ($node->getNodeType()->isOfType($nodeType)
                && \in_array($name, $configuration['ignoredProperties'], true)
            ) {
                // Ignored properties should not be indexed.
                return false;
            }
        }

        return true;
    }

    private function extractDimensionSearchSets(): \Generator
    {
        $dimensions = $this->getContentDimensions();

        // Search for everything if no dimensions defined
        if (empty($dimensions)) {
            yield [];
            return;
        }

        foreach ($dimensions as $dimensionName => $dimension) {
            foreach ($dimension['presets'] as $name => $preset) {
                yield [ $dimensionName => $preset['values'] ];
            }
        }
    }
}
