<?php
/*********************************************************************************
 *
 * TimeTrex is a Workforce Management program developed by
 * TimeTrex Software Inc. Copyright (C) 2003 - 2021 TimeTrex Software Inc.
 *
 * This program is free software; you can redistribute it and/or modify it under
 * the terms of the GNU Affero General Public License version 3 as published by
 * the Free Software Foundation with the addition of the following permission
 * added to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED
 * WORK IN WHICH THE COPYRIGHT IS OWNED BY TIMETREX, TIMETREX DISCLAIMS THE
 * WARRANTY OF NON INFRINGEMENT OF THIRD PARTY RIGHTS.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public License for more
 * details.
 *
 *
 * You should have received a copy of the GNU Affero General Public License along
 * with this program; if not, see http://www.gnu.org/licenses or write to the Free
 * Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 * 02110-1301 USA.
 *
 *
 * You can contact TimeTrex headquarters at Unit 22 - 2475 Dobbin Rd. Suite
 * #292 West Kelowna, BC V4T 2E9, Canada or at email address info@timetrex.com.
 *
 *
 * The interactive user interfaces in modified source and object code versions
 * of this program must display Appropriate Legal Notices, as required under
 * Section 5 of the GNU Affero General Public License version 3.
 *
 *
 * In accordance with Section 7(b) of the GNU Affero General Public License
 * version 3, these Appropriate Legal Notices must retain the display of the
 * "Powered by TimeTrex" logo. If the display of the logo is not reasonably
 * feasible for technical reasons, the Appropriate Legal Notices must display
 * the words "Powered by TimeTrex".
 *
 ********************************************************************************/


/*
--	  id serial NOT NULL,
CREATE TABLE hierarchy_tree (
	tree_id integer DEFAULT 0 NOT NULL,
	parent_id integer DEFAULT 0 NOT NULL,
	object_id integer DEFAULT 0 NOT NULL,
	left_id bigint DEFAULT 0 NOT NULL,
	right_id bigint DEFAULT 0 NOT NULL
) WITHOUT OIDS;
--Order of the columsn in the below index seem to matter
create index "hierarchy_tree_left_id_right_id" on hierarchy_tree(left_id, right_id);
create index "hierarchy_tree_tree_id_object_id" on hierarchy_tree(tree_id, object_id);
create index "hierarchy_tree_tree_id_parent_id" on hierarchy_tree(tree_id, parent_id);
*/


/**
 * @package Core
 */
class FastTree {
	var $db = null;
	var $table = 'fast_tree';
	var $tree_id = 0;

	var $spacer = 0;

	/**
	 * FastTree constructor.
	 * @param null $options
	 */
	function __construct( $options = null ) {
		//Debug::Text(' Contruct... ', __FILE__, __LINE__, __METHOD__, 10);

		$this->db = $options['db'];
		//Debug::Text(' Setting DB to: '. $options['db'], __FILE__, __LINE__, __METHOD__, 10);

		$this->table = $options['table'];
		//Debug::Text(' Setting Table to: '. $options['table'], __FILE__, __LINE__, __METHOD__, 10);

		if ( isset( $options['tree_id'] ) ) {
			$this->setTree( $options['tree_id'] );
			//$this->tree_id = $options['tree_id'];
			//Debug::Text(' Setting Tree ID to: '. $options['tree_id'], __FILE__, __LINE__, __METHOD__, 10);
		}

		return true;
	}

	/**
	 * @return int
	 */
	function getTree() {
		return $this->tree_id;
	}

	/**
	 * @param string $id UUID
	 * @return bool
	 */
	function setTree( $id ) {
		if ( $id != '' ) {
			//Debug::Text(' Setting Tree ID to: '. $id, __FILE__, __LINE__, __METHOD__, 10);
			$this->tree_id = $id;

			$this->_setupTree();

			return true;
		}

		return false;
	}

	/**
	 * @return bool
	 */
	function _setupTree() {
		//Add the root node if its missing.
		$node_data = $this->getNode( 0 );
		if ( $node_data === false ) {
			Debug::Text( ' Initiating Tree with Root object: ', __FILE__, __LINE__, __METHOD__, 10 );
			$this->add( 0, -1 );

			return true;
		}

		//Debug::Text(' NOT Initiating Tree with Root object: ', __FILE__, __LINE__, __METHOD__, 10);
		return false;
	}

