<?
class tree {
	// Structure table and fields
	var $s_table	= "";
	var $s_fields	= array(
			"id"		=> false,
			"parent_id"	=> false,
			"position"	=> false,
			"left"		=> false,
			"right"		=> false,
			"level"		=> false
		);

	// Additional fields (stored in format `table_name.field_name`)
	var $d_fields	= array();

	// Tree type (or types)
	var $adjacency	= false;
	var $nestedset	= false;

	// Database
	var $db			= false;

	// Constructor
	function __construct($tables = array()) {
		if(!is_array($tables) || !count($tables)) return;
		foreach($tables as $table_name => $fields) {
			if(is_array($fields)) {
				foreach($fields as $key => $field) {
					switch($key) {
						case "id":
						case "parent_id":
						case "position":
						case "left":
						case "right":
						case "level":
							$this->s_table = $table_name;
							$this->s_fields[$key] = $field;
							break;
						default:
							$this->d_fields[] = $table_name.".".$field;
							break;
					}
				}
			}
		}

		// Determine what kind of a tree is used (or both)
		if($this->s_fields["id"] && $this->s_fields["position"])														$this->adjacency = true;
		if($this->s_fields["id"] && $this->s_fields["left"] && $this->s_fields["right"] && $this->s_fields["level"])	$this->nestedset = true;

		// Database
		$this->db = new DB;
	}
	function tree($tables = array()) { return $this->__construct($tables); } // PHP 4 compatibilty

