<?php
/**
 * nestedTree class is based on excellent work of Kuzma Feskov
 * (http://php.russofile.ru/ru/authors/sql/nestedsets01/)
 *
 * One day we'll move nestedTree to Clearbricks.
 *
 * @package Dotclear
 * @subpackage Core
 *
 * @copyright Olivier Meunier & Association Dotclear
 * @copyright GPL-2.0-only
 *
 */

if (!defined('DC_RC_PATH')) {return;}

class dcCategories extends nestedTree
{
    protected $f_left  = 'cat_lft';
    protected $f_right = 'cat_rgt';
    protected $f_id    = 'cat_id';

    protected $core;
    protected $blog_id;

    public function __construct($core)
    {
        $this->core          = &$core;
        $this->con           = &$core->con;
        $this->blog_id       = $core->blog->id;
        $this->table         = $core->prefix . 'category';
        $this->add_condition = ['blog_id' => "'" . $this->con->escape($this->blog_id) . "'"];
    }

    public function getChildren($start = 0, $id = null, $sort = 'asc', $fields = [])
    {
        $fields = array_merge(['cat_title', 'cat_url', 'cat_desc'], $fields);
        return parent::getChildren($start, $id, $sort, $fields);
    }

    public function getParents($id, $fields = [])
    {
        $fields = array_merge(['cat_title', 'cat_url', 'cat_desc'], $fields);
        return parent::getParents($id, $fields);
    }

    public function getParent($id, $fields = [])
    {
        $fields = array_merge(['cat_title', 'cat_url', 'cat_desc'], $fields);
        return parent::getParent($id, $fields);
    }
}

abstract class nestedTree
{
    protected $con;

    protected $table;
    protected $f_left;
    protected $f_right;
    protected $f_id;

    protected $add_condition = [];

    protected $parents;

    public function __construct($con)
    {
        $this->con = &$con;
    }

    public function getChildren($start = 0, $id = null, $sort = 'asc', $fields = [])
    {
        $fields = count($fields) > 0 ? ', C2.' . implode(', C2.', $fields) : '';

        $sql = 'SELECT C2.' . $this->f_id . ', C2.' . $this->f_left . ', C2.' . $this->f_right . ', COUNT(C1.' . $this->f_id . ') AS level '
        . $fields . ' '
        . 'FROM ' . $this->table . ' AS C1, ' . $this->table . ' AS C2 %s '
        . 'WHERE C2.' . $this->f_left . ' BETWEEN C1.' . $this->f_left . ' AND C1.' . $this->f_right . ' '
        . ' %s '
        . $this->getCondition('AND', 'C2.')
        . $this->getCondition('AND', 'C1.')
        . 'GROUP BY C2.' . $this->f_id . ', C2.' . $this->f_left . ', C2.' . $this->f_right . ' ' . $fields . ' '
        . ' %s '
        . 'ORDER BY C2.' . $this->f_left . ' ' . ($sort == 'asc' ? 'ASC' : 'DESC') . ' ';

        $from = $where = '';
        if ($start > 0) {
            $from  = ', ' . $this->table . ' AS C3';
            $where = 'AND C3.' . $this->f_id . ' = ' . (integer) $start . ' AND C1.' . $this->f_left . ' >= C3.' . $this->f_left . ' AND C1.' . $this->f_right . ' <= C3.' . $this->f_right;
            $where .= $this->getCondition('AND', 'C3.');
        }

        $having = '';
        if ($id !== null) {
            $having = ' HAVING C2.' . $this->f_id . ' = ' . (integer) $id;
        }

        $sql = sprintf($sql, $from, $where, $having);

        return $this->con->select($sql);
    }