	/**
	 * @return mixed
	 */
	function getRootId() {
		$ph = [
				'tree_id' => $this->tree_id,
		];

		// get all children of this node
		$query = 'SELECT object_id FROM ' . $this->table . ' WHERE tree_id = ? AND parent_id = -1';
		$root_id = $this->db->GetOne( $query, $ph );

		return $root_id;
	}

	/**
	 * @param string $object_id UUID
	 * @return bool
	 */
	function getNode( $object_id ) {
		//Debug::Text(' Object ID: '. $object_id, __FILE__, __LINE__, __METHOD__, 10);

		//Check to make sure object_id doesn't exceed 32bit integer.
		if ( !is_numeric( $object_id ) || ( $object_id > 2147483647 || $object_id < -2147483648 ) ) {
			Debug::Text( ' aReturning False', __FILE__, __LINE__, __METHOD__, 10 );

			return false;
		}

		$ph = [
				'tree_id'   => (int)$this->getTree(),
				'object_id' => (int)$object_id,
		];

		// get all children of this node
		$query = '	SELECT a.object_id, a.parent_id, a.left_id, a.right_id, count(b.object_id)-1 as level
					FROM ' . $this->table . ' a
					LEFT JOIN ' . $this->table . ' b ON a.tree_id = b.tree_id AND a.left_id BETWEEN b.left_id AND b.right_id

					WHERE a.tree_id = ?
						AND a.object_id = ?
					GROUP BY a.object_id, a.left_id, a.object_id, a.parent_id, a.right_id
				';
		$data = $this->db->GetRow( $query, $ph );

		if ( count( $data ) == 0 ) {
			return false;
		}

		return $data;
	}

	/**
	 * @param string $object_id UUID
	 * @return bool
	 */
	function getLevel( $object_id ) {
		//Debug::Text(' Object ID: '. $object_id, __FILE__, __LINE__, __METHOD__, 10);

		$data = $this->getNode( $object_id );
		if ( $data === false ) {
			return false;
		}

		return $data['level'];
	}

	/**
	 * @param string $object_id UUID
	 * @return bool
	 */
	function getRightId( $object_id ) {
		//Debug::Text(' Object ID: '. $object_id, __FILE__, __LINE__, __METHOD__, 10);

		$data = $this->getNode( $object_id );
		if ( $data === false ) {
			return false;
		}

		return $data['right_id'];
	}

	/**
	 * @param string $object_id UUID
	 * @return bool
	 */
	function getLeftId( $object_id ) {
		//Debug::Text(' Object ID: '. $object_id, __FILE__, __LINE__, __METHOD__, 10);

		$data = $this->getNode( $object_id );
		if ( $data === false ) {
			return false;
		}

		return $data['left_id'];
	}

	/**
	 * @param string $object_id UUID
	 * @return bool
	 */
	function getParentId( $object_id ) {
		//Debug::Text(' Object ID: '. $object_id, __FILE__, __LINE__, __METHOD__, 10);

		$data = $this->getNode( $object_id );
		if ( $data === false ) {
			return false;
		}

		return $data['parent_id'];
	}

	/**
	 * @param bool $object_id
	 * @return bool
	 */
	function rebuildTree( $object_id = false ) {
		Debug::Text( ' Object ID: ' . $object_id, __FILE__, __LINE__, __METHOD__, 10 );

		$this->db->BeginTrans();
		$this->db->SetTransactionMode( 'SERIALIZABLE' ); //Serialize rebuild tree transactions so concurrency issues don't corrupt the tree.

		if ( $object_id === false ) {
			Debug::Text( ' Object ID not specified, using root: ', __FILE__, __LINE__, __METHOD__, 10 );
			$object_id = $this->getRootId();
			$left_id = 1;
		} else {
			Debug::Text( ' Object ID specified: ', __FILE__, __LINE__, __METHOD__, 10 );
			$left_id = $this->getLeftId( $object_id );
		}

		if ( $left_id === false ) {
			Debug::Text( ' Error getting left id: ', __FILE__, __LINE__, __METHOD__, 10 );

			return false;
		}

		Debug::Text( ' aObject ID: ' . $object_id . ' - Left ID: ' . $left_id, __FILE__, __LINE__, __METHOD__, 10 );
		$rebuilt = $this->_rebuildTree( $object_id, $left_id );

		if ( $rebuilt === false ) {
			Debug::Text( ' Error rebuilding tree: ', __FILE__, __LINE__, __METHOD__, 10 );
			$this->db->RollBackTrans();

			return false;
		}

		//$this->db->RollBackTrans();

		$this->db->CommitTrans();

		$this->db->SetTransactionMode( '' ); //Restore default transaction mode.

		Debug::Text( ' Tree Rebuilt: ', __FILE__, __LINE__, __METHOD__, 10 );

		return true;
	}