	// WRITING FUNCTIONS
	// Function for moving nodes
	// ID is the node that is being moved - 0 is creating a new NODE
	// REF_ID is the reference node in the move
	// TYPE is one of "after", "before" or "inside"
	function move($id, $ref_id, $type, $mode = "move") {
		if(!in_array($type, array("after", "before", "inside"))) return false;

		// Queries executed at the end
		$sql	= array();

		if(!(int)$id) $mode = "create";

		if($mode == "create") {
			// Fields and values that will be inserted
			$fields	= array();
			$values	= array();
			// Inserting an ID
			$fields[] = "`".$this->s_fields["id"]."`";
			$values[] = "NULL";
		}

		// If the tree maintains an ID->PARENT_ID relation
		if($this->adjacency) {
			$this->db->query("SELECT `".$this->s_fields["parent_id"]."`, `".$this->s_fields["position"]."` FROM `".$this->s_table."` WHERE `".$this->s_fields["id"]."` = ".(int)$ref_id);
			$this->db->nextr();

			// Determine new parent and position
			if($type == "inside") {
				$new_parent_id = $ref_id;
				$new_position = 1;
			}
			else {
				$new_parent_id = (int)$this->db->f(0);
				if($type == "before")	$new_position = $this->db->f(1);
				if($type == "after")	$new_position = $this->db->f(1) + 1;
			}

			// Cleanup old parent
			if($mode == "create") {
				$old_parent_id	= -1;
				$old_position	= 0;
			}
			else {
				$this->db->query("SELECT `".$this->s_fields["parent_id"]."`, `".$this->s_fields["position"]."` FROM `".$this->s_table."` WHERE `".$this->s_fields["id"]."` = ".(int)$id);
				$this->db->nextr();
				$old_parent_id	= $this->db->f(0);
				$old_position	= $this->db->f(1);
			}

			// A reorder was made
			if($old_parent_id == $new_parent_id) {
				if($new_position > $old_position) {
					$new_position = $new_position - 1;
					$sql[] = "UPDATE `".$this->s_table."` SET `".$this->s_fields["position"]."` = `".$this->s_fields["position"]."` - 1 WHERE `".$this->s_fields["parent_id"]."` = ".$old_parent_id." AND `".$this->s_fields["position"]."` BETWEEN ".($old_position + 1)." AND ".$new_position;
				}
				if($new_position < $old_position) {
					$sql[] = "UPDATE `".$this->s_table."` SET `".$this->s_fields["position"]."` = `".$this->s_fields["position"]."` + 1 WHERE `".$this->s_fields["parent_id"]."` = ".$old_parent_id." AND `".$this->s_fields["position"]."` BETWEEN ".$new_position." AND ".($old_position - 1);
				}
			}
			else {
				// Fix old parent (move siblings up)
				$sql[] = "UPDATE `".$this->s_table."` SET `".$this->s_fields["position"]."` = `".$this->s_fields["position"]."` - 1 WHERE `".$this->s_fields["parent_id"]."` = ".$old_parent_id." AND `".$this->s_fields["position"]."` > ".$old_position;
				// Prepare new parent (move sibling down)
				$sql[] = "UPDATE `".$this->s_table."` SET `".$this->s_fields["position"]."` = `".$this->s_fields["position"]."` + 1 WHERE `".$this->s_fields["parent_id"]."` = ".$new_parent_id." AND `".$this->s_fields["position"]."` >".($type != "after" ? "=" : "")." ".$new_position;
			}
			// Move the node to the new position
			if($mode == "create") {
				$fields[] = "`".$this->s_fields["parent_id"]."`";
				$fields[] = "`".$this->s_fields["position"]."`";
				$values[] = $new_parent_id;
				$values[] = $new_position;
			}
			else {
				$sql[] = "UPDATE `".$this->s_table."` SET `".$this->s_fields["position"]."` = ".$new_position.", `".$this->s_fields["parent_id"]."` = ".$new_parent_id." WHERE `".$this->s_fields["id"]."` = ".(int)$id;
			}
		}

		// If the tree maintains a nested set
		if($this->nestedset) {
			$this->db->query("SELECT `".$this->s_fields["id"]."` AS id, `".$this->s_fields["left"]."` AS lft, `".$this->s_fields["right"]."` AS rgt, `".$this->s_fields["level"]."` AS lvl FROM `".$this->s_table."` WHERE `".$this->s_fields["id"]."` IN(".(int)$id.",".(int)$ref_id.")");
			while($this->db->nextr()) {
				if($id == $this->db->f("id")) {
					$nod_lft = (int)$this->db->f("lft");
					$nod_rgt = (int)$this->db->f("rgt");
					$dif = $nod_rgt - $nod_lft + 1;
				}
				if($ref_id == $this->db->f("id")) {
					$ref_lft = (int)$this->db->f("lft");
					$ref_rgt = (int)$this->db->f("rgt");
					$ref_lvl = (int)$this->db->f("lvl");
				}
			}

			if($mode == "move") {
				$sql[] = "UPDATE `".$this->s_table."` SET `".$this->s_fields["left"]."` = `".$this->s_fields["left"]."` - ".$dif." WHERE `".$this->s_fields["left"]."` > ".$nod_rgt;
				$sql[] = "UPDATE `".$this->s_table."` SET `".$this->s_fields["right"]."` = `".$this->s_fields["right"]."` - ".$dif." WHERE `".$this->s_fields["right"]."` > ".$nod_rgt;
				if($ref_lft > $nod_rgt) $ref_lft -= $dif;
				if($ref_rgt > $nod_rgt) $ref_rgt -= $dif;
			}
			else $dif = 2;

			$ids = array();
			if($mode == "move") {
				$this->db->query("SELECT `".$this->s_fields["id"]."` FROM `".$this->s_table."` WHERE `".$this->s_fields["left"]."` >= ".$nod_lft." AND `".$this->s_fields["right"]."` <= ".$nod_rgt);
				while($this->db->nextr()) $ids[] = (int)$this->db->f(0);
			} 
			else $ids[] = -1;

			switch($type) {
				case "before":
					$sql[] = "UPDATE `".$this->s_table."` SET `".$this->s_fields["left"]."` = `".$this->s_fields["left"]."` + ".$dif." WHERE `".$this->s_fields["left"]."` >= ".$ref_lft." AND `".$this->s_fields["id"]."` NOT IN(".implode(",",$ids).") ";
					$sql[] = "UPDATE `".$this->s_table."` SET `".$this->s_fields["right"]."` = `".$this->s_fields["right"]."` + ".$dif." WHERE `".$this->s_fields["right"]."` > ".$ref_lft." AND `".$this->s_fields["id"]."` NOT IN(".implode(",",$ids).") ";
					if($mode == "move") {
						$dif = $ref_lft - $nod_lft;
						$sql[] = "UPDATE `".$this->s_table."` SET `".$this->s_fields["level"]."` = ".(int)$ref_lvl.", `".$this->s_fields["left"]."` = `".$this->s_fields["left"]."` + (".$dif."), `".$this->s_fields["right"]."` = `".$this->s_fields["right"]."` + (".$dif.") WHERE `".$this->s_fields["id"]."` IN (".implode(",",$ids).") ";
					}
					else {
						$fields[] = "`".$this->s_fields["level"]."`";
						$fields[] = "`".$this->s_fields["left"]."`";
						$fields[] = "`".$this->s_fields["right"]."`";
						$values[] = (int)$ref_lvl;
						$values[] = (int)$ref_lft;
						$values[] = ((int)$ref_lft + 2);
					}
					break;
				case "after":
					$sql[] = "UPDATE `".$this->s_table."` SET `".$this->s_fields["left"]."` = `".$this->s_fields["left"]."` + ".$dif." WHERE `".$this->s_fields["left"]."` > ".$ref_rgt." AND `".$this->s_fields["id"]."` NOT IN(".implode(",",$ids).") ";
					$sql[] = "UPDATE `".$this->s_table."` SET `".$this->s_fields["right"]."` = `".$this->s_fields["right"]."` + ".$dif." WHERE `".$this->s_fields["right"]."` > ".$ref_rgt." AND `".$this->s_fields["id"]."` NOT IN(".implode(",",$ids).") ";
					if($mode == "move") {
						$dif = ($ref_rgt + 1) - $nod_lft;
						$sql[] = "UPDATE `".$this->s_table."` SET `".$this->s_fields["level"]."` = ".(int)$ref_lvl.", `".$this->s_fields["left"]."` = `".$this->s_fields["left"]."` + (".$dif."), `".$this->s_fields["right"]."` = `".$this->s_fields["right"]."` + (".$dif.") WHERE `".$this->s_fields["id"]."` IN (".implode(",",$ids).") ";
					} else {
						$fields[] = "`".$this->s_fields["level"]."`";
						$fields[] = "`".$this->s_fields["left"]."`";
						$fields[] = "`".$this->s_fields["right"]."`";
						$values[] = (int)$ref_lvl;
						$values[] = ((int)$ref_rgt + 1);
						$values[] = ((int)$ref_rgt + 3);
					}
					break;
				case "inside":
				default:
					$sql[] = "UPDATE `".$this->s_table."` SET `".$this->s_fields["left"]."` = `".$this->s_fields["left"]."` + ".$dif." WHERE `".$this->s_fields["left"]."` > ".$ref_lft." AND `".$this->s_fields["id"]."` NOT IN(".implode(",",$ids).") ";
					$sql[] = "UPDATE `".$this->s_table."` SET `".$this->s_fields["right"]."` = `".$this->s_fields["right"]."` + ".$dif." WHERE `".$this->s_fields["right"]."` > ".$ref_lft." AND `".$this->s_fields["id"]."` NOT IN(".implode(",",$ids).") ";
					if($mode == "move") {
						$dif = ($ref_lft + 1) - $nod_lft;
						$sql[] = "UPDATE `".$this->s_table."` SET `".$this->s_fields["level"]."` = ".(int)($ref_lvl + 1).", `".$this->s_fields["left"]."` = `".$this->s_fields["left"]."` + (".$dif."), `".$this->s_fields["right"]."` = `".$this->s_fields["right"]."` + (".$dif.") WHERE `".$this->s_fields["id"]."` IN (".implode(",",$ids).") ";
					}
					else {
						$fields[] = "`".$this->s_fields["level"]."`";
						$fields[] = "`".$this->s_fields["left"]."`";
						$fields[] = "`".$this->s_fields["right"]."`";
						$values[] = ((int)$ref_lvl + 1);
						$values[] = ((int)$ref_lft + 1);
						$values[] = ((int)$ref_lft + 3);
					}
					break;
			}
		}
		
		// If creating a new node
		if($mode == "create") $sql[] = "INSERT INTO `".$this->s_table."` (".implode(",",$fields).") VALUES (".implode(",",$values).")";

		// Applying all changes - there should be a transaction here
		foreach($sql as $q) { $this->db->query($q); }

		if($mode == "create") return mysql_insert_id();
	}

