<?php
/**
 * i-doit - Documentation and CMDB solution for IT environments
 *
 * This file is part of the i-doit framework. Modify at your own risk.
 *
 * Please visit http://www.i-doit.com/license for a full copyright and license information.
 *
 * @version     1.10
 * @package     i-doit
 * @author      synetics GmbH
 * @copyright   synetics GmbH
 * @url         http://www.i-doit.com
 * @license     http://www.i-doit.com/license
 */
/**
 * i-doit
 *
 * MPTT
 *
 * @package    i-doit
 * @subpackage Components
 * @author     Andre Woesten <awoesten@i-doit.de>
 * @version    1.0
 * @copyright  synetics GmbH
 * @license    http://www.i-doit.com/license
 */

/**
 * Notes:
 * ------
 * * Action stack ist eine Hash table, wobei als Index die Node-ID verwendet
 *   wird, um einen direkten Zusammenhang zwischen Stack und Node zu bilden.
 *   Ein Eintrag beinhaltet wiederrum ein Array mit den Aktionsdaten.
 * * Die Konstanten befinden sich im Konstantenmanager deklariert
 */
class isys_component_dao_mptt extends isys_component_dao
{
    private $m_actionstack;
    /**
     * Cache for method has_children
     *
     * @var array
     */
    private $m_child_cache = [];
    private $m_initialized;
    private $m_src_field_const;
    private $m_src_field_id;
    private $m_src_field_left;
    private $m_src_field_node;
    private $m_src_field_parent_id;
    private $m_src_field_right;
    private $m_src_tbl;

    public function get_next_node_as_integer()
    {
        $l_q = "SELECT " . "MAX({$this->m_src_field_node}) " . "FROM " . "{$this->m_src_tbl};";

        $l_nextdao = $this->retrieve($l_q);
        if ($l_nextdao->num_rows())
        {
            list($l_nodeid) = $l_nextdao->get_row(IDOIT_C__DAO_RESULT_TYPE_ROW);

            return $l_nodeid + 1;
        }

        return null;
    }

    public function get_by_const($p_const)
    {
        if (is_numeric($p_const))
        {
            $l_q = "SELECT " . "* " . "FROM " . "{$this->m_src_tbl} " . "WHERE " . "{$this->m_src_field_const}=" . "'" . $p_const . "'";

            return $this->retrieve($l_q);
        }

        return null;
    }

    /**
     * @param $p_nodeid
     *
     * @return isys_component_dao_result
     * @throws Exception
     * @throws isys_exception_database
     */
    public function get_by_node_id($p_nodeid)
    {
        $l_q = "SELECT * FROM " . "{$this->m_src_tbl} " . "INNER JOIN isys_obj " . "ON isys_obj__id = " . "{$this->m_src_field_node} " . "WHERE " . "{$this->m_src_field_node}=" . (int) $p_nodeid . " " . "ORDER BY isys_obj__title";

        return $this->retrieve($l_q);

    }

    /**
     * @param $p_parentid
     *
     * @return isys_component_dao_result
     * @throws Exception
     * @throws isys_exception_database
     */
    public function get_by_parent_id($p_parentid)
    {
        $l_q = "SELECT " . "* " . "FROM " . "{$this->m_src_tbl} " . "WHERE " . "{$this->m_src_field_parent_id}=" . (int) $p_parentid;

        return $this->retrieve($l_q);
    }

    /**
     * Get all children of $p_parent_id, this includes an entry of the node itself ($p_parent_id) as well
     *
     * @param $p_parent_id
     *
     * @author Dennis Stücken <dstuecken@i-doit.de>
     *
     * @return isys_component_dao_result|null
     */
    public function get_tree($p_parent_id)
    {
        $l_node = $this->get_by_node_id($p_parent_id)
            ->get_row();

        if ($l_node && isset($l_node[$this->m_src_field_left]) && isset($l_node[$this->m_src_field_right]))
        {
            return $this->get_left_by_left_right($l_node[$this->m_src_field_left], $l_node[$this->m_src_field_right], C__RECORD_STATUS__NORMAL);
        }

        return null;
    }