	/**
	 * @param string $object_id UUID
	 * @param string $left_id   UUID
	 * @return bool
	 */
	function _rebuildTree( $object_id, $left_id ) {
		Debug::Text( ' Object ID: ' . $object_id . ' - Left: ' . $left_id, __FILE__, __LINE__, __METHOD__, 10 );

		$ph = [
				'tree_id'   => (int)$this->getTree(),
				'parent_id' => (int)$object_id,
		];

		// get all children of this node
		$query = 'SELECT object_id FROM ' . $this->table . ' WHERE tree_id = ? AND parent_id = ?';
		$rs = $this->db->Execute( $query, $ph );

		if ( !is_object( $rs ) ) {
			Debug::Text( ' Select failed', __FILE__, __LINE__, __METHOD__, 10 );

			return false;
		}

		// the right value of this node is the left value + 1 (or more)
		$right_id = ( $left_id + 10 );

		while ( $row = $rs->FetchRow() ) {
			// recursive execution of this function for each
			// child of this node
			// $right is the current right value, which is
			// incremented by the rebuild_tree function
			Debug::Text( ' Right ID: ' . $right_id, __FILE__, __LINE__, __METHOD__, 10 );
			$right_id = $this->_rebuildTree( $row['object_id'], $right_id );

			if ( $right_id === false ) {
				Debug::Text( ' Right was false: ', __FILE__, __LINE__, __METHOD__, 10 );

				return false;
			}
		}

		$ph = [
				'left_id'   => (int)$left_id,
				'right_id'  => (int)$right_id,
				'tree_id'   => (int)$this->getTree(),
				'object_id' => (int)$object_id,
		];

		// we've got the left value, and now that we've processed
		// the children of this node we also know the right value
		$query = 'UPDATE ' . $this->table . ' SET left_id = ?, right_id = ? WHERE tree_id = ? AND object_id = ?';
		$rs = $this->db->Execute( $query, $ph );

		//Use this to help debug concurrency issues.
		//usleep(100000);

		if ( !is_object( $rs ) ) {
			Debug::Text( ' Rebuild Failed... ', __FILE__, __LINE__, __METHOD__, 10 );

			return false;
		}

		// return the right value of this node + 1
		return ( $right_id + 1 );
	}

	/**
	 * @param string $object_id UUID
	 * @return bool
	 */
	function getAllParents( $object_id ) {
		//Debug::Text(' Object ID: '. $object_id, __FILE__, __LINE__, __METHOD__, 10);

		if ( $object_id === '' ) {
			Debug::Text( ' aReturning False...', __FILE__, __LINE__, __METHOD__, 10 );

			return false;
		}

		$ph = [
				'tree_id'    => (int)$this->getTree(),
				'object_id'  => (int)$object_id,
				'object_id2' => (int)$object_id,
		];

		$query = '
				SELECT		b.object_id
				FROM		' . $this->table . ' as a
				LEFT JOIN ' . $this->table . ' as b ON a.tree_id = b.tree_id AND a.left_id BETWEEN b.left_id AND b.right_id
				WHERE		a.tree_id = ?
					AND		a.object_id = ?
					AND		b.object_id != 0
					AND		b.object_id != ?
				ORDER BY	b.left_id desc
				';

		return $this->db->GetCol( $query, $ph );
	}

	/**
	 * @param string $object_id UUID
	 * @return bool
	 */
	function getChild( $object_id ) {
		if ( !is_numeric( $object_id ) ) {
			Debug::Text( ' aReturning False...', __FILE__, __LINE__, __METHOD__, 10 );

			return false;
		}

		$ph = [
				'tree_id'   => (int)$this->getTree(),
				'object_id' => (int)$object_id,
		];

		Debug::Text( ' Getting Last Child of: ' . $object_id, __FILE__, __LINE__, __METHOD__, 10 );
		//Order by last child first.
		//GetOne() automatically sets LIMIT 1;
		$query = 'SELECT object_id FROM ' . $this->table . ' WHERE tree_id = ? AND parent_id = ? ORDER BY left_id desc';
		$child_id = $this->db->GetOne( $query, $ph );

		//var_dump($child_id);

		return $child_id;
	}

