<?php
// Copyright (c) Pickware GmbH. All rights reserved.
// This file is part of software that is released under a proprietary license.
// You must not copy, modify, distribute, make publicly available, or execute
// its contents or parts thereof without express permission by the copyright
// holder, unless otherwise permitted by law.

namespace Shopware\Plugins\ViisonPickwareMobile\Components\QueryBuilder;

class QueryBuilderJoinResolver
{
    /**
     * Determines all tables that are required by the given `$queryBuilder` and joins them to it, if not already joined.
     * Only tables available in the passed `$joinComponents` can be joined.
     *
     * @param QueryBuilder $queryBuilder
     * @param AbstractJoinQueryComponent[] $joinComponents
     */
    public static function addMissingJoinsToQueryBuilder(QueryBuilder $queryBuilder, array $joinComponents)
    {
        $joinComponentIndex = array_combine(
            array_map(
                function (AbstractJoinQueryComponent $joinComponents) {
                    return $joinComponents->getJoiningTableName();
                },
                $joinComponents
            ),
            $joinComponents
        );

        $joinedTableNames = array_fill_keys($queryBuilder->getJoinedTableNames(), true);
        $missingJoins = self::collectRequiredJoins(
            $joinComponentIndex,
            $queryBuilder->getRequiredTableNames(),
            $joinedTableNames
        );
        $queryBuilder->join(...$missingJoins);
    }

    /**
     * Recursively collects all join components contained in $availableJoins, that join tables defined in
     * $requiredTableNames. If any required join component requires another table to be joined first, that join is
     * collected first. Furthermore no table is joined twice. If any required table is not available for joining, an
     * exception is thrown.
     *
     * @param array $availableJoins
     * @param string[] $requiredTableNames
     * @param string[] &$joinedTableNames
     * @return AbstractJoinQueryComponent[]
     * @throws QueryBuilderException if any of the $requiredTableNames is not available for joining.
     */
    protected static function collectRequiredJoins(
        array $availableJoins,
        array $requiredTableNames,
        array &$joinedTableNames
    ) {
        if (count($requiredTableNames) === 0) {
            // End recursion
            return [];
        }

        // Check if join is possible
        $requiredTableName = array_shift($requiredTableNames);
        if (!isset($availableJoins[$requiredTableName])) {
            throw QueryBuilderException::tableNotAvailableForJoining($requiredTableName);
        }

        // Don't join any table twice
        if (isset($joinedTableNames[$requiredTableName])) {
            return self::collectRequiredJoins($availableJoins, $requiredTableNames, $joinedTableNames);
        }

        // Collect all joins needed for the required table
        $joinedTableNames[$requiredTableName] = true;
        $joinCondition = $availableJoins[$requiredTableName];
        $joins = self::collectRequiredJoins($availableJoins, $joinCondition->getRequiredTables(), $joinedTableNames);
        $joins[] = $joinCondition;

        // Recursively add remaining joins
        return array_merge($joins, self::collectRequiredJoins($availableJoins, $requiredTableNames, $joinedTableNames));
    }
}