    public function getParents($id, $fields = [])
    {
        $fields = count($fields) > 0 ? ', C1.' . implode(', C1.', $fields) : '';

        return $this->con->select(
            'SELECT C1.' . $this->f_id . ' ' . $fields . ' '
            . 'FROM ' . $this->table . ' C1, ' . $this->table . ' C2 '
            . 'WHERE C2.' . $this->f_id . ' = ' . (integer) $id . ' '
            . 'AND C1.' . $this->f_left . ' < C2.' . $this->f_left . ' '
            . 'AND C1.' . $this->f_right . ' > C2.' . $this->f_right . ' '
            . $this->getCondition('AND', 'C2.')
            . $this->getCondition('AND', 'C1.')
            . 'ORDER BY C1.' . $this->f_left . ' ASC '
        );
    }

    public function getParent($id, $fields = [])
    {
        $fields = count($fields) > 0 ? ', C1.' . implode(', C1.', $fields) : '';

        return $this->con->select(
            'SELECT C1.' . $this->f_id . ' ' . $fields . ' '
            . 'FROM ' . $this->table . ' C1, ' . $this->table . ' C2 '
            . 'WHERE C2.' . $this->f_id . ' = ' . (integer) $id . ' '
            . 'AND C1.' . $this->f_left . ' < C2.' . $this->f_left . ' '
            . 'AND C1.' . $this->f_right . ' > C2.' . $this->f_right . ' '
            . $this->getCondition('AND', 'C2.')
            . $this->getCondition('AND', 'C1.')
            . 'ORDER BY C1.' . $this->f_left . ' DESC '
            . $this->con->limit(1)
        );
    }

    /* ------------------------------------------------
     * Tree manipulations
     * ---------------------------------------------- */
    public function addNode($data, $target = 0)
    {
        if (!is_array($data) && !($data instanceof cursor)) {
            throw new Exception('Invalid data block');
        }

        if (is_array($data)) {
            $D    = $data;
            $data = $this->con->openCursor($this->table);
            foreach ($D as $k => $v) {
                $data->{$k} = $v;
            }
            unset($D);
        }

        # We want to put it at the end
        $this->con->writeLock($this->table);
        try
        {
            $rs = $this->con->select('SELECT MAX(' . $this->f_id . ') as n_id FROM ' . $this->table);
            $id = $rs->n_id;

            $rs = $this->con->select(
                'SELECT MAX(' . $this->f_right . ') as n_r ' .
                'FROM ' . $this->table .
                $this->getCondition('WHERE')
            );
            $last = $rs->n_r == 0 ? 1 : $rs->n_r;

            $data->{$this->f_id}    = $id + 1;
            $data->{$this->f_left}  = $last + 1;
            $data->{$this->f_right} = $last + 2;

            $data->insert();
            $this->con->unlock();
            try {
                $this->setNodeParent($id + 1, $target);
                return $data->{$this->f_id};
            } catch (Exception $e) {} # We don't mind error in this case
        } catch (Exception $e) {
            $this->con->unlock();
            throw $e;
        }
    }

    public function updatePosition($id, $left, $right)
    {
        $node_left  = (integer) $left;
        $node_right = (integer) $right;
        $node_id    = (integer) $id;
        $sql        = 'UPDATE ' . $this->table . ' SET '
        . $this->f_left . ' = ' . $node_left . ', '
        . $this->f_right . ' = ' . $node_right
        . ' WHERE ' . $this->f_id . ' = ' . $node_id
        . $this->getCondition();

        $this->con->begin();
        try {
            $this->con->execute($sql);
            $this->con->commit();
        } catch (Exception $e) {
            $this->con->rollback();
            throw $e;
        }
    }