    /**
     * Get all children of $p_parent_id, this does NOT includes an entry of the node itself ($p_parent_id) as well
     *
     * @param $p_parent_id
     *
     * @author Dennis Stücken <dstuecken@i-doit.de>
     *
     * @return isys_component_dao_result|null
     */
    public function get_children($p_parent_id)
    {
        $l_node = $this->get_by_node_id($p_parent_id)
            ->get_row();

        if ($l_node && isset($l_node[$this->m_src_field_left]) && isset($l_node[$this->m_src_field_right]))
        {
            return $this->get_left_by_left_right($l_node[$this->m_src_field_left], $l_node[$this->m_src_field_right], C__RECORD_STATUS__NORMAL, false);
        }

        return null;
    }

    /**
     * Recursively searches for $p_children in tree
     *
     * @param $p_root_object_id
     * @param $p_child_object_id
     *
     * @author Dennis Stücken <dstuecken@i-doit.de>
     *
     * @return isys_component_dao_result|null
     */
    public function has_children($p_root_object_id, $p_child_object_id)
    {
        if (isset($this->m_child_cache[$p_root_object_id][$p_child_object_id]))
        {
            return $this->m_child_cache[$p_root_object_id][$p_child_object_id];
        }

        if ($p_root_object_id > 0)
        {
            $l_node = $this->get_by_node_id($p_root_object_id)
                ->get_row();

            if ($l_node && isset($l_node[$this->m_src_field_left]) && isset($l_node[$this->m_src_field_right]))
            {

                $l_q = "SELECT " . $this->m_src_field_id . ", " . $this->m_src_field_node . ", " . $this->m_src_field_parent_id . ", " . $this->m_src_field_const . ", " . $this->m_src_field_left . ", " . $this->m_src_field_right . ", " . "isys_obj__title, " . "isys_obj_type__id, " . "isys_obj_type__title " .

                    "FROM " . "{$this->m_src_tbl} ";

                $l_q .= "INNER JOIN isys_obj ON isys_obj__id = {$this->m_src_tbl}__isys_obj__id " . "INNER JOIN isys_obj_type ON isys_obj__isys_obj_type__id = isys_obj_type__id ";

                $l_q .= "WHERE {$this->m_src_field_left} BETWEEN " . $l_node[$this->m_src_field_left] . " AND " . $l_node[$this->m_src_field_right] . ' AND ' . $this->m_src_field_node . ' = ' . $this->convert_sql_int(
                        $p_child_object_id
                    );

                if ($this->retrieve($l_q)
                        ->num_rows() > 0
                )
                {
                    $this->m_child_cache[$p_root_object_id][$p_child_object_id] = true;

                    return true;
                }
                else
                {
                    $this->m_child_cache[$p_root_object_id][$p_child_object_id] = false;
                }
            }
        }

        return false;
    }

    /**
     * Get items between left and right
     *
     * @param      $p_left
     * @param      $p_right
     * @param null $p_nRecStatus
     *
     * @return isys_component_dao_result|null
     * @throws Exception
     * @throws isys_exception_database
     */
    public function get_left_by_left_right($p_left, $p_right, $p_nRecStatus = null, $p_include_self = true)
    {
        if (is_numeric($p_left) && is_numeric($p_right))
        {
            $l_q = "SELECT " . $this->m_src_field_id . ", " . $this->m_src_field_node . ", " . $this->m_src_field_parent_id . ", " . $this->m_src_field_const . ", " . $this->m_src_field_left . ", " . $this->m_src_field_right . ", " . "isys_obj__title, " . "isys_obj_type__id, " . "isys_obj_type__title " .

                "FROM " . "{$this->m_src_tbl} ";

            $l_q .= "INNER JOIN isys_obj ON isys_obj__id = {$this->m_src_tbl}__isys_obj__id " . "INNER JOIN isys_obj_type ON isys_obj__isys_obj_type__id = isys_obj_type__id ";

            $l_q .= "WHERE {$this->m_src_field_left} BETWEEN " . $p_left . " AND " . $p_right;

            if (!is_null($p_nRecStatus))
            {
                $l_q .= " AND isys_obj__status = " . $this->convert_sql_int($p_nRecStatus) . " ";
            }

            if (!$p_include_self)
            {
                $l_q .= ' AND isys_obj__id != ' . $this->convert_sql_int($p_left);
            }

            $l_q .= " ORDER BY {$this->m_src_field_left} ASC";

            return $this->retrieve($l_q);
        }

        return null;
    }