	/**
	 * @param string|bool $object_id UUID
	 * @param bool $recurse
	 * @param int $data_format
	 * @return array|bool
	 */
	function getAllChildren( $object_id = null, $recurse = false, $data_format = 0 ) {
		$original_object_id = $object_id;
		//Debug::Text(' Object ID: '. $object_id .' Recurse: '. $recurse, __FILE__, __LINE__, __METHOD__, 10);

		if ( $object_id === '' ) {
			Debug::Text( ' aReturning False...', __FILE__, __LINE__, __METHOD__, 10 );

			return false;
		}

		if ( $object_id === null || $object_id === false ) {
			$object_id = $this->getRootId();
			Debug::Text( ' Getting Root ID: ' . $object_id, __FILE__, __LINE__, __METHOD__, 10 );
		}

		$node_data = $this->getNode( $object_id );

		if ( $node_data === false ) {
			Debug::Text( ' Getting node data of object id failed.', __FILE__, __LINE__, __METHOD__, 10 );

			return false;
		}
		//Debug::Text(' Left ID: '. $node_data['left_id'] .' Level: '. $node_data['level'], __FILE__, __LINE__, __METHOD__, 10);

		$query = '
				SELECT		a.object_id, a.parent_id, count(b.object_id) as level
				FROM		' . $this->table . ' a
				LEFT JOIN ' . $this->table . ' b ON a.tree_id = b.tree_id AND a.left_id BETWEEN b.left_id AND b.right_id
				';
		switch ( strtoupper( $recurse ) ) {
			case 'RECURSE':
				$ph = [
						'tree_id'  => $this->getTree(),
						'left_id'  => $node_data['left_id'],
						'right_id' => $node_data['right_id'],
				];

				//Don't use >= <= (use > < ) - instead to not include the parent object.
				//Make sure current node is not included in the result as well. Otherwise we are saying the current node
				//is a child of itself.
				$query .= '
				WHERE		a.tree_id = ?
					AND		b.left_id > ?
					AND		b.right_id <= ?';

				//Exclude the parnet, but only when the passed object is forsure NULL!
				if ( $original_object_id === null || $original_object_id === false ) {
					$ph['object_id'] = $object_id;

					$query .= '
					AND		a.object_id != ?
					';
				}

				break;
			default:
				$ph = [
						'tree_id'   => (int)$this->getTree(),
						'object_id' => (int)$object_id,
				];

				$query .= '
						WHERE a.tree_id = ?
							AND a.parent_id = ?';
		}
		$query .= '
				GROUP BY	a.object_id, a.parent_id, a.left_id
				ORDER BY	a.left_id';

		$rs = $this->db->Execute( $query, $ph );

		$retarr = [];
		while ( $row = $rs->FetchRow() ) {
			if ( $data_format == 1 ) {
				$retarr[$row['object_id']] = $row;
			} else {
				$retarr[$row['object_id']] = $row['level'];
			}
		}

		if ( empty( $retarr ) == false ) {
			//Debug::Arr( $retarr, ' Children: ', __FILE__, __LINE__, __METHOD__, 10);
			return $retarr;
		}

		return false;
	}

	/**
	 * @param string $parent_id UUID
	 * @return array|bool
	 */
	function _getLeftAndRightIds( $parent_id ) {
		Debug::Text( ' getLeftAndRightIds: ' . $parent_id, __FILE__, __LINE__, __METHOD__, 10 );

		$node_data = $this->getNode( $parent_id );

		$parent_left = $node_data['left_id'];
		$parent_right = $node_data['right_id'];

		$child_id = $this->getChild( $parent_id );
		if ( $child_id !== false ) {
			Debug::Text( ' Child found, getting Child data: ' . $child_id, __FILE__, __LINE__, __METHOD__, 10 );
			$child_node_data = $this->getNode( $child_id );
			$child_left_id = $child_node_data['left_id'];
			$child_right_id = $child_node_data['right_id'];
			unset( $child_node_data );
			Debug::Text( ' Child Left ID: ' . $child_left_id, __FILE__, __LINE__, __METHOD__, 10 );
			Debug::Text( ' Child Right ID: ' . $child_right_id, __FILE__, __LINE__, __METHOD__, 10 );
			Debug::Text( ' Parent Right ID: ' . $parent_right, __FILE__, __LINE__, __METHOD__, 10 );

			$left_id = ( $child_right_id + 1 );
			$right_id = ( $child_right_id + 10 );

			if ( $right_id >= $parent_right
					|| $left_id >= $parent_right ) {
				Debug::Text( ' NO CHILD GAP LEFT: ', __FILE__, __LINE__, __METHOD__, 10 );

				return false;
			}
		} else {
			//Nothing yet.

			//Try to keep a large gap for these.
			$left_id = ( $parent_left + 1 );
			$right_id = ( $parent_right - 1 );

			if ( $right_id >= $parent_right
					|| $left_id >= $parent_right ) {
				Debug::Text( ' NO PARENT GAP LEFT: ', __FILE__, __LINE__, __METHOD__, 10 );

				return false;
			}
		}

		Debug::Text( ' Next Left ID: ' . $left_id, __FILE__, __LINE__, __METHOD__, 10 );
		Debug::Text( ' Next Right ID: ' . $right_id, __FILE__, __LINE__, __METHOD__, 10 );

		return [ 'left_id' => (int)$left_id, 'right_id' => (int)$right_id ];
	}