    public function deleteNode($node, $keep_children = true)
    {
        $node = (integer) $node;

        $rs = $this->getChildren(0, $node);
        if ($rs->isEmpty()) {
            throw new Exception('Node does not exist.');
        }
        $node_left  = (integer) $rs->{$this->f_left};
        $node_right = (integer) $rs->{$this->f_right};

        try
        {
            $this->con->begin();

            if ($keep_children) {
                $this->con->execute('DELETE FROM ' . $this->table . ' WHERE ' . $this->f_id . ' = ' . $node);

                $sql = 'UPDATE ' . $this->table . ' SET '
                . $this->f_right . ' = CASE '
                . 'WHEN ' . $this->f_right . ' BETWEEN ' . $node_left . ' AND ' . $node_right . ' '
                . 'THEN ' . $this->f_right . ' - 1 '
                . 'WHEN ' . $this->f_right . ' > ' . $node_right . ' '
                . 'THEN ' . $this->f_right . ' - 2 '
                . 'ELSE ' . $this->f_right . ' '
                . 'END, '
                . $this->f_left . ' = CASE '
                . 'WHEN ' . $this->f_left . ' BETWEEN ' . $node_left . ' AND ' . $node_right . ' '
                . 'THEN ' . $this->f_left . ' - 1 '
                . 'WHEN ' . $this->f_left . ' > ' . $node_right . ' '
                . 'THEN ' . $this->f_left . ' - 2 '
                . 'ELSE ' . $this->f_left . ' '
                . 'END '
                . 'WHERE ' . $this->f_right . ' > ' . $node_left
                . $this->getCondition();

                $this->con->execute($sql);
            } else {
                $this->con->execute('DELETE FROM ' . $this->table . ' WHERE ' . $this->f_left . ' BETWEEN ' . $node_left . ' AND ' . $node_right);

                $node_delta = $node_right - $node_left + 1;
                $sql        = 'UPDATE ' . $this->table . ' SET '
                . $this->f_left . ' = CASE '
                . 'WHEN ' . $this->f_left . ' > ' . $node_left . ' '
                . 'THEN ' . $this->f_left . ' - (' . $node_delta . ') '
                . 'ELSE ' . $this->f_left . ' '
                . 'END, '
                . $this->f_right . ' = CASE '
                . 'WHEN ' . $this->f_right . ' > ' . $node_left . ' '
                . 'THEN ' . $this->f_right . ' - (' . $node_delta . ') '
                . 'ELSE ' . $this->f_right . ' '
                . 'END '
                . 'WHERE ' . $this->f_right . ' > ' . $node_right
                . $this->getCondition();
            }

            $this->con->commit();
        } catch (Exception $e) {
            $this->con->rollback();
            throw $e;
        }
    }

    public function resetOrder()
    {
        $rs = $this->con->select(
            'SELECT ' . $this->f_id . ' '
            . 'FROM ' . $this->table . ' '
            . $this->getCondition('WHERE')
            . 'ORDER BY ' . $this->f_left . ' ASC '
        );

        $lft = 2;
        $this->con->begin();
        try
        {
            while ($rs->fetch()) {
                $this->con->execute(
                    'UPDATE ' . $this->table . ' SET '
                    . $this->f_left . ' = ' . ($lft++) . ', '
                    . $this->f_right . ' = ' . ($lft++) . ' '
                    . 'WHERE ' . $this->f_id . ' = ' . (integer) $rs->{$this->f_id} . ' '
                    . $this->getCondition()
                );
            }
            $this->con->commit();
        } catch (Exception $e) {
            $this->con->rollback();
            throw $e;
        }
    }