    public function get_outer_by_left_right($p_left, $p_right)
    {
        if (is_numeric($p_left) && is_numeric($p_right))
        {
            $l_q = "SELECT " . "*" . "FROM " . "{$this->m_src_tbl} " . "WHERE " . "{$this->m_src_field_left} < $p_left " . "AND " . "{$this->m_src_field_right} > $p_right " . "GROUP BY " . "{$this->m_src_tbl}__isys_obj__id " . "ORDER BY " . "{$this->m_src_field_left} ASC" . ";";

            return $this->retrieve($l_q);
        }

        return null;
    }

    public function action_stack_add($p_action, $p_data)
    {
        if ($p_action > C__MPTT__ACTION_BEGIN && $p_action < C__MPTT__ACTION_END)
        {
            array_push(
                $this->m_actionstack,
                [
                    $p_action,
                    $p_data
                ]
            );

            return true;
        }

        return false;
    }

    /**
     * Public wrapper function for recursive writing algorithm
     *
     * @param object $p_callback
     *
     * @return mixed
     */
    public function write($p_callback = null)
    {
        return $this->_write($p_callback);
    }

    public function read($p_id, $p_callback = null, $p_userdata = null)
    {
        $l_resultset    = [];
        $l_treeres      = $this->get_by_node_id($p_id);
        $l_use_callback = (!is_null($p_callback));

        if ($l_treeres)
        {
            if ($l_treeres->num_rows())
            {
                $l_rstack = [];

                /* Fetch data of origin object */
                $l_startrow = $l_treeres->get_row();

                if ($l_startrow[$this->m_src_field_left] > $l_startrow[$this->m_src_field_right])
                {
                    $l_left                               = $l_startrow[$this->m_src_field_left];
                    $l_startrow[$this->m_src_field_left]  = $l_startrow[$this->m_src_field_right];
                    $l_startrow[$this->m_src_field_right] = $l_left;
                }

                /* Fetch results of sub-tree */
                $l_subres = $this->get_left_by_left_right(
                    $l_startrow[$this->m_src_field_left],
                    $l_startrow[$this->m_src_field_right],
                    C__RECORD_STATUS__NORMAL
                );

                if ($l_subres)
                {
                    /* Iterate through result set */
                    while ($l_subrow = $l_subres->get_row())
                    {

                        /* If there are entries in the stack ... */
                        if (count($l_rstack))
                        {
                            /* Remove all entries smaller than the current one */
                            while ($l_rstack[count($l_rstack) - 1] < $l_subrow[$this->m_src_field_right])
                            {
                                if (count($l_rstack) - 1 < 0) break;
                                array_pop($l_rstack);
                            }
                        }

                        /* Decide whether to use callback or append it to the result set */
                        if ($l_use_callback)
                        {

                            $p_callback->mptt_read(
                                $this->m_src_tbl,
                                count($l_rstack), /* Level */
                                $l_subrow[$this->m_src_field_id],
                                $l_subrow[$this->m_src_field_node],
                                $l_subrow[$this->m_src_field_parent_id],
                                $l_subrow[$this->m_src_field_const],
                                $l_subrow[$this->m_src_field_left],
                                $l_subrow[$this->m_src_field_right],
                                $p_userdata,
                                $l_subrow["isys_obj__title"]
                            );

                        }
                        else
                        {
                            $l_resultset[] = $l_subrow;
                        }

                        /* Add current right entry to stack */
                        $l_rstack[] = $l_subrow[$this->m_src_field_right];
                    }
                }

                if ($l_use_callback)
                {
                    return true;
                }

                return $l_resultset;
            }
        }

        if (isset($p_callback))
        {
            return false;
        }

        return null;
    }

    public function is_initialized()
    {
        return $this->m_initialized;
    }

    public function configure_datasource($p_tbl, $p_field_id, $p_field_node, $p_field_parentid, $p_field_const, $p_field_left, $p_field_right)
    {
        $this->m_src_tbl = $p_tbl;

        $this->m_src_field_id        = $p_field_id;
        $this->m_src_field_node      = $p_field_node;
        $this->m_src_field_parent_id = $p_field_parentid;
        $this->m_src_field_const     = $p_field_const;
        $this->m_src_field_left      = $p_field_left;
        $this->m_src_field_right     = $p_field_right;

        return true;
    }