	// Function for removing nodes
	// ID is the node (or array of nodes) that is being removed
	function remove($id) {
		if(is_array($id)) {
			foreach($id as $i) { $this->remove($i); }
			return;
		}
		if(!(int)$id) return false;

		// Take care of nested sets (and adjacency at the same time if applicable)
		if($this->nestedset) {
			$this->db->query("SELECT `".$this->s_fields["left"]."` AS lft, `".$this->s_fields["right"]."` AS rgt ".( ($this->adjacency) ? " , `".$this->s_fields["parent_id"]."` AS pid, `".$this->s_fields["position"]."` AS pos " : "" )." FROM `".$this->s_table."` WHERE `".$this->s_fields["id"]."` = ".(int)$id);
			$this->db->nextr();
			if($this->adjacency) {
				$pid = (int)$this->db->f("pid");
				$pos = (int)$this->db->f("pos");
			}
			$lft = (int)$this->db->f("lft");
			$rgt = (int)$this->db->f("rgt");
			$dif = $rgt - $lft + 1;

			$this->db->query("DELETE FROM `".$this->s_table."` WHERE `".$this->s_fields["left"]."` >= ".$lft." AND `".$this->s_fields["right"]."` <= ".$rgt);
			$this->db->query("UPDATE `".$this->s_table."` SET `".$this->s_fields["left"]."` = `".$this->s_fields["left"]."` - ".$dif." WHERE `".$this->s_fields["left"]."` > ".$rgt);
			$this->db->query("UPDATE `".$this->s_table."` SET `".$this->s_fields["right"]."` = `".$this->s_fields["right"]."` - ".$dif." WHERE `".$this->s_fields["right"]."` > ".$lft);
			if($this->adjacency) {
				$this->db->query("UPDATE `".$this->s_table."` SET `".$this->s_fields["position"]."` = `".$this->s_fields["position"]."` - 1 WHERE `".$this->s_fields["parent_id"]."` = ".$pid." AND `".$this->s_fields["position"]."` > ".$pos);
			}
			return;
		}
		// Only end up here if the tree is adjacency only
		if($this->adjacency) {
			$this->db->query("SELECT `".$this->s_fields["parent_id"]."` AS pid, `".$this->s_fields["position"]."` AS pos FROM `".$this->s_table."` WHERE `".$this->s_fields["id"]."` = ".(int)$id);
			$this->db->nextr();
			$pid = (int)$this->db->f("pid");
			$pos = (int)$this->db->f("pos");

			$tmp = array($id);
			$ids = array($id);
			while(count($tmp)) {
				$t = array_shift($tmp);
				if($t) {
					$this->db->query("SELECT `".$this->s_fields["id"]."` FROM `".$this->s_table."` WHERE `".$this->s_fields["parent_id"]."` = ".(int)$t);
					while($this->db->nextr()) { 
						array_push($ids, $this->db->f(0));
						array_push($tmp, $this->db->f(0));
					}
				}
			}
			$this->db->query("DELETE FROM `".$this->s_table."` WHERE `".$this->s_fields["id"]."` IN (".implode(",",$ids).")");
			$this->db->query("UPDATE `".$this->s_table."` SET `".$this->s_fields["position"]."` = `".$this->s_fields["position"]."` - 1 WHERE `".$this->s_fields["parent_id"]."` = ".$pid." AND `".$this->s_fields["position"]."` > ".$pos);
		}
	}

