<?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\ObjectBuilder;

/**
 * Behavior to adds nested set tree structure columns and abilities
 *
 * @author François Zaninotto
 * @author heltem <heltem@o2php.com>
 */
class NestedSetBehaviorObjectBuilderModifier
{
    /**
     * @var \Propel\Generator\Behavior\NestedSet\NestedSetBehavior
     */
    protected $behavior;

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

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

    /**
     * @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 string
     */
    protected function getColumnAttribute(string $name): string
    {
        return strtolower($this->behavior->getColumnForParameter($name)->getName());
    }

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

    /**
     * @param \Propel\Generator\Builder\Om\ObjectBuilder $builder
     *
     * @return void
     */
    protected function setBuilder(ObjectBuilder $builder): void
    {
        $this->builder = $builder;
    }

    /**
     * @param \Propel\Generator\Builder\Om\ObjectBuilder $builder
     *
     * @return string
     */
    public function preSave(ObjectBuilder $builder): string
    {
        $queryClassName = $builder->getQueryClassName();
        $objectClassName = $builder->getObjectClassName();

        $script = "if (\$this->isNew() && \$this->isRoot()) {
    // check if no other root exist in, the tree
    \$rootExists = $queryClassName::create()
        ->addUsingOperator($objectClassName::LEFT_COL, 1, Criteria::EQUAL)";

        if ($this->behavior->useScope()) {
            $script .= "
        ->addUsingOperator($objectClassName::SCOPE_COL, \$this->getScopeValue(), Criteria::EQUAL)";
        }

        $script .= "
        ->exists(\$con);
    if (\$rootExists) {
            throw new PropelException(";

        if ($this->behavior->useScope()) {
            $script .= "sprintf('A root node already exists in this tree with scope \"%s\".', \$this->getScopeValue())";
        } else {
            $script .= "'A root node already exists in this tree. To allow multiple root nodes, add the `use_scope` parameter in the nested_set behavior tag.'";
        }

        $script .= ");
    }
}
\$this->processNestedSetQueries(\$con);";

        return $script;
    }

