<?php

/**
 * MIT License. This file is part of the Propel package.
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Propel\Generator\Behavior\NestedSet;

use Propel\Generator\Builder\Om\QueryBuilder;
use Propel\Generator\Model\Column;

/**
 * Behavior to adds nested set tree structure columns and abilities
 *
 * @author François Zaninotto
 */
class NestedSetBehaviorQueryBuilderModifier
{
    /**
     * @var \Propel\Generator\Behavior\NestedSet\NestedSetBehavior
     */
    protected $behavior;

    /**
     * @var \Propel\Generator\Model\Table
     */
    protected $table;

    /**
     * @var \Propel\Generator\Builder\Om\QueryBuilder
     */
    protected $builder;

    /**
     * @var string
     */
    protected $objectClassName;

    /**
     * @var string
     */
    protected $queryClassName;

    /**
     * @var string
     */
    protected $tableMapClassName;

    /**
     * @param \Propel\Generator\Behavior\NestedSet\NestedSetBehavior $behavior
     */
    public function __construct(NestedSetBehavior $behavior)
    {
        $this->behavior = $behavior;
        $this->table = $behavior->getTable();
    }

    /**
     * @param string $key
     *
     * @return mixed
     */
    protected function getParameter(string $key)
    {
        return $this->behavior->getParameter($key);
    }

    /**
     * @param string $name
     *
     * @return \Propel\Generator\Model\Column
     */
    protected function getColumn(string $name): Column
    {
        return $this->behavior->getColumnForParameter($name);
    }

    /**
     * @param \Propel\Generator\Builder\Om\QueryBuilder $builder
     *
     * @return void
     */
    protected function setBuilder(QueryBuilder $builder): void
    {
        $this->builder = $builder;
        $this->objectClassName = $builder->getObjectClassName();
        $this->queryClassName = $builder->getQueryClassName();
        $this->tableMapClassName = $builder->getTableMapClassName();
    }

    /**
     * @param \Propel\Generator\Builder\Om\QueryBuilder $builder
     *
     * @return string
     */
    public function queryMethods(QueryBuilder $builder): string
    {
        $this->setBuilder($builder);
        $script = '';

        // select filters
        if ($this->behavior->useScope()) {
            $this->addTreeRoots($script);
            $this->addInTree($script);
        }

        $this->addDescendantsOf($script);
        $this->addBranchOf($script);
        $this->addChildrenOf($script);
        $this->addSiblingsOf($script);
        $this->addAncestorsOf($script);
        $this->addRootsOf($script);
        // select orders
        $this->addOrderByBranch($script);
        $this->addOrderByLevel($script);
        // select termination methods
        $this->addFindRoot($script);
        if ($this->behavior->useScope()) {
            $this->addFindRoots($script);
        }
        $this->addFindTree($script);

        if ($this->behavior->useScope()) {
            $this->addRetrieveRoots($script);
        }

        $this->addRetrieveRoot($script);
        $this->addRetrieveTree($script);
        $this->addIsValid($script);
        $this->addDeleteTree($script);
        $this->addShiftRLValues($script);
        $this->addShiftLevel($script);
        $this->addUpdateLoadedNodes($script);
        $this->addMakeRoomForLeaf($script);
        $this->addFixLevels($script);

        if ($this->behavior->useScope()) {
            $this->addSetNegativeScope($script);
        }

        return $script;
    }

    /**
     * @param string $script
     *
     * @return void
     */
    protected function addTreeRoots(string &$script): void
    {
        $leftColumnName = $this->behavior->getColumnConstant('left_column');
        $script .= "
/**
 * Filter the query to restrict the result to root objects
 *
 * @return \$this
 */
public function treeRoots()
{
    \$this->addUsingOperator(\$this->resolveLocalColumnByName('{$leftColumnName}'), 1, Criteria::EQUAL);

    return \$this;
}
";
    }

    /**
     * @param string $script
     *
     * @return void
     */
    protected function addInTree(string &$script): void
    {
        $scopeColumnName = $this->behavior->getColumnConstant('scope_column');
        $script .= "
/**
 * Returns the objects in a certain tree, from the tree scope
 *
 * @param int|null \$scope Scope to determine which objects node to return
 *
 * @return \$this
 */
public function inTree(?int \$scope = null)
{
    \$this->addUsingOperator(\$this->resolveLocalColumnByName('{$scopeColumnName}'), \$scope, Criteria::EQUAL);

    return \$this;
}
";
    }

