<?php
namespace Newland\NeosCommon\Service;

/*
 * This file is part of the "neos-common" 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\NodeDimension;
use Neos\ContentRepository\Domain\Model\NodeInterface;
use Neos\Eel\FlowQuery\FlowQuery;
use Neos\Flow\Annotations as Flow;
use Doctrine\ORM\EntityManagerInterface;
use Neos\ContentRepository\Domain\Model\Node;
use Neos\ContentRepository\Domain\Model\NodeData;
use Neos\Neos\Controller\CreateContentContextTrait;
use Neos\Neos\Domain\Model\Site;
use Neos\Neos\Domain\Service\ContentContext;
use Newland\NeosCommon\Exceptions\UnknownSiteException;

/**
 * @Flow\Scope("singleton")
 */
class NodeService
{
    use CreateContentContextTrait;

    const DOCUMENT_NODE_TYPE = 'Neos.Neos:Document';

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

    /**
     * Fetches the closest parent of the given type.
     * NOTE: If the given node is of the type already then it itself is returned.
     *
     * @param NodeInterface $referenceNode
     * @param string $type
     * @return NodeInterface
     */
    public function getClosestParentOfType(NodeInterface $referenceNode, string $type): ?NodeInterface
    {
        if ($referenceNode->getNodeType()->isOfType($type)) {
            return $referenceNode;
        }

        return (new FlowQuery([ $referenceNode ]))
            ->parents(sprintf('[instanceof %s]', $type))
            ->get(0);
    }

    public function getDocumentNode(NodeInterface $referenceNode): ?NodeInterface
    {
        return $this->getClosestParentOfType($referenceNode, static::DOCUMENT_NODE_TYPE);
    }

    /**
     * @param Node[] $nodes
     * @return Node|null
     */
    public function getFirstAvailableNode($nodes)
    {
        $firstAvailableNode = null;

        $orphanedNodes = $this->getOrphanedNodes();

        $orphanedNodesIdentifiers = array_map(
            function (array $node) {
                return $node['identifier'];
            },
            $orphanedNodes
        );

        foreach ($nodes as $node) {
            if (!in_array($node->getIdentifier(), $orphanedNodesIdentifiers)) {
                $firstAvailableNode = $node;
                break;
            }
        }

        return $firstAvailableNode;
    }

    private function getOrphanedNodes(): array
    {
        /** @var \Doctrine\ORM\QueryBuilder $queryBuilder */
        $queryBuilder = $this->entityManager->createQueryBuilder();

        $workspaceName = 'live';
        $workspaceList = [ $workspaceName ];

        $query = $queryBuilder
            ->select('n')
            ->from(NodeData::class, 'n')
            ->leftJoin(
                NodeData::class,
                'n2',
                \Doctrine\ORM\Query\Expr\Join::WITH,
                'n.parentPathHash = n2.pathHash AND n2.workspace IN (:workspaceList)'
            )
            ->where('n2.path IS NULL')
            ->andWhere($queryBuilder->expr()->not('n.path = :slash'))
            ->andWhere('n.workspace = :workspace');
        $parameters = [
            'workspaceList' => $workspaceList,
            'slash' => '/',
            'workspace' => $workspaceName,
        ];

        $orphanedNodes = $query
            ->setParameters($parameters)
            ->getQuery()->getArrayResult();

        return $orphanedNodes;
    }

    public function getCurrentSite(NodeInterface $node): Site
    {
        $context = $node->getContext();
        if ($context instanceof ContentContext) {
            return $context->getCurrentSite();
        }

        throw new UnknownSiteException('The current site could not be determined for this node', 1554465418);
    }

    public function getCurrentSiteNodeName(NodeInterface $node): string
    {
        return (string) $this->getCurrentSite($node)->getNodeName();
    }

    public function getCurrentSiteNode(?NodeInterface $node): ?Node
    {
        if ($node === null) {
            return null;
        }

        $path = sprintf('/sites/%s', $this->getCurrentSiteNodeName($node));

        do {
            /** @var Node $node */
            if ($node->getPath() === $path) {
                return $node;
            }
        } while ($node = $node->getParent());

        return null;
    }

    /** @return mixed */
    public function getPropertyWithFallback(
        NodeInterface $node,
        array $fallbackProperties = [],
        string $finalFallback = null
    ) {
        foreach ($fallbackProperties as $property) {
            if ($node->hasProperty($property)) {
                $value = $node->getProperty($property);
                if (!empty($value)) {
                    return $value;
                }
            }
        }

        return $finalFallback;
    }

    /**
     * @param NodeInterface|\Neos\ContentRepository\Domain\Projection\Content\NodeInterface $node
     * @return string|null
     */
    public function getLanguage($node): ?string
    {
        $language = $node->getContext()->getDimensions()['language'][0] ?? null;
        if ($language) {
            return $language;
        }

        if ($node instanceof NodeInterface) {
            return $node->getDimensions()['language'][0] ?? null;
        }

        return null;
    }


    public function nodeInLanguage(NodeInterface $node, string $language): ?NodeInterface
    {
        $context = $this->createContentContext($node->getWorkspace()->getName(), [ 'language' => [ $language ] ]);

        /** @var NodeInterface|null $translatedNode */
        $translatedNode = $context->getNodeByIdentifier($node->getIdentifier());
        if (!$translatedNode) {
            return null;
        }

        // Sometimes the default language node may not have the dimension set.
        $dimensions = $translatedNode->getNodeData()->getDimensions();
        if (count($dimensions) === 0) {
            $translatedNode->getNodeData()->setDimensions(
                [
                    new NodeDimension($translatedNode->getNodeData(), 'language', $language),
                ]
            );
        }

        return $translatedNode;
    }
}