    /**
     * @param \Propel\Generator\Builder\Om\ObjectBuilder $builder
     *
     * @return string
     */
    public function preDelete(ObjectBuilder $builder): string
    {
        $queryClassName = $builder->getQueryClassName();

        return "if (\$this->isRoot()) {
    throw new PropelException('Deletion of a root node is disabled for nested sets. Use `$queryClassName::deleteTree(" . ($this->behavior->useScope() ? '$scope' : '') . ")` instead to delete an entire tree');
}

if (\$this->isInTree()) {
    \$this->deleteDescendants(\$con);
}
";
    }

    /**
     * @param \Propel\Generator\Builder\Om\ObjectBuilder $builder
     *
     * @return string
     */
    public function postDelete(ObjectBuilder $builder): string
    {
        $queryClassName = $builder->getQueryClassName();

        return "if (\$this->isInTree()) {
    // fill up the room that was used by the node
    $queryClassName::shiftRLValues(-2, \$this->getRightValue() + 1, null" . ($this->behavior->useScope() ? ', $this->getScopeValue()' : '') . ", \$con);
}
";
    }

    /**
     * @param \Propel\Generator\Builder\Om\ObjectBuilder $builder
     *
     * @return string
     */
    public function objectClearReferences(ObjectBuilder $builder): string
    {
        return "\$this->collNestedSetChildren = null;
\$this->aNestedSetParent = null;";
    }

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

        $this->addProcessNestedSetQueries($script);

        if ($this->getColumnPhpName('left_column') !== 'LeftValue') {
            $this->addGetLeft($script);
        }
        if ($this->getColumnPhpName('right_column') !== 'RightValue') {
            $this->addGetRight($script);
        }
        if ($this->getColumnPhpName('level_column') !== 'Level') {
            $this->addGetLevel($script);
        }
        if (
            $this->getParameter('use_scope') === 'true'
            && $this->getColumnPhpName('scope_column') !== 'ScopeValue'
        ) {
            $this->addGetScope($script);
        }

        if ($this->getColumnPhpName('left_column') !== 'LeftValue') {
            $script .= $this->addSetLeft();
        }
        if ($this->getColumnPhpName('right_column') !== 'RightValue') {
            $this->addSetRight($script);
        }
        if ($this->getColumnPhpName('level_column') !== 'Level') {
            $this->addSetLevel($script);
        }
        if (
            $this->getParameter('use_scope') === 'true'
            && $this->getColumnPhpName('scope_column') !== 'ScopeValue'
        ) {
            $this->addSetScope($script);
        }

        $this->addMakeRoot($script);

        $this->addIsInTree($script);
        $this->addIsRoot($script);
        $this->addIsLeaf($script);
        $this->addIsDescendantOf($script);
        $this->addIsAncestorOf($script);

        $this->addHasParent($script);
        $this->addSetParent($script);
        $this->addGetParent($script);

        $this->addHasPrevSibling($script);
        $this->addGetPrevSibling($script);

        $this->addHasNextSibling($script);
        $this->addGetNextSibling($script);

        $this->addNestedSetChildrenClear($script);
        $this->addNestedSetChildrenInit($script);
        $this->addNestedSetChildAdd($script);
        $this->addHasChildren($script);
        $this->addGetChildren($script);
        $this->addCountChildren($script);

        $this->addGetFirstChild($script);
        $this->addGetLastChild($script);
        $this->addGetSiblings($script);
        $this->addGetDescendants($script);
        $this->addCountDescendants($script);
        $this->addGetBranch($script);
        $this->addGetAncestors($script);

        $this->builder->declareClassFromBuilder($builder->getStubObjectBuilder(), 'Child');
        $this->addAddChild($script);
        $this->addInsertAsFirstChildOf($script);

        $script .= $this->addInsertAsLastChildOf();

        $this->addInsertAsPrevSiblingOf($script);
        $this->addInsertAsNextSiblingOf($script);

        $this->addMoveToFirstChildOf($script);
        $this->addMoveToLastChildOf($script);
        $this->addMoveToPrevSiblingOf($script);
        $this->addMoveToNextSiblingOf($script);
        $this->addMoveSubtreeTo($script);

        $this->addDeleteDescendants($script);

        $this->builder->declareClass(
            '\Propel\Runtime\ActiveRecord\NestedSetRecursiveIterator',
        );

        $script .= $this->addGetIterator();

        return $script;
    }

    /**
     * @param string $script
     *
     * @return void
     */
    protected function addProcessNestedSetQueries(string &$script): void
    {
        $script .= "
/**
 * Execute queries that were saved to be run inside the save transaction
 *
 * @param \Propel\Runtime\Connection\ConnectionInterface \$con Connection to use.
 *
 * @return void
 */
protected function processNestedSetQueries(ConnectionInterface \$con): void
{
    foreach (\$this->nestedSetQueries as ['callable' => \$callable, 'arguments' => \$arguments]) {
        \$arguments[] = \$con;
        \$callable(...\$arguments);
    }
    \$this->nestedSetQueries = [];
}
";
    }

    /**
     * @param string $script
     *
     * @return void
     */
    protected function addGetLeft(string &$script): void
    {
        $script .= "
/**
 * Proxy getter method for the left value of the nested set model.
 * It provides a generic way to get the value, whatever the actual column name is.
 *
 * @return int The nested set left value
 */
public function getLeftValue(): int
{
    return \$this->{$this->getColumnAttribute('left_column')} ?: 0;
}
";
    }

    /**
     * @param string $script
     *
     * @return void
     */
    protected function addGetRight(string &$script): void
    {
        $script .= "
/**
 * Proxy getter method for the right value of the nested set model.
 * It provides a generic way to get the value, whatever the actual column name is.
 *
 * @return int The nested set right value
 */
public function getRightValue(): int
{
    return \$this->{$this->getColumnAttribute('right_column')} ?: 0;
}
";
    }

    /**
     * @param string $script
     *
     * @return void
     */
    protected function addGetLevel(string &$script): void
    {
        $script .= "
/**
 * Proxy getter method for the level value of the nested set model.
 * It provides a generic way to get the value, whatever the actual column name is.
 *
 * @return int The nested set level value
 */
public function getLevel(): int
{
    return \$this->{$this->getColumnAttribute('level_column')} ?: 0;
}
";
    }

    /**
     * @param string $script
     *
     * @return void
     */
    protected function addGetScope(string &$script): void
    {
        $script .= "
/**
 * Proxy getter method for the scope value of the nested set model.
 * It provides a generic way to get the value, whatever the actual column name is.
 *
 * @return int The nested set scope value
 */
public function getScopeValue(): int
{
    return \$this->{$this->getColumnAttribute('scope_column')};
}
";
    }

    /**
     * @return string
     */
    protected function addSetLeft(): string
    {
        return $this->behavior->renderTemplate('objectSetLeft', [
            'objectClassName' => $this->builder->getObjectClassName(),
            'leftColumn' => $this->getColumnPhpName('left_column'),
        ]);
    }

    /**
     * @param string $script
     *
     * @return void
     */
    protected function addSetRight(string &$script): void
    {
        $script .= "
/**
 * Proxy setter method for the right value of the nested set model.
 * It provides a generic way to set the value, whatever the actual column name is.
 *
 * @param int \$v The nested set right value
 *
 * @return \$this
 */
public function setRightValue(int \$v)
{
    \$this->set{$this->getColumnPhpName('right_column')}(\$v);

    return \$this;
}
";
    }

    /**
     * @param string $script
     *
     * @return void
     */
    protected function addSetLevel(string &$script): void
    {
        $script .= "
/**
 * Proxy setter method for the level value of the nested set model.
 * It provides a generic way to set the value, whatever the actual column name is.
 *
 * @param int \$v The nested set level value
 *
 * @return \$this
 */
public function setLevel(int \$v)
{
    \$this->set{$this->getColumnPhpName('level_column')}(\$v);

    return \$this;
}
";
    }

    /**
     * @param string $script
     *
     * @return void
     */
    protected function addSetScope(string &$script): void
    {
        $script .= "
/**
 * Proxy setter method for the scope value of the nested set model.
 * It provides a generic way to set the value, whatever the actual column name is.
 *
 * @param int \$v The nested set scope value
 *
 * @return \$this
 */
public function setScopeValue(int \$v)
{
    \$this->set{$this->getColumnPhpName('scope_column')}(\$v);

    return \$this;
}
";
    }

    /**
     * @param string $script
     *
     * @return void
     */
    protected function addMakeRoot(string &$script): void
    {
        $script .= "
/**
 * Creates the supplied node as the root node.
 *
 * @return \$this
 *
 * @throws     PropelException
 */
public function makeRoot()
{
    if (\$this->getLeftValue() || \$this->getRightValue()) {
        throw new PropelException('Cannot turn an existing node into a root node.');
    }

    \$this->setLeftValue(1);
    \$this->setRightValue(2);
    \$this->setLevel(0);

    return \$this;
}
";
    }

    /**
     * @param string $script
     *
     * @return void
     */
    protected function addIsInTree(string &$script): void
    {
        $script .= "
/**
 * Tests if object is a node, i.e. if it is inserted in the tree
 *
 * @return bool
 */
public function isInTree(): bool
{
    return \$this->getLeftValue() > 0 && \$this->getRightValue() > \$this->getLeftValue();
}
";
    }

    /**
     * @param string $script
     *
     * @return void
     */
    protected function addIsRoot(string &$script): void
    {
        $script .= "
/**
 * Tests if node is a root
 *
 * @return bool
 */
public function isRoot(): bool
{
    return \$this->isInTree() && \$this->getLeftValue() == 1;
}
";
    }

    /**
     * @param string $script
     *
     * @return void
     */
    protected function addIsLeaf(string &$script): void
    {
        $script .= "
/**
 * Tests if node is a leaf
 *
 * @return bool
 */
public function isLeaf(): bool
{
    return \$this->isInTree() &&  (\$this->getRightValue() - \$this->getLeftValue()) == 1;
}
";
    }

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

        $script .= "