    /**
     * @param string $script
     *
     * @return void
     */
    protected function addDescendantsOf(string &$script): void
    {
        $objectName = '$' . $this->table->getCamelCaseName();
        $leftColumnName = $this->behavior->getColumnConstant('left_column');
        $script .= "
/**
 * Filter the query to restrict the result to descendants of an object
 *
 * @param {$this->objectClassName} $objectName The object to use for descendant search
 *
 * @return \$this
 */
public function descendantsOf($this->objectClassName $objectName)
{
    \$leftColumn = \$this->resolveLocalColumnByName('{$leftColumnName}');
    \$this";
        if ($this->behavior->useScope()) {
            $script .= "
        ->inTree({$objectName}->getScopeValue())";
        }
        $script .= "
        ->addUsingOperator(\$leftColumn, {$objectName}->getLeftValue(), Criteria::GREATER_THAN)
        ->addUsingOperator(\$leftColumn, {$objectName}->getRightValue(), Criteria::LESS_THAN);

    return \$this;
}
";
    }

    /**
     * @param string $script
     *
     * @return void
     */
    protected function addBranchOf(string &$script): void
    {
        $objectName = '$' . $this->table->getCamelCaseName();
        $leftColumnName = $this->behavior->getColumnConstant('left_column');
        $script .= "
/**
 * Filter the query to restrict the result to the branch of an object.
 * Same as descendantsOf(), except that it includes the object passed as parameter in the result
 *
 * @param {$this->objectClassName} $objectName The object to use for branch search
 *
 * @return \$this
 */
public function branchOf($this->objectClassName $objectName)
{
    \$leftColumn = \$this->resolveLocalColumnByName('{$leftColumnName}');
    \$this";
        if ($this->behavior->useScope()) {
            $script .= "
        ->inTree({$objectName}->getScopeValue())";
        }
        $script .= "
        ->addUsingOperator(\$leftColumn, {$objectName}->getLeftValue(), Criteria::GREATER_EQUAL)
        ->addUsingOperator(\$leftColumn, {$objectName}->getRightValue(), Criteria::LESS_EQUAL);

    return \$this;
}
";
    }

    /**
     * @param string $script
     *
     * @return void
     */
    protected function addChildrenOf(string &$script): void
    {
        $objectName = '$' . $this->table->getCamelCaseName();
        $levelColumnName = $this->behavior->getColumnConstant('level_column');
        $script .= "
/**
 * Filter the query to restrict the result to children of an object
 *
 * @param {$this->objectClassName} $objectName The object to use for child search
 *
 * @return \$this
 */
public function childrenOf($this->objectClassName $objectName)
{
    \$this
        ->descendantsOf($objectName)
        ->addUsingOperator(\$this->resolveLocalColumnByName('{$levelColumnName}'), {$objectName}->getLevel() + 1, Criteria::EQUAL);

    return \$this;
}
";
    }

    /**
     * @param string $script
     *
     * @return void
     */
    protected function addSiblingsOf(string &$script): void
    {
        $objectName = '$' . $this->table->getCamelCaseName();
        $script .= "
/**
 * Filter the query to restrict the result to siblings of an object.
 * The result does not include the object passed as parameter.
 *
 * @param {$this->objectClassName} $objectName The object to use for sibling search
 * @param \Propel\Runtime\Connection\ConnectionInterface \$con Connection to use.
 *
 * @return \$this
 */
public function siblingsOf($this->objectClassName $objectName, ?ConnectionInterface \$con = null)
{
    if ({$objectName}->isRoot()) {
        \$this->
            add({$this->objectClassName}::LEVEL_COL, '1<>1', Criteria::CUSTOM);
    } else {
        \$this
            ->childrenOf({$objectName}->getParent(\$con))
            ->prune($objectName);
    }

    return \$this;
}
";
    }