    public function setNodeParent($node, $target = 0)
    {
        if ($node == $target) {
            return;
        }
        $node   = (integer) $node;
        $target = (integer) $target;

        $rs = $this->getChildren(0, $node);
        if ($rs->isEmpty()) {
            throw new Exception('Node does not exist.');
        }
        $node_left  = (integer) $rs->{$this->f_left};
        $node_right = (integer) $rs->{$this->f_right};
        $node_level = (integer) $rs->level;

        if ($target > 0) {
            $rs = $this->getChildren(0, $target);
        } else {
            $rs = $this->con->select(
                'SELECT MIN(' . $this->f_left . ')-1 AS ' . $this->f_left . ', MAX(' . $this->f_right . ')+1 AS ' . $this->f_right . ', 0 AS level ' .
                'FROM ' . $this->table . ' ' .
                $this->getCondition('WHERE')
            );
        }
        $target_left  = (integer) $rs->{$this->f_left};
        $target_right = (integer) $rs->{$this->f_right};
        $target_level = (integer) $rs->level;

        if ($node_left == $target_left
            || ($target_left >= $node_left && $target_left <= $node_right)
            || ($node_level == $target_level + 1 && $node_left > $target_left && $node_right < $target_right)
        ) {
            throw new Exception('Cannot move tree');
        }

        if ($target_left < $node_left && $target_right > $node_right && $target_level < $node_level - 1) {
            $sql = 'UPDATE ' . $this->table . ' SET '
            . $this->f_right . ' = CASE '
            . 'WHEN ' . $this->f_right . ' BETWEEN ' . ($node_right + 1) . ' AND ' . ($target_right - 1) . ' '
            . 'THEN ' . $this->f_right . '-(' . ($node_right - $node_left + 1) . ') '
            . 'WHEN ' . $this->f_left . ' BETWEEN ' . $node_left . ' AND ' . $node_right . ' '
            . 'THEN ' . $this->f_right . '+' . ((($target_right - $node_right - $node_level + $target_level) / 2) * 2 + $node_level - $target_level - 1) . ' '
            . 'ELSE '
            . $this->f_right . ' '
            . 'END, '
            . $this->f_left . ' = CASE '
            . 'WHEN ' . $this->f_left . ' BETWEEN ' . ($node_right + 1) . ' AND ' . ($target_right - 1) . ' '
            . 'THEN ' . $this->f_left . '-(' . ($node_right - $node_left + 1) . ') '
            . 'WHEN ' . $this->f_left . ' BETWEEN ' . $node_left . ' AND ' . $node_right . ' '
            . 'THEN ' . $this->f_left . '+' . ((($target_right - $node_right - $node_level + $target_level) / 2) * 2 + $node_level - $target_level - 1) . ' '
            . 'ELSE ' . $this->f_left . ' '
            . 'END '
            . 'WHERE ' . $this->f_left . ' BETWEEN ' . ($target_left + 1) . ' AND ' . ($target_right - 1) . '';
        } elseif ($target_left < $node_left) {
            $sql = 'UPDATE ' . $this->table . ' SET '
            . $this->f_left . ' = CASE '
            . 'WHEN ' . $this->f_left . ' BETWEEN ' . $target_right . ' AND ' . ($node_left - 1) . ' '
            . 'THEN ' . $this->f_left . '+' . ($node_right - $node_left + 1) . ' '
            . 'WHEN ' . $this->f_left . ' BETWEEN ' . $node_left . ' AND ' . $node_right . ' '
            . 'THEN ' . $this->f_left . '-(' . ($node_left - $target_right) . ') '
            . 'ELSE ' . $this->f_left . ' '
            . 'END, '
            . $this->f_right . ' = CASE '
            . 'WHEN ' . $this->f_right . ' BETWEEN ' . $target_right . ' AND ' . $node_left . ' '
            . 'THEN ' . $this->f_right . '+' . ($node_right - $node_left + 1) . ' '
            . 'WHEN ' . $this->f_right . ' BETWEEN ' . $node_left . ' AND ' . $node_right . ' '
            . 'THEN ' . $this->f_right . '-(' . ($node_left - $target_right) . ') '
            . 'ELSE ' . $this->f_right . ' '
            . 'END '
            . 'WHERE (' . $this->f_left . ' BETWEEN ' . $target_left . ' AND ' . $node_right . ' '
            . 'OR ' . $this->f_right . ' BETWEEN ' . $target_left . ' AND ' . $node_right . ')';
        } else {
            $sql = 'UPDATE ' . $this->table . ' SET '
            . $this->f_left . ' = CASE '
            . 'WHEN ' . $this->f_left . ' BETWEEN ' . $node_right . ' AND ' . $target_right . ' '
            . 'THEN ' . $this->f_left . '-' . ($node_right - $node_left + 1) . ' '
            . 'WHEN ' . $this->f_left . ' BETWEEN ' . $node_left . ' AND ' . $node_right . ' '
            . 'THEN ' . $this->f_left . '+' . ($target_right - 1 - $node_right) . ' '
            . 'ELSE ' . $this->f_left . ' '
            . 'END, '
            . $this->f_right . ' = CASE '
            . 'WHEN ' . $this->f_right . ' BETWEEN ' . ($node_right + 1) . ' AND ' . ($target_right - 1) . ' '
            . 'THEN ' . $this->f_right . '-' . ($node_right - $node_left + 1) . ' '
            . 'WHEN ' . $this->f_right . ' BETWEEN ' . $node_left . ' AND ' . $node_right . ' '
            . 'THEN ' . $this->f_right . '+' . ($target_right - 1 - $node_right) . ' '
            . 'ELSE ' . $this->f_right . ' '
            . 'END '
            . 'WHERE (' . $this->f_left . ' BETWEEN ' . $node_left . ' AND ' . $target_right . ' '
            . 'OR ' . $this->f_right . ' BETWEEN ' . $node_left . ' AND ' . $target_right . ')';
        }

        $sql .= ' ' . $this->getCondition();

        $this->con->execute($sql);
    }

