<?php
/**
 * Copyright © Magento, Inc. All rights reserved.
 * See COPYING.txt for license details.
 */
namespace Magento\Framework\Data\Tree;

use Magento\Framework\DB\Select;

/**
 * Data DB tree
 *
 * Data model:
 * id  |  path  |  order
 *
 * @author      Magento Core Team <core@magentocommerce.com>
 */
class Dbp extends \Magento\Framework\Data\Tree
{
    public const ID_FIELD = 'id';

    public const PATH_FIELD = 'path';

    public const ORDER_FIELD = 'order';

    public const LEVEL_FIELD = 'level';

    /**
     * DB connection
     *
     * @var \Magento\Framework\DB\Adapter\AdapterInterface
     */
    protected $_conn;

    /**
     * Data table name
     *
     * @var string
     */
    protected $_table;

    /**
     * Indicates if loaded
     *
     * @var bool
     */
    protected $_loaded = false;

    /**
     * SQL select object
     *
     * @var \Magento\Framework\DB\Select
     */
    protected $_select;

    /**
     * Tree structure field: id
     *
     * @var string
     */
    protected $_idField;

    /**
     * Tree structure field: path
     *
     * @var string
     */
    protected $_pathField;

    /**
     * Tree structure field: order
     *
     * @var string
     */
    protected $_orderField;

    /**
     * Tree structure field: level
     *
     * @var string
     */
    protected $_levelField;

    /**
     * Db tree constructor
     *
     * $fields = array(
     *      \Magento\Framework\Data\Tree\Dbp::ID_FIELD       => string,
     *      \Magento\Framework\Data\Tree\Dbp::PATH_FIELD     => string,
     *      \Magento\Framework\Data\Tree\Dbp::ORDER_FIELD    => string
     *      \Magento\Framework\Data\Tree\Dbp::LEVEL_FIELD    => string
     * )
     *
     * @param \Magento\Framework\DB\Adapter\AdapterInterface $connection
     * @param string $table
     * @param array $fields
     * @throws \Exception
     */
    public function __construct(\Magento\Framework\DB\Adapter\AdapterInterface $connection, $table, $fields)
    {
        parent::__construct();

        if (!$connection) {
            // phpcs:ignore Magento2.Exceptions.DirectThrow
            throw new \Exception('Wrong "$connection" parametr');
        }

        $this->_conn = $connection;
        $this->_table = $table;

        if (!isset(
            $fields[self::ID_FIELD]
        ) || !isset(
            $fields[self::PATH_FIELD]
        ) || !isset(
            $fields[self::LEVEL_FIELD]
        ) || !isset(
            $fields[self::ORDER_FIELD]
        )
        ) {
            // phpcs:ignore Magento2.Exceptions.DirectThrow
            throw new \Exception('"$fields" tree configuratin array');
        }

        $this->_idField = $fields[self::ID_FIELD];
        $this->_pathField = $fields[self::PATH_FIELD];
        $this->_orderField = $fields[self::ORDER_FIELD];
        $this->_levelField = $fields[self::LEVEL_FIELD];

        $this->_select = $this->_conn->select();
        $this->_select->from($this->_table);
    }

    /**
     * Retrieve current select object
     *
     * @return Select
     */
    public function getDbSelect()
    {
        return $this->_select;
    }

    /**
     * Set Select object
     *
     * @param Select $select
     * @return void
     */
    public function setDbSelect($select)
    {
        $this->_select = $select;
    }