/**
 * Tests if node is a descendant of another node
 *
 * @param $objectClassName \$parent Propel node object
 *
 * @return bool
 */
public function isDescendantOf($objectClassName \$parent): bool
{";
        if ($this->behavior->useScope()) {
            $script .= "
    if (\$this->getScopeValue() !== \$parent->getScopeValue()) {
        return false; //since the `this` and \$parent are in different scopes, there's no way that `this` is be a descendant of \$parent.
    }
";
        }
        $script .= "
    return \$this->isInTree() && \$this->getLeftValue() > \$parent->getLeftValue() && \$this->getRightValue() < \$parent->getRightValue();
}
";
    }

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

        $script .= "
/**
 * Tests if node is a ancestor of another node
 *
 * @param $objectClassName \$child Propel node object
 *
 * @return bool
 */
public function isAncestorOf($objectClassName \$child): bool
{
    return \$child->isDescendantOf(\$this);
}
";
    }

    /**
     * @param string $script
     *
     * @return void
     */
    protected function addHasParent(string &$script): void
    {
        $script .= "
/**
 * Tests if object has an ancestor
 *
 * @return bool
 */
public function hasParent(): bool
{
    return \$this->getLevel() > 0;
}
";
    }

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

        $script .= "
/**
 * Sets the cache for parent node of the current object.
 * Warning: this does not move the current object in the tree.
 * Use moveTofirstChildOf() or moveToLastChildOf() for that purpose
 *
 * @param $objectClassName \$parent
 *
 * @return \$this
 */
public function setParent(?$objectClassName \$parent = null)
{
    \$this->aNestedSetParent = \$parent;

    return \$this;
}
";
    }

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

        $script .= "
/**
 * Gets parent node for the current object if it exists
 * The result is cached so further calls to the same method don't issue any queries
 *
 * @param \Propel\Runtime\Connection\ConnectionInterface \$con Connection to use.
 *
 * @return $objectClassName|null Propel object if exists else null
 */
public function getParent(?ConnectionInterface \$con = null)
{
    if (null === \$this->aNestedSetParent && \$this->hasParent()) {
        \$this->aNestedSetParent = {$queryClassName}::create()
            ->ancestorsOf(\$this)
            ->orderByLevel(true)
            ->findOne(\$con);
    }

    return \$this->aNestedSetParent;
}
";
    }

    /**
     * @param string $script
     *
     * @return void
     */
    protected function addHasPrevSibling(string &$script): void
    {
        $queryClassName = $this->builder->getQueryClassName();

        $script .= "
/**
 * Determines if the node has previous sibling
 *
 * @param \Propel\Runtime\Connection\ConnectionInterface \$con Connection to use.
 *
 * @return bool
 */
public function hasPrevSibling(?ConnectionInterface \$con = null): bool
{
    if (!{$queryClassName}::isValid(\$this)) {
        return false;
    }

    return $queryClassName::create()
        ->filterBy" . $this->getColumnPhpName('right_column') . '($this->getLeftValue() - 1)';
        if ($this->behavior->useScope()) {
            $script .= "
        ->inTree(\$this->getScopeValue())";
        }
        $script .= "
        ->exists(\$con);
}
";
    }

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

        $script .= "
/**
 * Gets previous sibling for the given node if it exists
 *
 * @param \Propel\Runtime\Connection\ConnectionInterface \$con Connection to use.
 *
 * @return $objectClassName|null         Propel object if exists else null
 */
public function getPrevSibling(?ConnectionInterface \$con = null)
{
    return $queryClassName::create()
        ->filterBy" . $this->getColumnPhpName('right_column') . '($this->getLeftValue() - 1)';
        if ($this->behavior->useScope()) {
            $script .= "
        ->inTree(\$this->getScopeValue())";
        }
        $script .= "
        ->findOne(\$con);
}
";
    }

    /**
     * @param string $script
     *
     * @return void
     */
    protected function addHasNextSibling(string &$script): void
    {
        $queryClassName = $this->builder->getQueryClassName();

        $script .= "
/**
 * Determines if the node has next sibling
 *
 * @param \Propel\Runtime\Connection\ConnectionInterface \$con Connection to use.
 * @return bool
 */
public function hasNextSibling(?ConnectionInterface \$con = null): bool
{
    if (!{$queryClassName}::isValid(\$this)) {
        return false;
    }

    return $queryClassName::create()
        ->filterBy" . $this->getColumnPhpName('left_column') . '($this->getRightValue() + 1)';
        if ($this->behavior->useScope()) {
            $script .= "
        ->inTree(\$this->getScopeValue())";
        }
        $script .= "
        ->exists(\$con);
}
";
    }

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

        $script .= "