	/**
	 * @param string $parent_id UUID
	 * @return bool
	 */
	function insertGaps( $parent_id ) {
		$this->spacer++;

		Debug::Text( ' Attempting to insert gaps: ' . $this->spacer, __FILE__, __LINE__, __METHOD__, 10 );

		$node_data = $this->getNode( $parent_id );

		if ( $node_data != false ) {
			Debug::Text( ' Inserting gaps: ' . $this->spacer, __FILE__, __LINE__, __METHOD__, 10 );

			$ph = [
					'tree_id'  => $this->getTree(),
					'right_id' => $node_data['right_id'],
			];

			$query = 'UPDATE ' . $this->table . ' SET right_id = right_id + 1000 WHERE tree_id = ? AND right_id >= ?';
			$this->db->Execute( $query, $ph );

			$query = 'UPDATE ' . $this->table . ' SET left_id = left_id + 1000 WHERE tree_id = ? AND left_id > ?';
			$this->db->Execute( $query, $ph );

			return true;
		}
		Debug::Text( ' Node Data Null: ' . $this->spacer, __FILE__, __LINE__, __METHOD__, 10 );

		return false;
	}

	/**
	 * MPTT + Gap add function.
	 * @param string $object_id UUID
	 * @param int $parent_id
	 * @return bool
	 */
	function add( $object_id, $parent_id = 0 ) {
		Debug::Text( ' Object ID: ' . $object_id . ' Parent ID: ' . $parent_id, __FILE__, __LINE__, __METHOD__, 10 );

		if ( !is_numeric( $object_id ) ) {
			Debug::Text( ' aReturning False...', __FILE__, __LINE__, __METHOD__, 10 );

			return false;
		}

		/*
		if ( $object_id == $parent_id ) {
			Debug::Text(' bReturning False...', __FILE__, __LINE__, __METHOD__, 10);
			return FALSE;
		}
		*/
		//$insert_id = $this->db->GenID( $this->table.'_id_seq', 10);

		//Make sure object doesn't exist already
		if ( $this->getNode( $object_id ) !== false ) {
			Debug::Text( ' cReturning False...', __FILE__, __LINE__, __METHOD__, 10 );

			return false;
		}

		$this->db->BeginTrans();

		if ( $parent_id == -1 ) {
			Debug::Text( ' Parent is 0', __FILE__, __LINE__, __METHOD__, 10 );

			$ph = [
					'tree_id' => $this->getTree(),
			];

			$query = 'SELECT object_id FROM ' . $this->table . ' WHERE tree_id = ? AND parent_id = -1';
			$rs = $this->db->Execute( $query, $ph );

			if ( !is_object( $rs ) ) {
				Debug::Text( ' Select failed', __FILE__, __LINE__, __METHOD__, 10 );
				$this->db->RollBackTrans();

				return false;
			}

			if ( $rs->RowCount() > 0 ) {
				Debug::Text( ' A root node already exists', __FILE__, __LINE__, __METHOD__, 10 );
				$this->db->RollBackTrans();

				return false;
			}

			$left_id = 0;

			//Get max right_id, just incase other nodes exist in the tree.
			$ph = [
					'tree_id' => $this->getTree(),
			];

			$query = 'SELECT max(right_id) as right_id FROM ' . $this->table . ' WHERE tree_id = ?';
			$right_id = ( $this->db->GetOne( $query, $ph ) + 1000 );
		} else {
			Debug::Text( ' Parent IS NOT 0', __FILE__, __LINE__, __METHOD__, 10 );

			$left_and_right_ids = $this->_getLeftAndRightIds( $parent_id );

			if ( $left_and_right_ids === false ) {
				$this->insertGaps( $parent_id );
				$left_and_right_ids = $this->_getLeftAndRightIds( $parent_id );
			}

			$left_id = $left_and_right_ids['left_id'];
			$right_id = $left_and_right_ids['right_id'];
		}

		if ( is_numeric( $this->getTree() )
				&& is_numeric( $parent_id )
				&& is_numeric( $object_id )
				&& is_numeric( $left_id )
				&& is_numeric( $right_id ) ) {
			$ph = [
					'tree_id'   => (int)$this->getTree(),
					'parent_id' => (int)$parent_id,
					'object_id' => (int)$object_id,
					'left_id'   => (int)$left_id,
					'right_id'  => (int)$right_id,
			];

			Debug::Text( ' Inserting Node... Left ID: ' . $left_id . ' Right ID: ' . $right_id, __FILE__, __LINE__, __METHOD__, 10 );
			$query = 'INSERT INTO ' . $this->table . ' (tree_id, parent_id, object_id, left_id, right_id) VALUES (?, ?, ?, ?, ?)';
			$rs = $this->db->Execute( $query, $ph );

			if ( !is_object( $rs ) ) {
				Debug::Text( ' Error inserting node', __FILE__, __LINE__, __METHOD__, 10 );
				$this->db->RollBackTrans();

				return false;
			}

			$this->db->CommitTrans();

			Debug::Text( ' Returning True.', __FILE__, __LINE__, __METHOD__, 10 );

			return true;
		}

		return false;
	}

