<?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\NodeType;
use Neos\ContentRepository\Domain\Service\ContextFactoryInterface;
use Neos\ContentRepository\Domain\Service\NodeTypeManager;
use Neos\Eel\InterpretedEvaluator;
use Neos\Flow\Annotations as Flow;
use Neos\Neos\Domain\Service\ContentContext;
use Newland\Toubiz\Search\Neos\Utility\NodeTypeUtility;

/**
 * @Flow\Scope("singleton")
 */
abstract class AbstractNodeIndexer
{
    /**
     * Neos content dimension configuration.
     *
     * @Flow\InjectConfiguration(
     *   package="Neos.ContentRepository",
     *   path="contentDimensions"
     * )
     * @var array
     */
    protected $contentDimensions;

    /**
     * @var ContextFactoryInterface
     * @Flow\Inject()
     */
    protected $contextFactory;

    /**
     * @var InterpretedEvaluator
     * @Flow\Inject()
     */
    protected $eelEvaluator;

    /**
     * @var NodeTypeUtility
     * @Flow\Inject()
     */
    protected $nodeTypeUtility;

    /** @var array */
    protected $configuration;

    public function setConfiguration(array $configuration = []): void
    {
        $this->configuration = $configuration;
    }

    /**
     * Builds a context for querying with flow query.
     *
     * @param array $options
     * @return ContentContext
     */
    protected function getContext(array $options = []): ContentContext
    {
        /** @var ContentContext $context */
        $context = $this->contextFactory->create(
            array_merge(
                [
                    'workspaceName' => 'live',
                    'currentDateTime' => new \Neos\Flow\Utility\Now(),
                    'dimensions' => [],
                    'invisibleContentShown' => false,
                    'removedContentShown' => false,
                    'inaccessibleContentShown' => false,
                ],
                $options
            )
        );

        return $context;
    }

    /**
     * Checks if a given node should be indexed.
     *
     * @param Node $node
     * @return bool
     */
    protected function nodeShouldBeIndexed(Node $node): bool
    {
        if ($node->getProperty('excludeFromSearch')) {
            return false;
        }

        if ($node->isHidden()) {
            return false;
        }

        if (!$this->nodeTypeShouldBeIndexed($node)) {
            return false;
        }

        /** @var \DateTime|null $hiddenBefore */
        $hiddenBefore = $node->getHiddenBeforeDateTime();
        if ($hiddenBefore && $hiddenBefore > new \DateTime()) {
            return false;
        }
        /** @var \DateTime|null $hiddenAfter */
        $hiddenAfter = $node->getHiddenBeforeDateTime();
        if ($hiddenAfter && $hiddenAfter < new \DateTime()) {
            return false;
        }

        return true;
    }

    /**
     * Checks if a given node type should be indexed.
     *
     * @param Node $node
     * @return bool
     */
    protected function nodeTypeShouldBeIndexed(Node $node): bool
    {
        $nodeConfigurations = $this->nodeTypeUtility->sortAndFilterConfigurationByNodeType(
            $node->getNodeType(),
            $this->configuration['nodeTypes'] ?? []
        );
        foreach ($nodeConfigurations as $nodeType => $configuration) {
            if (array_key_exists('index', $configuration)) {
                return (bool) $configuration['index'];
            }
        }

        // Index in in case of no configuration telling the opposite.
        return true;
    }

    /**
     * Strips ignored strings from index.
     *
     * @param Node $node
     * @param string $value
     * @return string
     */
    protected function stripIgnoredStrings(Node $node, string $value): string
    {
        $nodeConfigurations = $this->nodeTypeUtility->sortAndFilterConfigurationByNodeType(
            $node->getNodeType(),
            $this->configuration['nodeTypes'] ?? []
        );
        foreach ($nodeConfigurations as $nodeType => $configuration) {
            if (!array_key_exists('ignoredStrings', $configuration)) {
                continue;
            }

            foreach ($configuration['ignoredStrings'] as $string) {
                $value = str_ireplace($string, '', $value);
            }
        }

        return $value;
    }

    protected function getTitleForNode(Node $node): ?string
    {
        return $this->renderEelExpressionInNodeTypeConfiguration($node, 'title');
    }

    protected function getDescriptionForNode(Node $node): ?string
    {
        return $this->renderEelExpressionInNodeTypeConfiguration($node, 'description');
    }

    protected function renderEelExpressionInNodeTypeConfiguration(Node $node, string $configurationName): ?string
    {
        $nodeConfigurations = $this->nodeTypeUtility->sortAndFilterConfigurationByNodeType(
            $node->getNodeType(),
            $this->configuration['nodeTypes'] ?? []
        );
        foreach ($nodeConfigurations as $configuration) {
            if (empty($configuration[$configurationName])) {
                continue;
            }

            $value = \Neos\Eel\Utility::evaluateEelExpression(
                $configuration[$configurationName],
                $this->eelEvaluator,
                ['node' => $node]
            );

            if (!empty($value)) {
                return (string) $value;
            }
        }

        return null;
    }


    /**
     * Gets the scope for the given node.
     *
     * @param Node $node
     * @return string
     */
    protected function getScopeForNode(Node $node): ?string
    {
        if (!preg_match('/^(\/sites\/.*?)\//', $node->getPath(), $matches)) {
            return null;
        }

        return $matches[1] ?? null;
    }

    /**
     * Gets the language dimension for this node.
     *
     * @param Node $node
     * @return string|null
     */
    protected function getLanguageForNode(Node $node): ?string
    {
        if (array_key_exists('language', $node->getDimensions())) {
            return $node->getDimensions()['language'][0];
        }
        return null;
    }

    /**
     * Returns configured content dimensions.
     *
     * @return array
     */
    protected function getContentDimensions(): array
    {
        return $this->contentDimensions;
    }
}