/**
 * Gets next sibling for the given node if it exists
 *
 * @param \Propel\Runtime\Connection\ConnectionInterface \$con Connection to use.
 * @return $objectClassName|null         Propel object if exists else null
 */
public function getNextSibling(?ConnectionInterface \$con = null)
{
    return $queryClassName::create()
        ->filterBy" . $this->getColumnPhpName('left_column') . '($this->getRightValue() + 1)';
        if ($this->behavior->useScope()) {
            $script .= "
        ->inTree(\$this->getScopeValue())";
        }
        $script .= "
        ->findOne(\$con);
}
";
    }

    /**
     * @param string $script
     *
     * @return void
     */
    protected function addNestedSetChildrenClear(string &$script): void
    {
        $script .= "
/**
 * Clears out the \$collNestedSetChildren collection
 *
 * This does not modify the database; however, it will remove any associated objects, causing
 * them to be refetched by subsequent calls to accessor method.
 *
 * @return void
 */
public function clearNestedSetChildren(): void
{
    \$this->collNestedSetChildren = null;
}
";
    }

    /**
     * @param string $script
     *
     * @return void
     */
    protected function addNestedSetChildrenInit(string &$script): void
    {
        $script .= "
/**
 * Initializes the \$collNestedSetChildren collection.
 *
 * @return void
 */
public function initNestedSetChildren(): void
{
    \$collectionClassName = " . $this->builder->getNewTableMapBuilder($this->table)->getFullyQualifiedClassName() . "::getTableMap()->getCollectionClassName();

    \$this->collNestedSetChildren = new \$collectionClassName;
    \$this->collNestedSetChildren->setModel('" . $this->builder->getNewStubObjectBuilder($this->table)->getFullyQualifiedClassName() . "');
}
";
    }

    /**
     * @param string $script
     *
     * @return void
     */
    protected function addNestedSetChildAdd(string &$script): void
    {
        $objectClassName = $this->builder->getObjectClassName();
        $objectName = '$' . $this->table->getCamelCaseName();

        $script .= "
/**
 * Adds an element to the internal \$collNestedSetChildren collection.
 * Beware that this doesn't insert a node in the tree.
 * This method is only used to facilitate children hydration.
 *
 * @param $objectClassName $objectName
 *
 * @return void
 */
public function addNestedSetChild($objectClassName $objectName): void
{
    if (null === \$this->collNestedSetChildren) {
        \$this->initNestedSetChildren();
    }
    if (!in_array($objectName, \$this->collNestedSetChildren->getArrayCopy(), true)) { // only add it if the **same** object is not already associated
        \$this->collNestedSetChildren[] = $objectName;
        {$objectName}->setParent(\$this);
    }
}
";
    }

    /**
     * @param string $script
     *
     * @return void
     */
    protected function addHasChildren(string &$script): void
    {
        $script .= "
/**
 * Tests if node has children
 *
 * @return bool
 */
public function hasChildren(): bool
{
    return (\$this->getRightValue() - \$this->getLeftValue()) > 1;
}
";
    }

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

        $script .= "
/**
 * Gets the children of the given node
 *
 * @param \Propel\Runtime\ActiveQuery\Criteria \$criteria Criteria to filter results.
 * @param \Propel\Runtime\Connection\ConnectionInterface \$con Connection to use.
 *
 * @return ObjectCollection|{$objectClassName}[] List of $objectClassName objects
 */
public function getChildren(?Criteria \$criteria = null, ?ConnectionInterface \$con = null)
{
    if (null === \$this->collNestedSetChildren || null !== \$criteria) {
        if (\$this->isLeaf() || (\$this->isNew() && null === \$this->collNestedSetChildren)) {
            // return empty collection
            \$this->initNestedSetChildren();
        } else {
            \$collNestedSetChildren = $queryClassName::create(null, \$criteria)
                ->childrenOf(\$this)
                ->orderByBranch()
                ->find(\$con);
            if (null !== \$criteria) {
                return \$collNestedSetChildren;
            }
            \$this->collNestedSetChildren = \$collNestedSetChildren;
        }
    }

    return \$this->collNestedSetChildren;
}
";
    }

    /**
     * @param string $script
     *
     * @return void
     */
    protected function addCountChildren(string &$script): void
    {
        $queryClassName = $this->builder->getQueryClassName();

        $script .= "
/**
 * Gets number of children for the given node
 *
 * @param \Propel\Runtime\ActiveQuery\Criteria \$criteria Criteria to filter results.
 * @param \Propel\Runtime\Connection\ConnectionInterface \$con Connection to use.
 *
 * @return int Number of children
 */
public function countChildren(?Criteria \$criteria = null, ?ConnectionInterface \$con = null)
{
    if (null === \$this->collNestedSetChildren || null !== \$criteria) {
        if (\$this->isLeaf() || (\$this->isNew() && null === \$this->collNestedSetChildren)) {
            return 0;
        } else {
            return $queryClassName::create(null, \$criteria)
                ->childrenOf(\$this)
                ->count(\$con);
        }
    } else {
        return count(\$this->collNestedSetChildren);
    }
}
";
    }

    /**
     * @param string $script
     *
     * @return void
     */
    protected function addGetFirstChild(string &$script): void
    {
        $objectClassName = $this->builder->getObjectClassName();
        $queryClassName = $this->builder->getQueryClassName();
        $script .= "
/**
 * Gets the first child of the given node
 *
 * @param \Propel\Runtime\ActiveQuery\Criteria \$criteria Criteria to filter results.
 * @param \Propel\Runtime\Connection\ConnectionInterface \$con Connection to use.
 *
 * @return $objectClassName|null First child or null if this is a leaf
 */
public function getFirstChild(?Criteria \$criteria = null, ?ConnectionInterface \$con = null)
{
    if (\$this->isLeaf()) {
        return null;
    } else {
        return $queryClassName::create(null, \$criteria)
            ->childrenOf(\$this)
            ->orderByBranch()
            ->findOne(\$con);
    }
}
";
    }

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

        $script .= "