	function reconstruct() {
		if(!$this->adjacency || !$this->nestedset) return;

		// не знам защо да не е persistent
		$this->db->pcn = false;

		$q = "CREATE TEMPORARY TABLE temp_tree (".$this->s_fields["id"]." INTEGER NOT NULL, ".$this->s_fields["parent_id"]." INTEGER NOT NULL, ". $this->s_fields["position"]." INTEGER NOT NULL) type=HEAP";
		$this->db->query($q);

		$q = "INSERT INTO temp_tree SELECT ".$this->s_fields["id"].", ".$this->s_fields["parent_id"].", ".$this->s_fields["position"]." FROM ".$this->s_table;
		$this->db->query($q);


		$q = "CREATE TEMPORARY TABLE temp_stack (".$this->s_fields["id"]." INTEGER NOT NULL, ".$this->s_fields["left"]." INTEGER, ".$this->s_fields["right"]." INTEGER, ".$this->s_fields["level"]." INTEGER, stack_top INTEGER NOT NULL, ".$this->s_fields["parent_id"]." INTEGER, ".$this->s_fields["position"]." INTEGER) type=HEAP";
		$this->db->query($q);
		$counter = 2;

		$q = "SELECT COUNT(*) as maxcounter FROM temp_tree";
		$this->db->query($q);
		$this->db->nextr();
		$maxcounter = (int) $this->db->f("maxcounter") * 2;
		$currenttop = 1;

		$q = "INSERT INTO temp_stack SELECT ".$this->s_fields["id"].", 1, NULL, 0, 1, ".$this->s_fields["parent_id"].", ".$this->s_fields["position"]." FROM temp_tree WHERE ".$this->s_fields["parent_id"]." = 0";
		$this->db->query($q);

		$q = "DELETE FROM temp_tree WHERE ".$this->s_fields["parent_id"]." = 0";
		$this->db->query($q);

		while ($counter <= $maxcounter) {
			$q = "SELECT temp_tree.".$this->s_fields["id"]." AS tempmin, temp_tree.".$this->s_fields["parent_id"]." AS pid, temp_tree.".$this->s_fields["position"]." AS lid FROM temp_stack, temp_tree WHERE temp_stack.".$this->s_fields["id"]." = temp_tree.".$this->s_fields["parent_id"]." AND temp_stack.stack_top = ".$currenttop." ORDER BY temp_tree.".$this->s_fields["position"]." ASC LIMIT 1";
			$this->db->query($q);

			if ($this->db->nextr()) {
				$tmp = $this->db->f("tempmin");

				$q = "INSERT INTO temp_stack (stack_top, ".$this->s_fields["id"].", ".$this->s_fields["left"].", ".$this->s_fields["right"].", ".$this->s_fields["level"].", ".$this->s_fields["parent_id"].", ".$this->s_fields["position"].") VALUES(".($currenttop + 1).", ".$tmp.", ".$counter.", NULL, ".$currenttop.", ".$this->db->f("pid").", ".$this->db->f("lid").")";
				$this->db->query($q);
				$q = "DELETE FROM temp_tree WHERE ".$this->s_fields["id"]." = ".$tmp;
				$this->db->query($q);
				$counter++;
				$currenttop++;
			}
			else {
				$q = "UPDATE temp_stack SET ".$this->s_fields["right"]." = ".$counter.", stack_top = -stack_top WHERE stack_top = ".$currenttop;
				$this->db->query($q);
				$counter++;
				$currenttop--;
			}
		}

		$q = "TRUNCATE TABLE ".$this->s_table;
		$this->db->query($q);

		$q = "INSERT INTO ".$this->s_table." SELECT ".$this->s_fields["id"].", ".$this->s_fields["parent_id"].", ".$this->s_fields["position"].", ".$this->s_fields["left"].", ".$this->s_fields["right"].", ".$this->s_fields["level"]." FROM temp_stack ORDER BY ".$this->s_fields["id"];
		$this->db->query($q);
	}