    public function setNodePosition($nodeA, $nodeB, $position = 'after')
    {
        $nodeA = (integer) $nodeA;
        $nodeB = (integer) $nodeB;

        $rs = $this->getChildren(0, $nodeA);
        if ($rs->isEmpty()) {
            throw new Exception('Node does not exist.');
        }
        $A_left  = $rs->{$this->f_left};
        $A_right = $rs->{$this->f_right};
        $A_level = $rs->level;

        $rs = $this->getChildren(0, $nodeB);
        if ($rs->isEmpty()) {
            throw new Exception('Node does not exist.');
        }
        $B_left  = $rs->{$this->f_left};
        $B_right = $rs->{$this->f_right};
        $B_level = $rs->level;

        if ($A_level != $B_level) {
            throw new Exception('Cannot change position');
        }

        $rs      = $this->getParents($nodeA);
        $parentA = $rs->isEmpty() ? 0 : $rs->{$this->f_id};
        $rs      = $this->getParents($nodeB);
        $parentB = $rs->isEmpty() ? 0 : $rs->{$this->f_id};

        if ($parentA != $parentB) {
            throw new Exception('Cannot change position');
        }

        if ($position == 'before') {
            if ($A_left > $B_left) {
                $sql = 'UPDATE ' . $this->table . ' SET '
                . $this->f_right . ' = CASE WHEN ' . $this->f_left . ' BETWEEN ' . $A_left . ' AND ' . $A_right . ' THEN ' . $this->f_right . ' - (' . ($A_left - $B_left) . ') '
                . 'WHEN ' . $this->f_left . ' BETWEEN ' . $B_left . ' AND ' . ($A_left - 1) . ' THEN ' . $this->f_right . ' +  ' . ($A_right - $A_left + 1) . ' ELSE ' . $this->f_right . ' END, '
                . $this->f_left . ' = CASE WHEN ' . $this->f_left . ' BETWEEN ' . $A_left . ' AND ' . $A_right . ' THEN ' . $this->f_left . ' - (' . ($A_left - $B_left) . ') '
                . 'WHEN ' . $this->f_left . ' BETWEEN ' . $B_left . ' AND ' . ($A_left - 1) . ' THEN ' . $this->f_left . ' + ' . ($A_right - $A_left + 1) . ' ELSE ' . $this->f_left . ' END '
                . 'WHERE ' . $this->f_left . ' BETWEEN ' . $B_left . ' AND ' . $A_right;
            } else {
                $sql = 'UPDATE ' . $this->table . ' SET '
                . $this->f_right . ' = CASE WHEN ' . $this->f_left . ' BETWEEN ' . $A_left . ' AND ' . $A_right . ' THEN ' . $this->f_right . ' + ' . (($B_left - $A_left) - ($A_right - $A_left + 1)) . ' '
                . 'WHEN ' . $this->f_left . ' BETWEEN ' . ($A_right + 1) . ' AND ' . ($B_left - 1) . ' THEN ' . $this->f_right . ' - (' . (($A_right - $A_left + 1)) . ') ELSE ' . $this->f_right . ' END, '
                . $this->f_left . ' = CASE WHEN ' . $this->f_left . ' BETWEEN ' . $A_left . ' AND ' . $A_right . ' THEN ' . $this->f_left . ' + ' . (($B_left - $A_left) - ($A_right - $A_left + 1)) . ' '
                . 'WHEN ' . $this->f_left . ' BETWEEN ' . ($A_right + 1) . ' AND ' . ($B_left - 1) . ' THEN ' . $this->f_left . ' - (' . ($A_right - $A_left + 1) . ') ELSE ' . $this->f_left . ' END '
                . 'WHERE ' . $this->f_left . ' BETWEEN ' . $A_left . ' AND ' . ($B_left - 1);
            }
        } else {
            if ($A_left > $B_left) {
                $sql = 'UPDATE ' . $this->table . ' SET '
                . $this->f_right . ' = CASE WHEN ' . $this->f_left . ' BETWEEN ' . $A_left . ' AND ' . $A_right . ' THEN ' . $this->f_right . ' - (' . ($A_left - $B_left - ($B_right - $B_left + 1)) . ') '
                . 'WHEN ' . $this->f_left . ' BETWEEN ' . ($B_right + 1) . ' AND ' . ($A_left - 1) . ' THEN ' . $this->f_right . ' +  ' . ($A_right - $A_left + 1) . ' ELSE ' . $this->f_right . ' END, '
                . $this->f_left . ' = CASE WHEN ' . $this->f_left . ' BETWEEN ' . $A_left . ' AND ' . $A_right . ' THEN ' . $this->f_left . ' - (' . ($A_left - $B_left - ($B_right - $B_left + 1)) . ') '
                . 'WHEN ' . $this->f_left . ' BETWEEN ' . ($B_right + 1) . ' AND ' . ($A_left - 1) . ' THEN ' . $this->f_left . ' + ' . ($A_right - $A_left + 1) . ' ELSE ' . $this->f_left . ' END '
                . 'WHERE ' . $this->f_left . ' BETWEEN ' . ($B_right + 1) . ' AND ' . $A_right;
            } else {
                $sql = 'UPDATE ' . $this->table . ' SET '
                . $this->f_right . ' = CASE WHEN ' . $this->f_left . ' BETWEEN ' . $A_left . ' AND ' . $A_right . ' THEN ' . $this->f_right . ' + ' . ($B_right - $A_right) . ' '
                . 'WHEN ' . $this->f_left . ' BETWEEN ' . ($A_right + 1) . ' AND ' . $B_right . ' THEN ' . $this->f_right . ' - (' . (($A_right - $A_left + 1)) . ') ELSE ' . $this->f_right . ' END, '
                . $this->f_left . ' = CASE WHEN ' . $this->f_left . ' BETWEEN ' . $A_left . ' AND ' . $A_right . ' THEN ' . $this->f_left . ' + ' . ($B_right - $A_right) . ' '
                . 'WHEN ' . $this->f_left . ' BETWEEN ' . ($A_right + 1) . ' AND ' . $B_right . ' THEN ' . $this->f_left . ' - (' . ($A_right - $A_left + 1) . ') ELSE ' . $this->f_left . ' END '
                . 'WHERE ' . $this->f_left . ' BETWEEN ' . $A_left . ' AND ' . $B_right;
            }
        }

        $sql .= $this->getCondition();
        $this->con->execute($sql);
    }

    protected function getCondition($start = 'AND', $prefix = '')
    {
        if (empty($this->add_condition)) {
            return '';
        }

        $w = [];
        foreach ($this->add_condition as $c => $n) {
            $w[] = $prefix . $c . ' = ' . $n;
        }
        return ' ' . $start . ' ' . implode(' AND ', $w) . ' ';
    }
}