    /**
     * @param string $script
     *
     * @return void
     */
    protected function addAncestorsOf(string &$script): void
    {
        $objectName = '$' . $this->table->getCamelCaseName();
        $leftColumnName = $this->behavior->getColumnConstant('left_column');
        $rightColumnName = $this->behavior->getColumnConstant('right_column');
        $script .= "
/**
 * Filter the query to restrict the result to ancestors of an object
 *
 * @param {$this->objectClassName} $objectName The object to use for ancestors search
 *
 * @return \$this
 */
public function ancestorsOf($this->objectClassName $objectName)
{
    \$this";
        if ($this->behavior->useScope()) {
            $script .= "
        ->inTree({$objectName}->getScopeValue())";
        }
        $script .= "
        ->addUsingOperator(\$this->resolveLocalColumnByName('{$leftColumnName}'), {$objectName}->getLeftValue(), Criteria::LESS_THAN)
        ->addUsingOperator(\$this->resolveLocalColumnByName('{$rightColumnName}'), {$objectName}->getRightValue(), Criteria::GREATER_THAN);

    return \$this;
}
";
    }

    /**
     * @param string $script
     *
     * @return void
     */
    protected function addRootsOf(string &$script): void
    {
        $objectName = '$' . $this->table->getCamelCaseName();
        $leftColumnName = $this->behavior->getColumnConstant('left_column');
        $rightColumnName = $this->behavior->getColumnConstant('right_column');
        $script .= "
/**
 * Filter the query to restrict the result to roots of an object.
 * Same as ancestorsOf(), except that it includes the object passed as parameter in the result
 *
 * @param {$this->objectClassName} $objectName The object to use for roots search
 *
 * @return \$this
 */
public function rootsOf($this->objectClassName $objectName)
{
    \$this";
        if ($this->behavior->useScope()) {
            $script .= "
        ->inTree({$objectName}->getScopeValue())";
        }
        $script .= "
        ->addUsingOperator(\$this->resolveLocalColumnByName('{$leftColumnName}'), {$objectName}->getLeftValue(), Criteria::LESS_EQUAL)
        ->addUsingOperator(\$this->resolveLocalColumnByName('{$rightColumnName}'), {$objectName}->getRightValue(), Criteria::GREATER_EQUAL);

    return \$this;
}
";
    }

    /**
     * @param string $script
     *
     * @return void
     */
    protected function addOrderByBranch(string &$script): void
    {
        $script .= "
/**
 * Order the result by branch, i.e. natural tree order
 *
 * @param bool \$reverse if true, reverses the order
 *
 * @return \$this
 */
public function orderByBranch(\$reverse = false)
{
    if (\$reverse) {
        \$this
            ->addDescendingOrderByColumn({$this->objectClassName}::LEFT_COL);
    } else {
        \$this
            ->addAscendingOrderByColumn({$this->objectClassName}::LEFT_COL);
    }

    return \$this;
}
";
    }

    /**
     * @param string $script
     *
     * @return void
     */
    protected function addOrderByLevel(string &$script): void
    {
        $script .= "
/**
 * Order the result by level, the closer to the root first
 *
 * @param bool \$reverse if true, reverses the order
 *
 * @return \$this
 */
public function orderByLevel(\$reverse = false)
{
    if (\$reverse) {
        \$this
            ->addDescendingOrderByColumn({$this->objectClassName}::LEVEL_COL)
            ->addDescendingOrderByColumn({$this->objectClassName}::LEFT_COL);
    } else {
        \$this
            ->addAscendingOrderByColumn({$this->objectClassName}::LEVEL_COL)
            ->addAscendingOrderByColumn({$this->objectClassName}::LEFT_COL);
    }

    return \$this;
}
";
    }

    /**
     * @param string $script
     *
     * @return void
     */
    protected function addFindRoot(string &$script): void
    {
        $useScope = $this->behavior->useScope();
        $leftColumnName = $this->behavior->getColumnConstant('left_column');
        $script .= "
/**
 * Returns " . ($useScope ? 'a' : 'the') . " root node for the tree
 *";
        if ($useScope) {
            $script .= "
 * @param int \$scope        Scope to determine which root node to return";
        }

        $script .= "
 * @param \Propel\Runtime\Connection\ConnectionInterface \$con Connection to use.
 *
 * @return {$this->objectClassName} The tree root object
 */
public function findRoot(" . ($useScope ? '$scope = null, ' : '') . "?ConnectionInterface \$con = null)
{
    return \$this
        ->addUsingOperator(\$this->resolveLocalColumnByName('{$leftColumnName}'), 1, Criteria::EQUAL)";
        if ($useScope) {
            $script .= "
        ->inTree(\$scope)";
        }
        $script .= "
        ->findOne(\$con);
}
";
    }