    private function action_handler_add($p_data)
    {
        /* Check some required parameters */
        if (!array_key_exists("node_id", $p_data)) return "Please set node_id!";
        if (!array_key_exists("parent_id", $p_data)) return "Please set parent_id!";

        /* Check if node is existent */
        $l_existsdao = $this->retrieve(
            "SELECT " . "* " . "FROM " . "{$this->m_src_tbl} " . "WHERE " . "{$this->m_src_field_node}={$p_data["node_id"]};"
        );

        /* If yes, break proc */
        if ($l_existsdao && $l_existsdao->num_rows() > 0) return "Node already existent!";

        /* Add node */
        $this->update(
            "INSERT INTO " . "{$this->m_src_tbl} " . "(" . "{$this->m_src_field_node}, " . "{$this->m_src_field_parent_id}, " . "{$this->m_src_field_const}, " . "{$this->m_src_field_left}, " . "{$this->m_src_field_right} " . ") VALUES (" . "'{$p_data["node_id"]}', " . "'{$p_data["parent_id"]}', " . "'{$p_data["const"]}', " . "'0', " . "'0' " . ");"
        );

        /* Everything was okay */

        return null;
    }

    private function action_handler_update($p_data)
    {
        if (!array_key_exists("node_id", $p_data)) return "Please set node_id";

        $l_nodeid = $p_data["node_id"];
        unset($p_data["node_id"]);

        $l_strextra = "";
        $l_ci       = 0;

        foreach ($p_data as $l_field => $l_data)
        {
            $l_strextra .= "$l_field='" . (is_numeric($l_data) ? $l_data : $this->m_db->escape_string($l_data)) . "' ";

            if ($l_ci < count($p_data) - 1)
            {
                $l_strextra .= ", ";
            }

            $l_ci++;
        }

        $l_q = "UPDATE " . "{$this->m_src_tbl} " . "SET " . "{$this->m_src_field_node}='" . $l_nodeid . "', " . $l_strextra . "WHERE " . "{$this->m_src_field_node}='" . $l_nodeid . "';";

        if (!$this->update($l_q))
        {
            return "Update was not successful ($l_q)!";
        }

        return null;
    }

    private function action_handler_delete($p_data)
    {
        if (!array_key_exists("node_id", $p_data)) return "Please set node_id";

        /* Delete node */
        $this->update(
            "DELETE FROM " . "{$this->m_src_tbl} " . "WHERE " . "{$this->m_src_field_node} = {$p_data["node_id"]};"
        );

        /* Everything was okay */

        return null;
    }

    private function action_handler_move($p_data)
    {
        if (!array_key_exists("node_id", $p_data)) return "Please set node_id";
        if (!array_key_exists("parent_id", $p_data)) return "Please set node_id";

        /* Delete node */
        $this->update(
            "UPDATE " . "{$this->m_src_tbl} " . "SET " . "{$this->m_src_field_parent_id} = {$p_data["parent_id"]} " . "WHERE " . "{$this->m_src_field_node} = {$p_data["node_id"]};"
        );

        /* Everything was okay */

        return null;
    }

    private function action_stack_process()
    {
        if (count($this->m_actionstack) > 0)
        {

            $this->begin_update();

            foreach ($this->m_actionstack as $l_action_data)
            {
                list($l_action, $l_data) = $l_action_data;
                $l_ret = null;

                switch ($l_action)
                {
                    case C__MPTT__ACTION_ADD:
                        $l_ret = $this->action_handler_add($l_data);
                        break;
                    case C__MPTT__ACTION_DELETE:
                        $l_ret = $this->action_handler_delete($l_data);
                        break;
                    case C__MPTT__ACTION_MOVE:
                        $l_ret = $this->action_handler_move($l_data);
                        break;
                    case C__MPTT__ACTION_UPDATE:
                        $l_ret = $this->action_handler_update($l_data);
                        break;
                    default:
                        throw new isys_exception_dao("MPTT: Invalid action $l_action!");
                }

                if ($l_ret != null)
                {
                    throw new isys_exception_dao("MPTT: Could not handle action $l_action: $l_ret");
                }
            }

            $this->apply_update();

            unset($this->m_actionstack);
            $this->m_actionstack = [];
        }
    }