	function analyze() {
		$this->errors = array();
		if($this->adjacency) {
			$this->db->query("SELECT COUNT(*) FROM ".$this->s_table." s WHERE ".$this->s_fields["parent_id"]." != 0 AND (SELECT COUNT(*) FROM ".$this->s_table." WHERE ".$this->s_fields["id"]." = s.".$this->s_fields["parent_id"].") = 0 ");
			$this->db->nextr();
			if($this->db->f(0) > 0) $this->errors[] = "Missing parents.";
		}
		if($this->nestedset) {
			$this->db->query("SELECT MAX(".$this->s_fields["right"].") FROM ".$this->s_table);
			$this->db->nextr();
			$n = $this->db->f(0);
			$this->db->query("SELECT COUNT(*) FROM ".$this->s_table);
			$this->db->nextr();
			$c = $this->db->f(0);
			if($n/2 != $c) $this->errors[] = "Right index does not match node count.";
		}
		if($this->adjacency && $this->nestedset) {
			$this->db->query("SELECT COUNT(".$this->s_fields["id"].") FROM ".$this->s_table." s WHERE (SELECT COUNT(*) FROM ".$this->s_table." WHERE ".$this->s_fields["right"]." < s.".$this->s_fields["right"]." AND ".$this->s_fields["left"]." > s.".$this->s_fields["left"]." AND ".$this->s_fields["level"]." = s.".$this->s_fields["level"]." + 1) != (SELECT COUNT(*) FROM ".$this->s_table." WHERE ".$this->s_fields["parent_id"]." = s.".$this->s_fields["id"].") ");
			$this->db->nextr();
			if($this->db->f(0) > 0) $this->errors = "Adjacency and nested set do not match.";
		}
		return $error;
	}
}
?>