	/**
	 * @param $array
	 * @param null $ph
	 * @return string
	 */
	protected function getListSQL( $array, &$ph = null ) {
		//Debug::Arr($ph, 'Place Holder BEFORE:', __FILE__, __LINE__, __METHOD__, 10);

		//Append $array values to end of $ph, return
		//one "?, " for each element in $array.

		$ph_arr = [];
		$array_count = count( $array );
		if ( is_array( $array ) && $array_count > 0 ) {
			foreach ( $array as $val ) {
				$ph_arr[] = '?';
				$ph[] = $val;
			}

			if ( empty( $ph_arr ) == false ) {
				$retval = implode( ',', $ph_arr );
			}
		} else if ( is_array( $array ) ) {
			//Return NULL, because this is an empty array.
			//This may have to return -1 instead of NULL
			//$ph[] = 'NULL';
			$ph[] = -1;
			$retval = '?';
		} else if ( $array == '' ) {
			//$ph[] = 'NULL';
			$ph[] = -1;
			$retval = '?';
		} else {
			$ph[] = $array;
			$retval = '?';
		}

		//Debug::Arr($ph, 'Place Holder AFTER:', __FILE__, __LINE__, __METHOD__, 10);

		//Just a single ID, return it.
		return $retval;
	}

	/**
	 * @param string $object_id UUID
	 * @param bool $recurse
	 * @return bool
	 */
	function delete( $object_id, $recurse = false ) {
		Debug::Text( ' Deleting Object: ' . $object_id, __FILE__, __LINE__, __METHOD__, 10 );

		if ( $object_id == '' ) {
			Debug::Text( ' aReturning False...', __FILE__, __LINE__, __METHOD__, 10 );

			return false;
		}

		//Find out if this node has children
		$this->db->BeginTrans();

		//This was the source of a bug that was causing the below recurse delete query
		//to delete the root node of the tree. getAllChildren was returning FALSE and array_keys()
		//was turning that into array(0 => 0), so we were deleting node 0 and XXX in a single operation.
		$children_ids = $this->getAllChildren( $object_id, 'RECURSE' );
		if ( $children_ids !== false && is_array( $children_ids ) ) {
			$children_ids = array_keys( $children_ids );
		} else {
			$children_ids = [];
		}

		if ( count( $children_ids ) == 0 ) {
			Debug::Text( ' No Children: ', __FILE__, __LINE__, __METHOD__, 10 );

			$ph = [
					'tree_id'   => (int)$this->getTree(),
					'object_id' => (int)$object_id,
			];

			$query = 'DELETE FROM ' . $this->table . ' WHERE tree_id = ? AND object_id = ?';
			$this->db->Execute( $query, $ph );
		} else if ( strtolower( $recurse ) == 'recurse' ) {
			Debug::Arr( $children_ids, ' Recursing Delete - Current Object: ' . $object_id . ' Child IDs: ', __FILE__, __LINE__, __METHOD__, 10 );

			$ph = [
					'tree_id' => $this->getTree(),
			];

			//Add current object_id to children for delete.
			$children_ids[] = $object_id;

			$query = 'DELETE FROM ' . $this->table . ' WHERE tree_id = ? AND object_id in (' . $this->getListSQL( $children_ids, $ph ) . ')';
			$this->db->Execute( $query, $ph );
		} else {
			Debug::Text( ' Re-parenting children: ', __FILE__, __LINE__, __METHOD__, 10 );

			$parent_id = $this->getParentId( $object_id );

			$ph = [
					'tree_id'   => (int)$this->getTree(),
					'object_id' => (int)$object_id,
			];

			$query = 'DELETE FROM ' . $this->table . ' WHERE tree_id = ? AND object_id = ?';
			$this->db->Execute( $query, $ph );

			$ph = [
					'parent_id' => (int)$parent_id,
					'tree_id'   => (int)$this->getTree(),
					'object_id' => (int)$object_id,
			];

			$query = '	UPDATE ' . $this->table . '
						SET parent_id = ?
						WHERE tree_id = ?
							AND parent_id = ?';
			$this->db->Execute( $query, $ph );
		}

		$this->db->CommitTrans();

		return true;
	}