/**
 * Gets the last child of the given node
 *
 * @param \Propel\Runtime\ActiveQuery\Criteria \$criteria Criteria to filter results.
 * @param \Propel\Runtime\Connection\ConnectionInterface \$con Connection to use.
 * @return $objectClassName|null Last child or null if this is a leaf
 */
public function getLastChild(?Criteria \$criteria = null, ?ConnectionInterface \$con = null)
{
    if (\$this->isLeaf()) {
        return null;
    } else {
        return $queryClassName::create(null, \$criteria)
            ->childrenOf(\$this)
            ->orderByBranch(true)
            ->findOne(\$con);
    }
}
";
    }

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

        $script .= "
/**
 * Gets the siblings of the given node
 *
 * @param bool \$includeNode Whether to include the current node or not
 * @param \Propel\Runtime\ActiveQuery\Criteria \$criteria Criteria to filter results.
 * @param \Propel\Runtime\Connection\ConnectionInterface \$con Connection to use.
 *
 * @return ObjectCollection|{$objectClassName}[] List of $objectClassName objects
 */
public function getSiblings(\$includeNode = false, ?Criteria \$criteria = null, ?ConnectionInterface \$con = null)
{
    if (\$this->isRoot()) {
        return [];
    } else {
        \$query = $queryClassName::create(null, \$criteria)
            ->childrenOf(\$this->getParent(\$con))
            ->orderByBranch();
        if (!\$includeNode) {
            \$query->prune(\$this);
        }

        return \$query->find(\$con);
    }
}
";
    }

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

        $script .= "
/**
 * Gets descendants for the given node
 *
 * @param \Propel\Runtime\ActiveQuery\Criteria \$criteria Criteria to filter results.
 * @param \Propel\Runtime\Connection\ConnectionInterface \$con Connection to use.
 * @return ObjectCollection|{$objectClassName}[] List of $objectClassName objects
 */
public function getDescendants(?Criteria \$criteria = null, ?ConnectionInterface \$con = null)
{
    if (\$this->isLeaf()) {
        return [];
    } else {
        return $queryClassName::create(null, \$criteria)
            ->descendantsOf(\$this)
            ->orderByBranch()
            ->find(\$con);
    }
}
";
    }

    /**
     * @param string $script
     *
     * @return void
     */
    protected function addCountDescendants(string &$script): void
    {
        $queryClassName = $this->builder->getQueryClassName();

        $script .= "
/**
 * Gets number of descendants for the given node
 *
 * @param \Propel\Runtime\ActiveQuery\Criteria \$criteria Criteria to filter results.
 * @param \Propel\Runtime\Connection\ConnectionInterface \$con Connection to use.
 *
 * @return int Number of descendants
 */
public function countDescendants(?Criteria \$criteria = null, ?ConnectionInterface \$con = null)
{
    if (\$this->isLeaf()) {
        // save one query
        return 0;
    } else {
        return $queryClassName::create(null, \$criteria)
            ->descendantsOf(\$this)
            ->count(\$con);
    }
}
";
    }

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

        $script .= "
/**
 * Gets descendants for the given node, plus the current node
 *
 * @param \Propel\Runtime\ActiveQuery\Criteria \$criteria Criteria to filter results.
 * @param \Propel\Runtime\Connection\ConnectionInterface \$con Connection to use.
 * @return ObjectCollection|{$objectClassName}[] List of $objectClassName objects
 */
public function getBranch(?Criteria \$criteria = null, ?ConnectionInterface \$con = null)
{
    return $queryClassName::create(null, \$criteria)
        ->branchOf(\$this)
        ->orderByBranch()
        ->find(\$con);
}
";
    }

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

        $script .= "
/**
 * Gets ancestors for the given node, starting with the root node
 * Use it for breadcrumb paths for instance
 *
 * @param \Propel\Runtime\ActiveQuery\Criteria \$criteria Criteria to filter results.
 * @param \Propel\Runtime\Connection\ConnectionInterface \$con Connection to use.
 * @return ObjectCollection|{$objectClassName}[] List of $objectClassName objects
 */