    /**
     * Load tree
     *
     * @param   int|Node|string $parentNode
     * @param   int $recursionLevel
     * @return  $this
     */
    public function load($parentNode = null, $recursionLevel = 0)
    {
        if (!$this->_loaded) {
            $startLevel = 1;
            $parentPath = '';

            if ($parentNode instanceof Node) {
                $parentPath = $parentNode->getData($this->_pathField);
                $startLevel = $parentNode->getData($this->_levelField);
            } elseif (is_numeric($parentNode)) {
                $select = $this->_conn->select()
                    ->from($this->_table, [$this->_pathField, $this->_levelField])
                    ->where("{$this->_idField} = ?", $parentNode);
                $parent = $this->_conn->fetchRow($select);

                $startLevel = $parent[$this->_levelField];
                $parentPath = $parent[$this->_pathField];
                $parentNode = null;
            } elseif (is_string($parentNode)) {
                $parentPath = $parentNode;
                $startLevel = count(explode(',', $parentPath)) - 1;
                $parentNode = null;
            }

            $select = clone $this->_select;

            $select->order($this->_table . '.' . $this->_orderField . ' ASC');
            if ($parentPath) {
                $pathField = $this->_conn->quoteIdentifier([$this->_table, $this->_pathField]);
                $select->where("{$pathField} LIKE ?", "{$parentPath}/%");
            }
            if ($recursionLevel != 0) {
                $levelField = $this->_conn->quoteIdentifier([$this->_table, $this->_levelField]);
                $select->where("{$levelField} <= ?", $startLevel + $recursionLevel);
            }

            $arrNodes = $this->_conn->fetchAll($select);

            $childrenItems = [];

            foreach ($arrNodes as $nodeInfo) {
                $pathToParent = explode('/', $nodeInfo[$this->_pathField] ?? '');
                array_pop($pathToParent);
                $pathToParent = implode('/', $pathToParent);
                $childrenItems[$pathToParent][] = $nodeInfo;
            }

            $this->addChildNodes($childrenItems, $parentPath, $parentNode);

            $this->_loaded = true;
        }

        return $this;
    }

    /**
     * Add child nodes
     *
     * @param array $children
     * @param string $path
     * @param Node $parentNode
     * @param int $level
     * @return void
     */
    public function addChildNodes($children, $path, $parentNode, $level = 0)
    {
        if (isset($children[$path])) {
            foreach ($children[$path] as $child) {
                $nodeId = isset($child[$this->_idField]) ? $child[$this->_idField] : false;
                if ($parentNode && $nodeId && ($node = $parentNode->getChildren()->searchById($nodeId))) {
                    $node->addData($child);
                } else {
                    $node = new Node($child, $this->_idField, $this, $parentNode);
                }

                //$node->setLevel(count(explode('/', $node->getData($this->_pathField)))-1);
                $node->setLevel($node->getData($this->_levelField));
                $node->setPathId($node->getData($this->_pathField));
                $this->addNode($node, $parentNode);

                if ($path) {
                    $childrenPath = explode('/', $path);
                } else {
                    $childrenPath = [];
                }
                $childrenPath[] = $node->getId();
                $childrenPath = implode('/', $childrenPath);

                $this->addChildNodes($children, $childrenPath, $node, $level + 1);
            }
        }
    }

    /**
     * Load node
     *
     * @param int|string $nodeId
     * @return Node
     */
    public function loadNode($nodeId)
    {
        $select = clone $this->_select;
        if (is_numeric($nodeId)) {
            $condField = $this->_conn->quoteIdentifier([$this->_table, $this->_idField]);
        } else {
            $condField = $this->_conn->quoteIdentifier([$this->_table, $this->_pathField]);
        }

        $select->where("{$condField} = ?", $nodeId);

        $node = new Node($this->_conn->fetchRow($select), $this->_idField, $this);
        $this->addNode($node);
        return $node;
    }

    /**
     * Get children
     *
     * @param Node $node
     * @param bool $recursive
     * @param array $result
     * @return array
     */
    public function getChildren($node, $recursive = true, $result = [])
    {
        if (is_numeric($node)) {
            $node = $this->getNodeById($node);
        }
        if (!$node) {
            return $result;
        }

        foreach ($node->getChildren() as $child) {
            if ($recursive) {
                if ($child->getChildren()) {
                    $result = $this->getChildren($child, $recursive, $result);
                }
            }
            $result[] = $child->getId();
        }
        return $result;
    }