	/**
	 * @param string $object_id UUID
	 * @param string $parent_id UUID
	 * @return bool
	 */
	function move( $object_id, $parent_id ) {
		Debug::Text( ' Object ID: ' . $object_id . ' Parent ID: ' . $parent_id, __FILE__, __LINE__, __METHOD__, 10 );

		if ( $object_id === '' ) {
			Debug::Text( ' aReturning False...', __FILE__, __LINE__, __METHOD__, 10 );

			return false;
		}

		if ( $parent_id === '' ) {
			Debug::Text( ' bReturning False...', __FILE__, __LINE__, __METHOD__, 10 );

			return false;
		}

		//Make sure we don't reparent to self.
		$children_ids = array_keys( (array)$this->getAllChildren( $object_id, 'RECURSE' ) );

		if ( $parent_id != TTUUID::getZeroID() && is_array( $children_ids ) && in_array( $parent_id, $children_ids ) == true ) {
			Debug::Text( ' Objects cant be re-parented to their own children...', __FILE__, __LINE__, __METHOD__, 10 );

			return false;
		}

		$this->db->BeginTrans();

		$ph = [
				'parent_id' => (int)$parent_id,
				'tree_id'   => (int)$this->getTree(),
				'object_id' => (int)$object_id,
		];

		$query = '	UPDATE ' . $this->table . '
					SET parent_id = ?
					WHERE tree_id = ?
						AND object_id = ?';
		$this->db->Execute( $query, $ph );

		//FIXME: rebuild tree starting from object_id and parent_id only perhaps?
		//Might cut down on some work.
		$this->rebuildTree();

		$this->db->CommitTrans();

		return true;
	}

	/**
	 * @param string $object_id     UUID
	 * @param string $new_object_id UUID
	 * @return bool
	 */
	function edit( $object_id, $new_object_id ) {
		Debug::Text( ' Object ID: ' . $object_id . ' New Object ID: ' . $new_object_id, __FILE__, __LINE__, __METHOD__, 10 );

		if ( $object_id == '' ) {
			Debug::Text( ' aReturning False...', __FILE__, __LINE__, __METHOD__, 10 );

			return false;
		}

		if ( $new_object_id == '' ) {
			Debug::Text( ' bReturning False...', __FILE__, __LINE__, __METHOD__, 10 );

			return false;
		}

		if ( $object_id == $new_object_id ) {
			Debug::Text( ' Object is the same ', __FILE__, __LINE__, __METHOD__, 10 );

			return true;
		}

		//Make sure new_object_id isn't already in the tree.
		if ( $this->getNode( $new_object_id ) === false ) {
			Debug::Text( ' Editing object ', __FILE__, __LINE__, __METHOD__, 10 );

			$this->db->BeginTrans();

			$ph = [
					'new_object_id' => (int)$new_object_id,
					'tree_id'       => (int)$this->getTree(),
					'object_id'     => (int)$object_id,
			];

			//Update parent IDs
			$query = '	UPDATE ' . $this->table . '
						SET parent_id = ?
						WHERE tree_id = ?
							AND parent_id = ?';
			$this->db->Execute( $query, $ph );

			//Update object ID
			$query = '	UPDATE ' . $this->table . '
						SET object_id = ?
						WHERE tree_id = ?
							AND object_id = ?';
			$this->db->Execute( $query, $ph );

			$this->db->CommitTrans();

			return true;
		} else {
			Debug::Text( ' New Object ID is already in the tree', __FILE__, __LINE__, __METHOD__, 10 );

			return false;
		}
	}