public function getAncestors(?Criteria \$criteria = null, ?ConnectionInterface \$con = null)
{
    if (\$this->isRoot()) {
        // save one query
        return [];
    } else {
        return $queryClassName::create(null, \$criteria)
            ->ancestorsOf(\$this)
            ->orderByBranch()
            ->find(\$con);
    }
}
";
    }

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

        $script .= "
/**
 * Inserts the given \$child node as first child of current
 * The modifications in the current object and the tree
 * are not persisted until the child object is saved.
 *
 * @param $objectClassName \$child    Propel object for child node
 *
 * @return \$this The current Propel object
 */
public function addChild($objectClassName \$child)
{
    if (\$this->isNew()) {
        throw new PropelException('A $objectClassName object must not be new to accept children.');
    }
    \$child->insertAsFirstChildOf(\$this);

    return \$this;
}
";
    }

    /**
     * @param string $script
     *
     * @return void
     */
    protected function addInsertAsFirstChildOf(string &$script): void
    {
        $objectClassName = $this->builder->getObjectClassName();
        $queryClassName = $this->builder->getQueryClassName(true);
        $useScope = $this->behavior->useScope();

        $script .= "
/**
 * Inserts the current node as first child of given \$parent node
 * The modifications in the current object and the tree
 * are not persisted until the current object is saved.
 *
 * @param $objectClassName \$parent    Propel object for parent node
 *
 * @return \$this The current Propel object
 */
public function insertAsFirstChildOf($objectClassName \$parent)
{
    if (\$this->isInTree()) {
        throw new PropelException('A $objectClassName object must not already be in the tree to be inserted. Use the moveToFirstChildOf() instead.');
    }
    \$left = \$parent->getLeftValue() + 1;
    // Update node properties
    \$this->setLeftValue(\$left);
    \$this->setRightValue(\$left + 1);
    \$this->setLevel(\$parent->getLevel() + 1);";

        if ($useScope) {
            $script .= "
    \$scope = \$parent->getScopeValue();
    \$this->setScopeValue(\$scope);";
        }

        $script .= "
    // update the children collection of the parent
    \$parent->addNestedSetChild(\$this);

    // Keep the tree modification query for the save() transaction
    \$this->nestedSetQueries[] = [
        'callable'  => array('$queryClassName', 'makeRoomForLeaf'),
        'arguments' => array(\$left" . ($useScope ? ', $scope' : '') . ", \$this->isNew() ? null : \$this)
    ];

    return \$this;
}
";
    }

    /**
     * @return string
     */
    protected function addInsertAsLastChildOf(): string
    {
        return $this->behavior->renderTemplate('objectInsertAsLastChildOf', [
            'objectClassName' => $this->builder->getObjectClassName(),
            'queryClassName' => $this->builder->getQueryClassName(true),
            'useScope' => $this->behavior->useScope(),
        ]);
    }

    /**
     * @param string $script
     *
     * @return void
     */
    protected function addInsertAsPrevSiblingOf(string &$script): void
    {
        $objectClassName = $this->builder->getObjectClassName();
        $queryClassName = $this->builder->getQueryClassName(true);
        $useScope = $this->behavior->useScope();

        $script .= "
/**
 * Inserts the current node as prev sibling given \$sibling node
 * The modifications in the current object and the tree
 * are not persisted until the current object is saved.
 *
 * @param $objectClassName \$sibling    Propel object for parent node
 *
 * @return \$this The current Propel object
 */
public function insertAsPrevSiblingOf($objectClassName \$sibling)
{
    if (\$this->isInTree()) {
        throw new PropelException('A $objectClassName object must not already be in the tree to be inserted. Use the moveToPrevSiblingOf() instead.');
    }
    \$left = \$sibling->getLeftValue();
    // Update node properties
    \$this->setLeftValue(\$left);
    \$this->setRightValue(\$left + 1);
    \$this->setLevel(\$sibling->getLevel());";
        if ($useScope) {
            $script .= "
    \$scope = \$sibling->getScopeValue();
    \$this->setScopeValue(\$scope);";
        }
        $script .= "
    // Keep the tree modification query for the save() transaction
    \$this->nestedSetQueries [] = [
        'callable'  => array('$queryClassName', 'makeRoomForLeaf'),
        'arguments' => array(\$left" . ($useScope ? ', $scope' : '') . ", \$this->isNew() ? null : \$this)
    ];

    return \$this;
}
";
    }

    /**
     * @param string $script
     *
     * @return void
     */
    protected function addInsertAsNextSiblingOf(string &$script): void
    {
        $objectClassName = $this->builder->getObjectClassName();
        $queryClassName = $this->builder->getQueryClassName(true);
        $useScope = $this->behavior->useScope();

        $script .= "
/**
 * Inserts the current node as next sibling given \$sibling node
 * The modifications in the current object and the tree
 * are not persisted until the current object is saved.
 *
 * @param $objectClassName \$sibling    Propel object for parent node
 *
 * @return \$this The current Propel object
 */
public function insertAsNextSiblingOf($objectClassName \$sibling)
{
    if (\$this->isInTree()) {
        throw new PropelException('A $objectClassName object must not already be in the tree to be inserted. Use the moveToNextSiblingOf() instead.');
    }
    \$left = \$sibling->getRightValue() + 1;
    // Update node properties
    \$this->setLeftValue(\$left);
    \$this->setRightValue(\$left + 1);
    \$this->setLevel(\$sibling->getLevel());";
        if ($useScope) {
            $script .= "
    \$scope = \$sibling->getScopeValue();
    \$this->setScopeValue(\$scope);";
        }
        $script .= "
    // Keep the tree modification query for the save() transaction
    \$this->nestedSetQueries [] = [
        'callable'  => ['$queryClassName', 'makeRoomForLeaf'],
        'arguments' => [\$left" . ($useScope ? ', $scope' : '') . ", \$this->isNew() ? null : \$this],
    ];

    return \$this;
}
";
    }

    /**
     * @param string $script
     *
     * @return void
     */
    protected function addMoveToFirstChildOf(string &$script): void
    {
        $objectClassName = $this->builder->getObjectClassName();
        $script .= "
/**
 * Moves current node and its subtree to be the first child of \$parent
 * The modifications in the current object and the tree are immediate
 *
 * @param $objectClassName \$parent    Propel object for parent node
 * @param \Propel\Runtime\Connection\ConnectionInterface \$con Connection to use.
 *
 * @return \$this The current Propel object
 */
public function moveToFirstChildOf($objectClassName \$parent, ?ConnectionInterface \$con = null)
{
    if (!\$this->isInTree()) {
        throw new PropelException('A $objectClassName object must be already in the tree to be moved. Use the insertAsFirstChildOf() instead.');
    }";

        $script .= "
    if (\$parent->isDescendantOf(\$this)) {
        throw new PropelException('Cannot move a node as child of one of its subtree nodes.');
    }

    \$this->moveSubtreeTo(\$parent->getLeftValue() + 1, \$parent->getLevel() - \$this->getLevel() + 1" . ($this->behavior->useScope() ? ', $parent->getScopeValue()' : '') . ", \$con);

    return \$this;
}
";
    }

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

        $script .= "