    /**
     * @param string $script
     *
     * @return void
     */
    protected function addFindRoots(string &$script): void
    {
        $script .= "
/**
 * Returns the root objects for all trees.
 *
 * @param \Propel\Runtime\Connection\ConnectionInterface \$con Connection to use.
 *
 * @return {$this->objectClassName}[]|ObjectCollection|mixed the list of results, formatted by the current formatter
 */
public function findRoots(?ConnectionInterface \$con = null)
{
    return \$this
        ->treeRoots()
        ->find(\$con);
}
";
    }

    /**
     * @param string $script
     *
     * @return void
     */
    protected function addFindTree(string &$script): void
    {
        $useScope = $this->behavior->useScope();
        $script .= "
/**
 * Returns " . ($useScope ? 'a' : 'the') . " tree of objects
 *";
        if ($useScope) {
            $script .= "
 * @param int \$scope        Scope to determine which tree node to return";
        }

        $script .= "
 * @param \Propel\Runtime\Connection\ConnectionInterface \$con Connection to use.
 *
 * @return {$this->objectClassName}[]|ObjectCollection|mixed the list of results, formatted by the current formatter
 */
public function findTree(" . ($useScope ? '$scope = null, ' : '') . "?ConnectionInterface \$con = null)
{
    return \$this";
        if ($useScope) {
            $script .= "
        ->inTree(\$scope)";
        }
        $script .= "
        ->orderByBranch()
        ->find(\$con);
}
";
    }

    /**
     * @param string $script
     *
     * @return void
     */
    protected function addRetrieveRoots(string &$script): void
    {
        $queryClassName = $this->queryClassName;
        $objectClassName = $this->objectClassName;
        $tableMapClassName = $this->builder->getTableMapClass();

        $script .= "
/**
 * Returns the root nodes for the tree
 *
 * @param \Propel\Runtime\ActiveQuery\Criteria|null \$query    Optional Criteria to filter the query
 * @param \Propel\Runtime\Connection\ConnectionInterface \$con Connection to use.
 * @return {$this->objectClassName}[]|ObjectCollection|mixed the list of results, formatted by the current formatter
 */
static public function retrieveRoots(?Criteria \$query = null, ?ConnectionInterface \$con = null)
{
    if (!\$query) {
        \$query = new Criteria($tableMapClassName::DATABASE_NAME);
    }
    \$query->addAnd($objectClassName::LEFT_COL, 1, Criteria::EQUAL);

    return $queryClassName::create(null, \$query)->find(\$con);
}
";
    }

    /**
     * @param string $script
     *
     * @return void
     */
    protected function addRetrieveRoot(string &$script): void
    {
        $queryClassName = $this->queryClassName;
        $objectClassName = $this->objectClassName;
        $useScope = $this->behavior->useScope();
        $tableMapClassName = $this->builder->getTableMapClass();

        $script .= "
/**
 * Returns the root node for a given scope
 *";
        if ($useScope) {
            $script .= "
 * @param int \$scope        Scope to determine which root node to return";
        }
        $script .= "
 * @param \Propel\Runtime\Connection\ConnectionInterface \$con Connection to use.
 *
 * @return {$this->objectClassName}            Propel object for root node
 */
static public function retrieveRoot(" . ($useScope ? '$scope = null, ' : '') . "?ConnectionInterface \$con = null)
{
    \$query = new Criteria($tableMapClassName::DATABASE_NAME);
    \$query->addAnd($objectClassName::LEFT_COL, 1, Criteria::EQUAL);";
        if ($useScope) {
            $script .= "
    \$query->addAnd($objectClassName::SCOPE_COL, \$scope, Criteria::EQUAL);";
        }
        $script .= "

    return $queryClassName::create(null, \$query)->findOne(\$con);
}
";
    }