	/**
	 * Flex requires that all index keys start at 0, even in the children section,
	 * So we need to handle that as well so Flex doesn't need any post processing.
	 * @param $nodes
	 * @param bool $include_root
	 * @return array
	 */
	static function FormatFlexArray( $nodes, $include_root = true ) {
		Debug::Text( ' Formatting Flex Array...', __FILE__, __LINE__, __METHOD__, 10 );
		$nested = [];
		$depths = [];

		if ( is_array( $nodes ) ) {
			foreach ( $nodes as $node ) {
				if ( $node['level'] == 1 ) {

					//Using sequential keys
					$nested[] = $node; //Each new branch of the tree the key should start at 0 and be a sequence without holes.
					end( $nested );
					$depths[( $node['level'] + 1 )] = key( $nested );
					/*
					//Using non-sequential keys:
					$nested[$key] = $node;
					$depths[$node['level'] + 1] = $key;
					*/
				} else {
					$parent =& $nested;
					for ( $i = 2; $i <= $node['level']; $i++ ) {
						//In cases where parent nodes were deleted without reparenting, prevent PHP warning.
						if ( !isset( $depths[$i] ) ) {
							$depths[$i] = 0;
						}

						if ( $i == 2 ) {
							$parent =& $parent[$depths[$i]];
						} else {
							$parent =& $parent['children'][$depths[$i]];
						}
					}

					//Using sequential keys.
					$parent['children'][] = $node; //Each new branch of the tree the key should start at 0 and be a sequence without holes.
					end( $parent['children'] );
					$depths[( $node['level'] + 1 )] = key( $parent['children'] );
					/*
					//Using non-sequential keys:
					$parent['children'][$key] = $node;
					$depths[$node['level'] + 1] = $key;
					*/
				}
			}
		}

		if ( $include_root == true ) {
			return [
					0 => [
							'id'       => 0,
							'name'     => TTi18n::getText( 'Root' ),
							'level'    => 0,
							'children' => $nested,
					],
			];
		} else {
			return $nested;
		}
	}

	/**
	 * @param $nodes
	 * @param string $type
	 * @param bool $include_root
	 * @return array|bool
	 */
	static function FormatArray( $nodes, $type = 'HTML', $include_root = false ) {
		$type = strtolower( $type );

		if ( $include_root === true ) {
			if ( !is_array( $nodes ) ) {
				$nodes = [];
			}

			$root_node = [
					'id'    => 0,
					'name'  => 'Root',
					'level' => 0,
			];

			array_unshift( $nodes, $root_node );
		}

		if ( $nodes === false ) {
			return false;
		}

		$retarr = [];
		foreach ( $nodes as $node ) {
			switch ( $type ) {
				case 'no_tree_text':
					$spacing = str_repeat( '|  &nbsp;', ( $node['level'] * 1 ) );
					$text = $node['name'];
					break;
				case 'text':
					$spacing = str_repeat( '|  &nbsp;', ( $node['level'] * 1 ) );
					$text = $spacing . $node['name'];
					break;
				case 'plain_text':
					$spacing = str_repeat( '|  ', ( $node['level'] * 1 ) );
					$text = $spacing . $node['name'];
					break;
				case 'html':
					$width = ( ( $node['level'] - 1 ) * 20 );
					$spacing = '<img src="' . Environment::getBaseURL() . 'images/s.gif" width="' . $width . '">';
					$text = $spacing . ' ' . $node['name'];
					break;
				case 'array':
					break;
			}

			$node['spacing'] = $spacing;
			$node['text'] = $text;

			$retarr[] = $node;

			unset( $node );
		}

		return $retarr;
	}
}

?>