/**
 * Moves current node and its subtree to be the last child of \$parent
 * The modifications in the current object and the tree are immediate
 *
 * @param $objectClassName \$parent    Propel object for parent node
 * @param \Propel\Runtime\Connection\ConnectionInterface \$con Connection to use.
 *
 * @return \$this The current Propel object
 */
public function moveToLastChildOf($objectClassName \$parent, ?ConnectionInterface \$con = null)
{
    if (!\$this->isInTree()) {
        throw new PropelException('A $objectClassName object must be already in the tree to be moved. Use the insertAsLastChildOf() instead.');
    }";

        $script .= "
    if (\$parent->isDescendantOf(\$this)) {
        throw new PropelException('Cannot move a node as child of one of its subtree nodes.');
    }

    \$this->moveSubtreeTo(\$parent->getRightValue(), \$parent->getLevel() - \$this->getLevel() + 1" . ($this->behavior->useScope() ? ', $parent->getScopeValue()' : '') . ", \$con);

    return \$this;
}
";
    }

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

        $script .= "
/**
 * Moves current node and its subtree to be the previous sibling of \$sibling
 * The modifications in the current object and the tree are immediate
 *
 * @param $objectClassName \$sibling    Propel object for sibling node
 * @param \Propel\Runtime\Connection\ConnectionInterface \$con Connection to use.
 *
 * @return \$this The current Propel object
 */
public function moveToPrevSiblingOf($objectClassName \$sibling, ?ConnectionInterface \$con = null)
{
    if (!\$this->isInTree()) {
        throw new PropelException('A $objectClassName object must be already in the tree to be moved. Use the insertAsPrevSiblingOf() instead.');
    }
    if (\$sibling->isRoot()) {
        throw new PropelException('Cannot move to previous sibling of a root node.');
    }";

        $script .= "
    if (\$sibling->isDescendantOf(\$this)) {
        throw new PropelException('Cannot move a node as sibling of one of its subtree nodes.');
    }

    \$this->moveSubtreeTo(\$sibling->getLeftValue(), \$sibling->getLevel() - \$this->getLevel()" . ($this->behavior->useScope() ? ', $sibling->getScopeValue()' : '') . ", \$con);

    return \$this;
}
";
    }

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

        $script .= "
/**
 * Moves current node and its subtree to be the next sibling of \$sibling
 * The modifications in the current object and the tree are immediate
 *
 * @param $objectClassName \$sibling    Propel object for sibling node
 * @param \Propel\Runtime\Connection\ConnectionInterface \$con Connection to use.
 *
 * @return \$this The current Propel object
 */
public function moveToNextSiblingOf($objectClassName \$sibling, ?ConnectionInterface \$con = null)
{
    if (!\$this->isInTree()) {
        throw new PropelException('A $objectClassName object must be already in the tree to be moved. Use the insertAsNextSiblingOf() instead.');
    }
    if (\$sibling->isRoot()) {
        throw new PropelException('Cannot move to next sibling of a root node.');
    }";

        $script .= "
    if (\$sibling->isDescendantOf(\$this)) {
        throw new PropelException('Cannot move a node as sibling of one of its subtree nodes.');
    }

    \$this->moveSubtreeTo(\$sibling->getRightValue() + 1, \$sibling->getLevel() - \$this->getLevel()" . ($this->behavior->useScope() ? ', $sibling->getScopeValue()' : '') . ", \$con);

    return \$this;
}
";
    }

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

        $script .= "
/**
 * Move current node and its children to location \$destLeft and updates rest of tree
 *
 * @param int \$destLeft Destination left value
 * @param int \$levelDelta Delta to add to the levels
 * @param \Propel\Runtime\Connection\ConnectionInterface \$con Connection to use.
 */