    /**
     * @param string $script
     *
     * @return void
     */
    protected function addRetrieveTree(string &$script): void
    {
        $queryClassName = $this->queryClassName;
        $objectClassName = $this->objectClassName;
        $useScope = $this->behavior->useScope();
        $tableMapClassName = $this->builder->getTableMapClass();

        $script .= "
/**
 * Returns the whole tree node for a given scope
 *";
        if ($useScope) {
            $script .= "
 * @param int \$scope        Scope to determine which root node to return";
        }
        $script .= "
 * @param \Propel\Runtime\ActiveQuery\Criteria \$query    Optional Criteria to filter the query
 * @param \Propel\Runtime\Connection\ConnectionInterface \$con Connection to use.
 *
 * @return {$this->objectClassName}[]|ObjectCollection|mixed the list of results, formatted by the current formatter
 */
static public function retrieveTree(" . ($useScope ? '$scope = null, ' : '') . "?Criteria \$query = null, ?ConnectionInterface \$con = null)
{
    if (null === \$query) {
        \$query = new Criteria($tableMapClassName::DATABASE_NAME);
    }
    \$query->addAscendingOrderByColumn($objectClassName::LEFT_COL);";
        if ($useScope) {
            $script .= "
    \$query->addAnd($objectClassName::SCOPE_COL, \$scope, Criteria::EQUAL);";
        }
        $script .= "

    return $queryClassName::create(null, \$query)->find(\$con);
}
";
    }

    /**
     * @param string $script
     *
     * @return void
     */
    protected function addIsValid(string &$script): void
    {
        $objectClassName = $this->objectClassName;

        $script .= "
/**
 * Tests if node is valid
 *
 * @param $objectClassName \$node    Propel object for src node
 *
 * @return bool
 */
static public function isValid(?$objectClassName \$node = null)
{
    if (is_object(\$node) && \$node->getRightValue() > \$node->getLeftValue()) {
        return true;
    } else {
        return false;
    }
}
";
    }

    /**
     * @param string $script
     *
     * @return void
     */
    protected function addDeleteTree(string &$script): void
    {
        $objectClassName = $this->objectClassName;
        $useScope = $this->behavior->useScope();
        $tableMapClassName = $this->builder->getTableMapClass();

        $script .= "
/**
 * Delete an entire tree
 * ";
        if ($useScope) {
            $script .= "
 * @param int \$scope        Scope to determine which tree to delete";
        }
        $script .= "
 * @param \Propel\Runtime\Connection\ConnectionInterface \$con Connection to use.
 *
 * @return int The number of deleted nodes
 */
static public function deleteTree(" . ($useScope ? '$scope = null, ' : '') . "?ConnectionInterface \$con = null)
{";
        if ($useScope) {
            $script .= "
    \$query = new Criteria($tableMapClassName::DATABASE_NAME);
    \$query->addAnd($objectClassName::SCOPE_COL, \$scope, Criteria::EQUAL);

    return $tableMapClassName::doDelete(\$query, \$con);";
        } else {
            $script .= "

    return $tableMapClassName::doDeleteAll(\$con);";
        }
        $script .= "
}
";
    }