    /**
     * Move tree node
     *
     * @param Node $node
     * @param Node $newParent
     * @param Node $prevNode
     * @return void
     * @throws \Exception
     * @todo Use adapter for generate conditions
     */
    public function move($node, $newParent, $prevNode = null)
    {
        $position = 1;

        $oldPath = $node->getData($this->_pathField);
        $newPath = $newParent->getData($this->_pathField);

        $newPath = $newPath . '/' . $node->getId();
        $oldPathLength = $oldPath !== null ? strlen($oldPath) : 0;

        $newLevel = $newParent->getLevel() + 1;
        $levelDisposition = $newLevel - $node->getLevel();

        $data = [
            $this->_levelField => new \Zend_Db_Expr("{$this->_levelField} + '{$levelDisposition}'"),
            $this->_pathField => new \Zend_Db_Expr(
                "CONCAT('{$newPath}', RIGHT({$this->_pathField}, LENGTH({$this->_pathField}) - {$oldPathLength}))"
            ),
        ];
        $condition = $this->_conn->quoteInto("{$this->_pathField} REGEXP ?", "^{$oldPath}(/|\$)");

        $this->_conn->beginTransaction();

        $reorderData = [$this->_orderField => new \Zend_Db_Expr("{$this->_orderField} + 1")];
        try {
            if ($prevNode && $prevNode->getId()) {
                $reorderCondition = "{$this->_orderField} > {$prevNode->getData($this->_orderField)}";
                $position = $prevNode->getData($this->_orderField) + 1;
            } else {
                $reorderCondition = $this->_conn->quoteInto(
                    "{$this->_pathField} REGEXP ?",
                    "^{$newParent->getData($this->_pathField)}/[0-9]+\$"
                );
                $select = $this->_conn->select()->from(
                    $this->_table,
                    new \Zend_Db_Expr("MIN({$this->_orderField})")
                )->where(
                    $reorderCondition
                );

                $position = (int)$this->_conn->fetchOne($select);
            }
            $this->_conn->update($this->_table, $reorderData, $reorderCondition);
            $this->_conn->update($this->_table, $data, $condition);
            $this->_conn->update(
                $this->_table,
                [$this->_orderField => $position, $this->_levelField => $newLevel],
                $this->_conn->quoteInto("{$this->_idField} = ?", $node->getId())
            );

            $this->_conn->commit();
        } catch (\Exception $e) {
            $this->_conn->rollBack();
            // phpcs:ignore Magento2.Exceptions.DirectThrow
            throw new \Exception("Can't move tree node due to error: " . $e->getMessage());
        }
    }

    /**
     * Load ensured nodes
     *
     * @param object $category
     * @param Node $rootNode
     * @return void
     */
    public function loadEnsuredNodes($category, $rootNode)
    {
        $pathIds = $category->getPathIds();
        $rootNodeId = $rootNode->getId();
        $rootNodePath = $rootNode->getData($this->_pathField);

        $select = clone $this->_select;
        $select->order($this->_table . '.' . $this->_orderField . ' ASC');

        if ($pathIds) {
            $condition = $this->_conn->quoteInto("{$this->_table}.{$this->_idField} in (?)", $pathIds);
            $select->where($condition);
        }

        $arrNodes = $this->_conn->fetchAll($select);

        if ($arrNodes) {
            $childrenItems = [];
            foreach ($arrNodes as $nodeInfo) {
                $nodeId = $nodeInfo[$this->_idField];
                if ($nodeId <= $rootNodeId) {
                    continue;
                }

                $pathToParent = explode('/', $nodeInfo[$this->_pathField] ?? '');
                array_pop($pathToParent);
                $pathToParent = implode('/', $pathToParent);
                $childrenItems[$pathToParent][] = $nodeInfo;
            }

            $this->_addChildNodes($childrenItems, $rootNodePath, $rootNode, true);
        }
    }

    /**
     * Add child nodes
     *
     * @param array $children
     * @param string $path
     * @param Node $parentNode
     * @param bool $withChildren
     * @param int $level
     * @return void
     */
    protected function _addChildNodes($children, $path, $parentNode, $withChildren = false, $level = 0)
    {
        if (isset($children[$path])) {
            foreach ($children[$path] as $child) {
                $nodeId = isset($child[$this->_idField]) ? $child[$this->_idField] : false;
                if ($parentNode && $nodeId && ($node = $parentNode->getChildren()->searchById($nodeId))) {
                    $node->addData($child);
                } else {
                    $node = new Node($child, $this->_idField, $this, $parentNode);
                    $node->setLevel($node->getData($this->_levelField));
                    $node->setPathId($node->getData($this->_pathField));
                    $this->addNode($node, $parentNode);
                }

                if ($withChildren) {
                    $this->_loaded = false;
                    $node->loadChildren(1);
                    $this->_loaded = false;
                }

                if ($path) {
                    $childrenPath = explode('/', $path);
                } else {
                    $childrenPath = [];
                }
                $childrenPath[] = $node->getId();
                $childrenPath = implode('/', $childrenPath);

                $this->_addChildNodes($children, $childrenPath, $node, $withChildren, $level + 1);
            }
        }
    }
}