protected function moveSubtreeTo(\$destLeft, \$levelDelta" . ($this->behavior->useScope() ? ', $targetScope = null' : '') . ", ?ConnectionInterface \$con = null)
{
    \$left  = \$this->getLeftValue();
    \$right = \$this->getRightValue();";

        if ($useScope) {
            $script .= "
    \$scope = \$this->getScopeValue();

    if (\$targetScope === null) {
        \$targetScope = \$scope;
    }";
        }

        $script .= "

    \$treeSize = \$right - \$left +1;

    if (null === \$con) {
        \$con = Propel::getServiceContainer()->getWriteConnection($tableMapClass::DATABASE_NAME);
    }

    \$con->transaction(function () use (\$con, \$treeSize, \$destLeft, \$left, \$right, \$levelDelta" . ($useScope ? ', $scope, $targetScope' : '') . ") {
        \$preventDefault = false;

        // make room next to the target for the subtree
        $queryClassName::shiftRLValues(\$treeSize, \$destLeft, null" . ($useScope ? ', $targetScope' : '') . ", \$con);
";

        if ($useScope) {
            $script .= "
        if (\$targetScope != \$scope) {

            //move subtree to < 0, so the items are out of scope.
            $queryClassName::shiftRLValues(-\$right, \$left, \$right, \$scope, \$con);

            //update scopes
            $queryClassName::setNegativeScope(\$targetScope, \$con);

            //update levels
            $queryClassName::shiftLevel(\$levelDelta, \$left - \$right, 0, \$targetScope, \$con);

            //move the subtree to the target
            $queryClassName::shiftRLValues((\$right - \$left) + \$destLeft, \$left - \$right, 0, \$targetScope, \$con);

            \$preventDefault = true;
        }
";
        }

        $script .= "
        if (!\$preventDefault) {

            if (\$left >= \$destLeft) { // src was shifted too?
                \$left += \$treeSize;
                \$right += \$treeSize;
            }

            if (\$levelDelta) {
                // update the levels of the subtree
                $queryClassName::shiftLevel(\$levelDelta, \$left, \$right" . ($useScope ? ', $scope' : '') . ", \$con);
            }

            // move the subtree to the target
            $queryClassName::shiftRLValues(\$destLeft - \$left, \$left, \$right" . ($useScope ? ', $scope' : '') . ", \$con);
        }
";

        $script .= "
        // remove the empty room at the previous location of the subtree
        $queryClassName::shiftRLValues(-\$treeSize, \$right + 1, null" . ($useScope ? ', $scope' : '') . ", \$con);

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

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

        $script .= "
/**
 * Deletes all descendants for the given node
 * Instance pooling is wiped out by this command,
 * so existing $objectClassName instances are probably invalid (except for the current one)
 *
 * @param \Propel\Runtime\Connection\ConnectionInterface \$con Connection to use.
 *
 * @return int number of deleted nodes
 */
public function deleteDescendants(?ConnectionInterface \$con = null)
{
    if (\$this->isLeaf()) {
        // save one query
        return;
    }
    if (null === \$con) {
        \$con = Propel::getServiceContainer()->getReadConnection($tableMapClass::DATABASE_NAME);
    }
    \$left = \$this->getLeftValue();
    \$right = \$this->getRightValue();";
        if ($useScope) {
            $script .= "
    \$scope = \$this->getScopeValue();";
        }
        $script .= "

    return \$con->transaction(function () use (\$con, \$left, \$right" . ($useScope ? ', $scope' : '') . ") {
        // delete descendant nodes (will empty the instance pool)
        \$ret = $queryClassName::create()
            ->descendantsOf(\$this)
            ->delete(\$con);

        // fill up the room that was used by descendants
        $queryClassName::shiftRLValues(\$left - \$right + 1, \$right, null" . ($useScope ? ', $scope' : '') . ", \$con);

        // fix the right value for the current node, which is now a leaf
        \$this->setRightValue(\$left + 1);

        return \$ret;
    });
}
";
    }

    /**
     * @param \Propel\Generator\Builder\Om\ObjectBuilder $builder
     *
     * @return string
     */
    public function objectAttributes(ObjectBuilder $builder): string
    {
        $tableName = $this->table->getName();
        $objectClassName = $builder->getObjectClassName();

        $script = "
/**
 * Queries to be executed in the save transaction
 *
 * @var array
 */
protected \$nestedSetQueries = [];

/**
 * Internal cache for children nodes
 *
 * @var  null|ObjectCollection
 */
protected \$collNestedSetChildren = null;

/**
 * Internal cache for parent node
 *
 * @var  null|$objectClassName
 */
protected \$aNestedSetParent = null;

/**
 * Left column for the set
 */
const LEFT_COL = '" . $tableName . '.' . $this->behavior->getColumnConstant('left_column') . "';

/**
 * Right column for the set
 */
const RIGHT_COL = '" . $tableName . '.' . $this->behavior->getColumnConstant('right_column') . "';

/**
 * Level column for the set
 */
const LEVEL_COL = '" . $tableName . '.' . $this->behavior->getColumnConstant('level_column') . "';
";

        if ($this->behavior->useScope()) {
            $script .= "
/**
 * Scope column for the set
 */
const SCOPE_COL = '" . $tableName . '.' . $this->behavior->getColumnConstant('scope_column') . "';
";
        }

        return $script;
    }

    /**
     * @return string
     */
    protected function addGetIterator(): string
    {
        return $this->behavior->renderTemplate('objectGetIterator');
    }
}