    /**
     * @param string $script
     *
     * @return void
     */
    protected function addShiftRLValues(string &$script): void
    {
        $objectClassName = $this->objectClassName;
        $useScope = $this->behavior->useScope();
        $tableMapClassName = $this->builder->getTableMapClass();

        $this->builder->declareClass('Propel\\Runtime\Map\\TableMap');

        $script .= "
/**
 * Adds \$delta to all L and R values that are >= \$first and <= \$last.
 * '\$delta' can also be negative.
 *
 * @param int \$delta               Value to be shifted by, can be negative
 * @param int \$first               First node to be shifted
 * @param int \$last                Last node to be shifted (optional)";
        if ($useScope) {
            $script .= "
 * @param int \$scope               Scope to use for the shift";
        }
        $script .= "
 * @param \Propel\Runtime\Connection\ConnectionInterface \$con Connection to use.
 *
 * @return void
 */
static public function shiftRLValues(\$delta, \$first, \$last = null" . ($useScope ? ', $scope = null' : '') . ", ?ConnectionInterface \$con = null)
{
    if (\$con === null) {
        \$con = Propel::getServiceContainer()->getWriteConnection($tableMapClassName::DATABASE_NAME);
    }

    // Shift left column values
    \$updateQuery = new Criteria($tableMapClassName::DATABASE_NAME);
    \$criterion = \$updateQuery->getNewCriterion($objectClassName::LEFT_COL, \$first, Criteria::GREATER_EQUAL);
    if (null !== \$last) {
        \$criterion->addAnd(\$updateQuery->getNewCriterion($objectClassName::LEFT_COL, \$last, Criteria::LESS_EQUAL));
    }
    \$updateQuery->addAnd(\$criterion);";
        if ($useScope) {
            $script .= "
    \$updateQuery->addAnd($objectClassName::SCOPE_COL, \$scope, Criteria::EQUAL);";
        }
        $script .= "

    \$updateQuery->setUpdateExpression($objectClassName::LEFT_COL, $objectClassName::LEFT_COL . ' + ?', \$delta, \PDO::PARAM_INT);

    \$updateQuery->doUpdate(null, \$con);

    // Shift right column values
    \$updateQuery = new Criteria($tableMapClassName::DATABASE_NAME);
    \$criterion = \$updateQuery->getNewCriterion($objectClassName::RIGHT_COL, \$first, Criteria::GREATER_EQUAL);
    if (null !== \$last) {
        \$criterion->addAnd(\$updateQuery->getNewCriterion($objectClassName::RIGHT_COL, \$last, Criteria::LESS_EQUAL));
    }
    \$updateQuery->addAnd(\$criterion);";
        if ($useScope) {
            $script .= "
    \$updateQuery->addAnd($objectClassName::SCOPE_COL, \$scope, Criteria::EQUAL);";
        }
        $script .= "

    \$updateQuery->setUpdateExpression($objectClassName::RIGHT_COL, $objectClassName::RIGHT_COL . ' + ?', \$delta, \PDO::PARAM_STR);

    \$updateQuery->doUpdate(null, \$con);
}
";
    }

    /**
     * @param string $script
     *
     * @return void
     */
    protected function addShiftLevel(string &$script): void
    {
        $objectClassName = $this->objectClassName;
        $useScope = $this->behavior->useScope();
        $tableMapClassName = $this->builder->getTableMapClass();

        $this->builder->declareClass('Propel\\Runtime\Map\\TableMap');

        $script .= "
/**
 * Adds \$delta to level for nodes having left value >= \$first and right value <= \$last.
 * '\$delta' can also be negative.
 *
 * @param int \$delta        Value to be shifted by, can be negative
 * @param int \$first        First node to be shifted
 * @param int \$last            Last node to be shifted";
        if ($useScope) {
            $script .= "
 * @param int \$scope        Scope to use for the shift";
        }
        $script .= "
 * @param \Propel\Runtime\Connection\ConnectionInterface \$con Connection to use.
 *
 * @return void
 */
static public function shiftLevel(\$delta, \$first, \$last" . ($useScope ? ', $scope = null' : '') . ", ?ConnectionInterface \$con = null)
{
    if (\$con === null) {
        \$con = Propel::getServiceContainer()->getWriteConnection($tableMapClassName::DATABASE_NAME);
    }

    \$updateQuery = new Criteria($tableMapClassName::DATABASE_NAME);
    \$updateQuery->addAnd($objectClassName::LEFT_COL, \$first, Criteria::GREATER_EQUAL);
    \$updateQuery->addAnd($objectClassName::RIGHT_COL, \$last, Criteria::LESS_EQUAL);";
        if ($useScope) {
            $script .= "
    \$updateQuery->addAnd($objectClassName::SCOPE_COL, \$scope, Criteria::EQUAL);";
        }
        $script .= "

    \$updateQuery->setUpdateExpression($objectClassName::LEVEL_COL, $objectClassName::LEVEL_COL . ' + ?', \$delta, \PDO::PARAM_INT);

    \$updateQuery->doUpdate(null, \$con);
}
";
    }