    private function _write($p_callback = null, $p_node_id = C__OBJ__ROOT_LOCATION, &$p_left = null)
    {
        /* Get node data */

        $l_noderes = $this->get_by_node_id($p_node_id);

        if ($l_noderes && $l_noderes->num_rows() > 0)
        {
            $l_noderow = $l_noderes->get_row();

            /* Before writing anything, we process the action stack,
               but from the root node, since changes can modify
               the whole tree! */
            if ($l_noderow[$this->m_src_field_node] == C__OBJ__ROOT_LOCATION)
            {
                $p_left = 1; /* Start from 1, please */

                /* Process respective action stack */
                $this->action_stack_process();
            }

            $l_right = $p_left + 1;

            /* Enumerate child nodes */
            $l_childres = $this->get_by_parent_id($l_noderow[$this->m_src_field_node]);

            if($l_childres->num_rows() > 0)
            {
                while ($l_childrow = $l_childres->get_row())
                {
                    $l_right = $this->_write($p_callback, $l_childrow[$this->m_src_field_node], $l_right);
                }
            }

            $l_dowrite = true;

            /* Call callback, if developer wants to change the vars again */
            if ($p_callback)
            {
                /* A callback can influence the behaviour. If the callback
                   does not return a true value, the update transaction
                   won't be commited */
                $l_dowrite = $p_callback->mptt_write(
                    $p_node_id,
                    $l_noderow[$this->m_src_field_parent_id],
                    $l_noderow[$this->m_src_field_const],
                    $p_left,
                    $l_right
                );
            }

            if ($l_dowrite)
            {
                /* Update transaction */
                {
                    $l_q = "UPDATE " . "{$this->m_src_tbl} " . "SET " . "{$this->m_src_field_left} = '$p_left', " . "{$this->m_src_field_right} = '$l_right' " . "WHERE " . "{$this->m_src_field_node} = '$p_node_id';";

                    /* Update node entry */
                    $this->update($l_q);
                }
            }

            return $l_right + 1;
        }
        return $p_left;
    }

    public function __construct(isys_component_database &$p_db, $p_tbl, $p_field_id, $p_field_node, $p_field_parentid, $p_field_const, $p_field_left, $p_field_right)
    {
        parent::__construct($p_db);

        $this->m_initialized = false;

        if ($this->configure_datasource(
            $p_tbl,
            $p_field_id,
            $p_field_node,
            $p_field_parentid,
            $p_field_const,
            $p_field_left,
            $p_field_right
        )
        )
        {
            $this->m_initialized = true;
            $this->m_actionstack = [];
        }
    }
}

/**
 * isys_mptt_callback is the interface you have to implement in a class,
 * which is reponsible to handle a reading / writing iteration from the
 * read / write methods of isys_component_dao_mptt.
 */
interface isys_mptt_callback
{
    /**
     * Implement this method in order to define a handler where you accept
     * incoming reading iterations. I.e. you can directly build up a JavaScript
     * tree by passing a isys_component_tree-object to $p_userdata
     *
     * @param string  $p_table
     * @param integer $p_level
     * @param integer $p_id
     * @param integer $p_node_id
     * @param integer $p_parent_id
     * @param string  $p_const
     * @param integer $p_left
     * @param integer $p_right
     * @param mixed   $p_userdata
     */
    public function mptt_read($p_table, $p_level, $p_id, $p_node_id, $p_parent_id, $p_const, $p_left, $p_right, $p_userdata);

    /**
     * Implement this method in order to define a handler where you can change
     * data to write before writing. This method has to return true in
     * its implementation in order to commit the write. Otherwise, with
     * false, nothing will be written.
     *
     * @param ref -integer $p_node_id
     * @param ref -integer $p_parent_id
     * @param ref -string $p_const
     * @param ref -integer $p_left
     * @param ref -integer $p_right
     */
    public function mptt_write(&$p_node_id, &$p_parent_id, &$p_const, &$p_left, &$p_right);
}

?>