<?php
namespace Newland\NeosCommon\Domain\Repository;

use Doctrine\Common\Persistence\ObjectManager;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\QueryBuilder;
use Neos\ContentRepository\Domain\Factory\NodeFactory;
use Neos\ContentRepository\Domain\Model\Node;
use Neos\ContentRepository\Domain\Model\NodeData;
use Neos\ContentRepository\Domain\Model\NodeInterface;
use Neos\ContentRepository\Domain\Model\NodeType;
use Neos\ContentRepository\Domain\Service\NodeTypeManager;
use Neos\Flow\Annotations as Flow;
use Neos\Flow\ObjectManagement\Exception\InvalidObjectException;
use Neos\Neos\Domain\Model\Site;
use Neos\Neos\Domain\Repository\SiteRepository;
use Neos\Neos\Domain\Service\ContentContext;
use Neos\Neos\Domain\Service\ContentContextFactory;

/**
 * @Flow\Scope("singleton")
 *
 * @method Site|null findOneByNodeName(string $name)
 */
class NodeRepository
{

    /**
     * @Flow\Inject()
     * @var NodeRepository
     */
    protected $nodeRepository;

    /**
     * @Flow\Inject()
     * @var EntityManagerInterface
     */
    protected $entityManager;

    /**
     * @Flow\Inject()
     * @var NodeFactory
     */
    protected $nodeFactory;

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

    /**
     * @Flow\Inject()
     * @var SiteRepository
     */
    protected $siteRepository;

    /**
     * @var NodeTypeManager
     * @Flow\Inject()
     */
    protected $nodeTypeManager;

    /**
     * Fetches all nodes of the given type.
     * If the second argument is set to `true` then the site the node belongs to is manually
     * resolved by traversing the parents instead of relying on the built-in resolution of Neos.
     *
     * This is useful because Neos caches the first domain it resolves and assumes that a process
     * only lives as long as a request to a single domain. This assumption is false for CLI jobs
     * that process multiple nodes of different domains.
     *
     * @param string $nodeType
     * @param bool $manualSiteResolution
     * @return NodeInterface[]
     */
    public function findNodesByNodeType(string $nodeType, bool $manualSiteResolution = false): array
    {
        return $this->executeNodeDataQuery($this->queryForNodeType($nodeType), $manualSiteResolution);
    }


    public function findOneByNodeType(string $nodeType, bool $manualSiteResolution = false): ?NodeInterface
    {
        $result = $this->queryForNodeType($nodeType)
            ->setMaxResults(1)
            ->getQuery()
            ->execute();

        if (!is_array($result)|| \count($result) === 0) {
            return null;
        }

        return $this->nodeFactory->createFromNodeData(
            $result[0],
            $this->createContentContext($result[0], $manualSiteResolution)
        );
    }

    public function findOneByNodePath(string $path, bool $manualSiteResolution = false): ?NodeInterface
    {
        $queryBuilder = $this->entityManager->createQueryBuilder();
        $nodeData = $queryBuilder->select('data')
            ->from(NodeData::class, 'data')
            ->where($queryBuilder->expr()->eq('data.path', ':path'))
            ->setMaxResults(1)
            ->getQuery()
            ->execute([ 'path' => $path ])[0] ?? null;

        if (!$nodeData) {
            return null;
        }

        return $this->nodeFactory->createFromNodeData(
            $nodeData,
            $this->createContentContext($nodeData, $manualSiteResolution)
        );
    }

    public function queryForNodeType(string $nodeType, string $alias = 'n'): QueryBuilder
    {
        $types = array_merge(
            array_keys($this->nodeTypeManager->getSubNodeTypes($nodeType)),
            [ $nodeType ]
        );

        $queryBuilder = $this->entityManager->createQueryBuilder();
        $queryBuilder->select($alias)
            ->from(NodeData::class, $alias)
            ->where($queryBuilder->expr()->in($alias . '.nodeType', $types));

        return $queryBuilder;
    }

    /**
     * Executes a node data repository query and maps the results into nodes.
     * @return NodeInterface[]
     */
    public function executeNodeDataQuery(QueryBuilder $queryBuilder, bool $manualSiteResolution = false): array
    {
        return array_map(
            function (NodeData $data) use ($manualSiteResolution) {
                return $this->nodeFactory->createFromNodeData(
                    $data,
                    $this->createContentContext($data, $manualSiteResolution)
                );
            },
            $queryBuilder->getQuery()->execute()
        );
    }

    private function createContentContext(NodeData $data, bool $manualSiteResolution): ContentContext
    {
        $context = $this->contextFactory->create([
             'workspaceName' => $data->getWorkspace()->getName(),
             'dimensions' => $data->getDimensionValues(),
             'currentSite' => $manualSiteResolution ? $this->siteForNodeData($data) : null,
             'invisibleContentShown' => true,
             'inaccessibleContentShown' => true,
             'removedContentShown' => true,
         ]);

        if (!($context instanceof ContentContext)) {
            throw new InvalidObjectException('Context must be ContentContext');
        }

        return $context;
    }

    private function siteForNodeData(NodeData $data): Site
    {
        do {
            $site = $this->siteRepository->findOneByNodeName($data->getName());
            if ($site) {
                return $site;
            }

            /** @var NodeData|null $data */
            $data = $data->getParent();
        } while ($data);

        return $this->siteRepository->findDefault();
    }
}