    /**
     * @param string $script
     *
     * @return void
     */
    protected function addUpdateLoadedNodes(string &$script): void
    {
        $queryClassName = $this->queryClassName;
        $objectClassName = $this->objectClassName;
        $tableMapClassName = $this->tableMapClassName;

        $script .= "
/**
 * Reload all already loaded nodes to sync them with updated db
 *
 * @param $objectClassName \$prune Object to prune from the update
 * @param \Propel\Runtime\Connection\ConnectionInterface \$con Connection to use.
 *
 * @return void
 */
static public function updateLoadedNodes(\$prune = null, ?ConnectionInterface \$con = null)
{
    if (Propel::isInstancePoolingEnabled()) {
        \$keys = [];
        /** @var \$obj $objectClassName */
        foreach ($tableMapClassName::\$instances as \$obj) {
            if (!\$prune || !\$prune->equals(\$obj)) {
                \$keys[] = \$obj->getPrimaryKey();
            }
        }

        if (!empty(\$keys)) {
            // We don't need to alter the object instance pool; we're just modifying these ones
            // already in the pool.
            \$query = new Criteria($tableMapClassName::DATABASE_NAME);";
        if (count($this->table->getPrimaryKey()) === 1) {
            $pkey = $this->table->getPrimaryKey();
            $col = array_shift($pkey);
            $script .= "
            \$query->addAnd(" . $this->builder->getColumnConstant($col) . ', $keys, Criteria::IN);';
        } else {
            $fields = [];
            foreach ($this->table->getPrimaryKey() as $col) {
                $fields[] = $this->builder->getColumnConstant($col);
            }
            $script .= "

            // Loop on each instances in pool
            foreach (\$keys as \$values) {
              // Create initial Criterion
                \$cton = \$query->getNewCriterion(" . $fields[0] . ', $values[0]);';
            unset($fields[0]);
            foreach ($fields as $k => $col) {
                $script .= "

                // Create next criterion
                \$nextcton = \$query->getNewCriterion(" . $col . ", \$values[$k]);
                // And merge it with the first
                \$cton->addAnd(\$nextcton);";
            }
            $script .= "

                // Add final Criterion to Criteria
                \$query->addOr(\$cton);
            }";
        }

        $script .= "
            \$dataFetcher = $queryClassName::create(null, \$query)->fetch(\$con);
            while (\$row = \$dataFetcher->fetch()) {
                \$key = $tableMapClassName::getPrimaryKeyHashFromRow(\$row, 0);
                /** @var \$object $objectClassName */
                if (null !== (\$object = $tableMapClassName::getInstanceFromPool(\$key))) {";
        $n = 0;
        foreach ($this->table->getColumns() as $col) {
            if ($col->isLazyLoad()) {
                continue;
            }
            if ($col->getPhpName() === $this->getColumnPhpName('left_column')) {
                $script .= "
                    \$object->setLeftValue(\$row[$n]);";
            } elseif ($col->getPhpName() === $this->getColumnPhpName('right_column')) {
                $script .= "
                    \$object->setRightValue(\$row[$n]);";
            } elseif ($this->getParameter('use_scope') == 'true' && $col->getPhpName() === $this->getColumnPhpName('scope_column')) {
                $script .= "
                    \$object->setScopeValue(\$row[$n]);";
            } elseif ($col->getPhpName() === $this->getColumnPhpName('level_column')) {
                $script .= "
                    \$object->setLevel(\$row[$n]);
                    \$object->clearNestedSetChildren();";
            }
            $n++;
        }
        $script .= "
                }
            }
            \$dataFetcher->close();
        }
    }
}
";
    }

    /**
     * @param string $script
     *
     * @return void
     */
    protected function addMakeRoomForLeaf(string &$script): void
    {
        $queryClassName = $this->queryClassName;
        $useScope = $this->behavior->useScope();

        $script .= "
/**
 * Update the tree to allow insertion of a leaf at the specified position
 *
 * @param int \$left    left column value";
        if ($useScope) {
            $script .= "
 * @param int \$scope    scope column value";
        }
        $script .= "
 * @param mixed \$prune    Object to prune from the shift
 * @param \Propel\Runtime\Connection\ConnectionInterface|null \$con Connection to use.
 *
 * @return void
 */
static public function makeRoomForLeaf(\$left" . ($useScope ? ', $scope' : '') . ", \$prune = null, ?ConnectionInterface \$con = null)
{
    // Update database nodes
    $queryClassName::shiftRLValues(2, \$left, null" . ($useScope ? ', $scope' : '') . ", \$con);

    // Update all loaded nodes
    $queryClassName::updateLoadedNodes(\$prune, \$con);
}
";
    }

    /**
     * @param string $script
     *
     * @return void
     */
    protected function addFixLevels(string &$script): void
    {
        $objectClassName = $this->objectClassName;
        $queryClassName = $this->queryClassName;
        $tableMapClassName = $this->tableMapClassName;
        $useScope = $this->behavior->useScope();

        $script .= "
/**
 * Update the tree to allow insertion of a leaf at the specified position
 *";
        if ($useScope) {
            $script .= "
 * @param int \$scope    scope column value";
        }
        $script .= "
 * @param \Propel\Runtime\Connection\ConnectionInterface|null \$con Connection to use.
 *
 * @return void
 */
static public function fixLevels(" . ($useScope ? '$scope, ' : '') . "?ConnectionInterface \$con = null): void
{
    \$query = new Criteria();";
        if ($useScope) {
            $script .= "
    \$query->addAnd($objectClassName::SCOPE_COL, \$scope, Criteria::EQUAL);";
        }
        $script .= "
    \$query->addAscendingOrderByColumn($objectClassName::LEFT_COL);
    \$dataFetcher = $queryClassName::create(null, \$query)->fetch(\$con);
    ";
        if (!$this->table->getChildrenColumn()) {
            $script .= "
    // set the class once to avoid overhead in the loop
    \$cls = $tableMapClassName::getOMClass(false);";
        }

        $script .= "
    \$level = null;
    // iterate over the statement
    while (\$row = \$dataFetcher->fetch()) {

        // hydrate object
        \$key = $tableMapClassName::getPrimaryKeyHashFromRow(\$row, 0);
        /** @var \$obj $objectClassName */
        if (null === (\$obj = $tableMapClassName::getInstanceFromPool(\$key))) {";
        if ($this->table->getChildrenColumn()) {
            $script .= "
            // class must be set each time from the record row
            \$cls = $tableMapClassName::getOMClass(\$row, 0);
            \$cls = substr('.'.\$cls, strrpos('.'.\$cls, '.') + 1);
            " . $this->builder->buildObjectInstanceCreationCode('$obj', '$cls') . "
            \$obj->hydrate(\$row);
            $tableMapClassName::addInstanceToPool(\$obj, \$key);";
        } else {
            $script .= "
            " . $this->builder->buildObjectInstanceCreationCode('$obj', '$cls') . "
            \$obj->hydrate(\$row);
            $tableMapClassName::addInstanceToPool(\$obj, \$key);";
        }
        $script .= "
        }

        // compute level
        // Algorithm shamelessly stolen from sfPropelActAsNestedSetBehaviorPlugin
        // Probably authored by Tristan Rivoallan
        if (\$level === null) {
            \$level = 0;
            \$i = 0;
            \$prev = [\$obj->getRightValue()];
        } else {
            while (\$obj->getRightValue() > \$prev[\$i]) {
                \$i--;
            }
            \$level = ++\$i;
            \$prev[\$i] = \$obj->getRightValue();
        }

        // update level in node if necessary
        if (\$obj->getLevel() !== \$level) {
            \$obj->setLevel(\$level);
            \$obj->save(\$con);
        }
    }
    \$dataFetcher->close();
}
";
    }

    /**
     * @param string $script
     *
     * @return void
     */
    protected function addSetNegativeScope(string &$script): void
    {
        $objectClassName = $this->objectClassName;
        $tableMapClassName = $this->tableMapClassName;
        $script .= "
/**
 * Updates all scope values for items that has negative left (<=0) values.
 *
 * @param mixed \$scope
 * @param \Propel\Runtime\Connection\ConnectionInterface|null \$con Connection to use.
 *
 * @return void
 */
public static function setNegativeScope(\$scope, ?ConnectionInterface \$con = null): void
{
    //adjust scope value to \$scope
    \$updateQuery = new Criteria($tableMapClassName::DATABASE_NAME);
    \$updateQuery->addAnd($objectClassName::LEFT_COL, 0, Criteria::LESS_EQUAL);
    \$updateQuery->setUpdateValue($objectClassName::SCOPE_COL, \$scope);

    \$updateQuery->doUpdate(null, \$con);
}
";
    }

    /**
     * @param string $columnName
     *
     * @return string
     */
    protected function getColumnPhpName(string $columnName): string
    {
        return $this->behavior->getColumnForParameter($columnName)->getPhpName();
    }
}
