<?php

class entry_cached_index extends caching_SBPT {
 // cache_filelister {
	var $position = 0;

	var $nodesize = 30;

	var $keylen = 12;

	/**
	 * opens the index belonging to a given category
	 *
	 * @params int $id_cat
	 */
	function __construct($id_cat = 0) {
		$F = INDEX_DIR . 'index-' . $id_cat . '.dat';
		
		if (!file_exists($F)) {
			trigger_error("Can't find index '{$F}'", E_USER_ERROR);
		}
		
		parent::__construct(fopen($F, 'rb'), fopen(INDEX_DIR . 'index.strings.dat', 'rb'), 256, $this->position, $this->nodesize, $this->keylen);
		
		$this->open();
	}
	
}

class entry_index {

	var $indices = array();

	var $_offset = 0;

	var $_chunksize = 30;

	var $_keysize = 12;

	var $_lock_file = null;

	function __construct() {
		$this->_lock_file = CACHE_DIR . 'bpt.lock';
		
		$this->catlist = entry_categories_list();
		
		// only main index s a SBPlus (string BPlus):
		// the other (other categories) are managed
		// as if they were simple BPlus trees, so
		// values in key,value pairs won't
		// be strings but integers
		//
		// the integer will be the seek position
		// in the SBPlus' string file
		//
		// they'll be loaded back with the string file
		// as SBPlus trees: the string-key, string-value pair
		// will be returned
		
		if ($oldfile = file_exists($f = INDEX_DIR . 'index-0.dat'))
			$mode = 'r+b';
		else
			$mode = 'w+b';
		
		$this->indices [0] = new SBPlusTree(fopen($f, $mode), fopen(INDEX_DIR . 'index.strings.dat', $mode), 256, $this->_offset, $this->_chunksize, $this->_keysize);
		
		if ($oldfile)
			$this->indices [0]->open();
		else
			$this->indices [0]->startup();
	}

	function _lock_acquire($exclusive = true, $cat = 0) {
		if (file_exists($this->_lock_file)) {
			trigger_error("Could not acquire write lock on INDEX. " . "Didn't I told you FlatPress is not designed for concurrency, already? ;) " . "Don't worry: your entry has been saved as draft!", E_USER_WARNING);
			return false;
		}
		
		// simulates atomic write by writing to a file, then moving in place
		$tmp = $this->_lock_file . ".tmp";
		if (io_write_file($tmp, 'dummy')) {
			if (rename($tmp, $this->_lock_file)) {
				return true;
			}
		}
		
		return false;
	}

	function _lock_release($cat = 0) {
		if (file_exists($this->_lock_file)) {
			return @unlink($this->_lock_file);
		} else {
			trigger_error("Lock file did not exist: ignoring (index was already unlocked.)", E_USER_NOTICE);
			return 2;
		}
	}

	function &get_index($cat = 0) {
		if (!is_numeric($cat))
			trigger_error("CAT must be an integer ($cat was given)", E_USER_ERROR);
		if (!isset($this->indices [$cat])) {
			$f = INDEX_DIR . 'index-' . $cat . '.dat';
			if ($oldfile = file_exists($f))
				$mode = 'r+b';
			else
				$mode = 'w+b';
			
			$this->indices [$cat] = new BPlusTree(fopen($f, $mode), $this->_offset, $this->_chunksize, $this->_keysize);
			if ($oldfile)
				$this->indices [$cat]->open();
			else
				$this->indices [$cat]->startup();
		}
		return $this->indices [$cat];
	}

	function add($id, $entry, $del = array(), $update_title = true) {
		$key = entry_idtokey($id);
		$val = $entry ['subject'];
		
		if (!$this->_lock_acquire())
			return false; // we're DOOMED!
		
		$main = & $this->get_index();
		$seek = null;
		
		// title must not be updated, let's get the offset value from has_key
		if (!$update_title)
			$seek = $main->has_key($key, $val);
		
		// if seek is null, then there is no such key, and we must set it
		// in the main index
		if (!is_numeric($seek))
			$seek = $main->setitem($key, $val);
		
		// key has been set, let's set the other indices (if any), and link them
		// to the title string using $seek
		
		if (isset($entry ['categories']) && is_array($entry ['categories'])) {
			
			$categories = array();
			
			foreach ($entry ['categories'] as $cat) {
				
				// skip non-numeric special categories (such as 'draft')
				if (!is_numeric($cat))
					continue;
				
				$categories [] = $cat;
				
				// traverse the full cat tree (in linearized form)
				// to update categories which eventually aren't
				// explicitly listed
				while ($parent = $this->catlist [$cat]) {
					$categories [] = $parent;
					$cat = $parent;
				}
			}
			
			// delete any duplicate
			$categories = array_unique($categories);
			
			foreach ($categories as $cat) {
				$this_index = & $this->get_index($cat);
				$this_index->setitem($key, $seek);
			}
		}
		
		// if the set of indices changed, some might have to be deleted
		if ($del) {
			foreach ($del as $cat) {
				// echo 'DEL '. $cat,"\n";
				if (!is_numeric($cat))
					continue;
				$this_index = & $this->get_index($cat);
				$this_index->delitem($key);
			}
		}
		
		return $this->_lock_release();
	}

	function delete($id, $entry) {
		$key = entry_idtokey($id);
		
		if (!$this->_lock_acquire())
			return false; // we're DOOMED!
		
		$main = & $this->get_index();
		$main->delitem($key);
		
		if (isset($entry ['categories']) && is_array($entry ['categories'])) {
			foreach ($entry ['categories'] as $cat) {
				if (!is_numeric($cat))
					continue;
				$this_index = & $this->get_index($cat);
				if ($this_index->has_key($key))
					$this_index->delitem($key);
			}
		}
		
		return $this->_lock_release();
	}
	
}

class entry_archives extends fs_filelister {

	var $_directory = CONTENT_DIR;

	var $_y = null;

	var $_m = null;

	var $_d = null;

	var $_count = 0;

	var $_filter = 'entry*';

	function __construct($y, $m = null, $d = null) {
		$this->_y = $y;
		$this->_m = $m;
		$this->_d = $d;
		
		$this->_directory .= "$y/";
		
		if ($m) {
			
			$this->_directory .= "$m/";
			
			if ($d) {
				$this->_filter = "entry$y$m$d*";
			}
		}
		
		return parent::__construct();
	}

	function _checkFile($directory, $file) {
		$f = "$directory/$file";
		if (is_dir($f) && ctype_digit($file)) {
			return 1;
		}
		
		if (fnmatch($this->_filter . EXT, $file)) {
			$id = basename($file, EXT);
			$this->_count++;
			array_push($this->_list, $id);
			return 0;
		}
	}

	function getList() {
		rsort($this->_list);
		return parent::getList();
	}

	function getCount() {
		return $this->_count;
	}
	
}

// //work in progress
// class entry {

// var $_indexer;
// var $id;

// function entry($id, $content) {
// //$this->_indexer =& $indexer;
// }

// function get($field) {
// $field = strtolower($field);
// if (!isset($this->$field)) {
// // if it is not set
// // tries to fetch from the database
// $arr = entry_parse($id);
// while(list($field, $val) = each($arr))
// $this->$field = $val;

// // if still is not set raises an error
// if (!isset($this->$field))
// trigger_error("$field is not set", E_USER_NOTICE);
// return;

// }

// return $this->$field;

// }

// function set($field, $val) {
// $field = strtolower($field);
// $this->$field = $val;
// }

// }

/**
 * function entry_init
 * fills the global array containing the entry object
 */
function &entry_init() {
	
	// global $fpdb;
	// $fpdb->init();
	static $entry_index = null;
	
	if (is_null($entry_index))
		$entry_index = new entry_index();
	
	return $entry_index;
}

function &entry_cached_index($id_cat) {
	$F = INDEX_DIR . 'index-' . $id_cat . '.dat';
	
	if (!file_exists($F)) {
		$o = false;
	} else {
		$o = new entry_cached_index($id_cat);
	}
	
	return $o;
}

/*
 * function entry_query($params=array()){
 *
 * global $fpdb;
 * $queryid = $fpdb->query($params);
 * $fpdb->doquery($queryid);
 *
 *
 * }
 *
 * function entry_hasmore() {
 * global $fpdb;
 * return $fpdb->hasmore();
 *
 * }
 *
 * function entry_get() {
 * $fpdb->get();
 * }
 */
function entry_keytoid($key) {
	$date = substr($key, 0, 6);
	$time = substr($key, 6);
	return "entry{$date}-{$time}";
}

function entry_idtokey($id) {
	return substr($id, 5, 6) . substr($id, 12);
}

function entry_timetokey($time) {
	return date('ymdHis', $time);
}

function entry_keytotime($key) {
	$arr ['y'] = substr($key, 0, 2);
	$arr ['m'] = substr($key, 2, 2);
	$arr ['d'] = substr($key, 4, 2);
	
	$arr ['H'] = substr($key, 6, 2);
	$arr ['M'] = substr($key, 8, 2);
	$arr ['S'] = substr($key, 10, 2);
	
	return mktime($arr ['H'], $arr ['M'], $arr ['S'], $arr ['m'], $arr ['d'], $arr ['y']);
}

function entry_idtotime($id) {
	$date = date_from_id($id);
	return $date ['time'];
}

function entry_list() {
	trigger_error('function deprecated', E_USER_ERROR);
	
	$obj = & entry_init();
	
	$entry_arr = $obj->getList();
	
	if ($entry_arr) {
		krsort($entry_arr);
		return $entry_arr;
	}
}

function entry_exists($id) {
	$f = entry_dir($id) . EXT;
	return file_exists($f) ? $f : false;
}

function entry_dir($id, $month_only = false) {
	if (!preg_match('|^entry[0-9]{6}-[0-9]{6}$|', $id))
		return false;
	$date = date_from_id($id);
	if ($month_only)
		$f = CONTENT_DIR . "{$date['y']}/{$date['m']}/";
	else
		$f = CONTENT_DIR . "{$date['y']}/{$date['m']}/$id";
	return $f;
}

function entry_parse($id, $raw = false) {
	$f = entry_exists($id);
	if (!$f)
		return array();
	
	$fc = io_load_file($f);
	
	if (!$fc)
		return array();
	
	$arr = utils_kexplode($fc);
	
	// propagates the error if entry does not exist
	
	if (isset($arr ['categories']) && // fix to bad old behaviour:
	(trim($arr ['categories']) != '')) {
		
		$cats = (array) explode(',', $arr ['categories']);
		$arr ['categories'] = (array) $cats;
	} else
		$arr ['categories'] = array();
	
	// if (!is_array($arr['categories'])) die();
	
	if (!isset($arr ['AUTHOR'])) {
		global $fp_config;
		$arr ['AUTHOR'] = $fp_config ['general'] ['author'];
	}
	
	if ($raw)
		return $arr;
	return $arr;
}

/**
 * function entry_get_comments
 *
 * @param
 *        	string id entry id
 * @param
 *        	array entry entry content array by ref; 'commentcount' field is added to the array
 *        	
 * @return object comment_indexer as reference
 *        
 */
function &entry_get_comments($id, &$count) {
	$obj = new comment_indexer($id);
	
	$count = count($obj->getList());
	
	return $obj;
}

function entry_categories_encode($cat_file) {
	
	// if ($string = io_load_file(CONTENT_DIR . 'categories.txt')) {
	$lines = explode("\n", trim($cat_file));
	$idstack = $result = $indentstack = array();
	
	while (!empty($lines)) {
		
		$v = array_pop($lines);
		
		$vt = trim($v);
		
		if ($vt) {
			
			$text = '';
			$indent = utils_countdashes($vt, $text);
			
			$val = explode(':', $text);
			
			$id = trim($val [1]);
			$label = trim($val [0]);
			
			// IDs must be strictly positive
			
			if ($label && $id <= 0)
				return -1;
			
			if (empty($indentstack)) {
				array_push($indentstack, $indent);
				array_push($idstack, $id);
				$indent_old = $indent;
			} else {
				$indent_old = end($indentstack);
			}
			
			if ($indent < $indent_old) {
				array_push($indentstack, $indent);
				array_push($idstack, $id);
			} elseif ($indent > $indent_old) {
				$idstack = array(
					$id
				);
				$indentstack = array(
					$indent
				);
			} else {
				array_pop($idstack);
				$idstack = array(
					$id
				);
			}
			
			$result ['rels'] [$id] = $idstack;
			$result ['defs'] [$id] = $label;
		}
	}
	
	ksort($result ['rels']);
	ksort($result ['defs']);
	
	// print_r($result);
	
	return io_write_file(CONTENT_DIR . 'categories_encoded.dat', serialize($result));
	
	// }
	
	return false;
}

/*
 *
 * function entry_categories_print(&$lines, &$indentstack, &$result, $params) {
 *
 *
 * }
 *
 */
function entry_categories_list() {
	if (!$string = io_load_file(CONTENT_DIR . 'categories.txt'))
		return false;
	
	$lines = explode("\n", trim($string));
	$idstack = array(
		0
	);
	$indentstack = array();
	
	// $categories = array(0=>null);
	$lastindent = 0;
	$lastid = 0;
	$parent = 0;
	
	$NEST = 0;
	
	foreach ($lines as $v) {
		
		$vt = trim($v);
		
		if (!$vt)
			continue;
		
		$text = '';
		$indent = utils_countdashes($vt, $text);
		
		$val = explode(':', $text);
		
		$id = trim($val [1]);
		$label = trim($val [0]);
		
		// echo "PARSE: $id:$label\n";
		if ($indent > $lastindent) {
			// echo "INDENT ($indent, $id, $lastid)\n";
			$parent = $lastid;
			array_push($indentstack, $lastindent);
			array_push($idstack, $lastid);
			$lastindent = $indent;
			$NEST++;
		} elseif ($indent < $lastindent) {
			// echo "DEDENT ($indent)\n";
			do {
				$dedent = array_pop($indentstack);
				array_pop($idstack);
				$NEST--;
			} while ($dedent > $indent);
			if ($dedent < $indent)
				return false; // trigger_error("failed parsing ($dedent<$indent)", E_USER_ERROR);
			$parent = end($idstack);
			$lastindent = $indent;
			$lastid = $id;
		}
		
		$lastid = $id;
		// echo "NEST: $NEST\n";
		
		$categories [$id] = $parent;
	}
	
	return $categories;
}

function entry_categories_get($what = null) {
	global $fpdb;
	
	$categories = array();
	
	if (!empty($fpdb->_categories)) {
		$categories = $fpdb->_categories;
	} else {
		
		$f = CONTENT_DIR . 'categories_encoded.dat';
		if (file_exists($f)) {
			if ($c = io_load_file($f))
				$categories = unserialize($c);
		}
	}
	
	if ($categories) {
		
		if ($what == 'defs' || $what == 'rels')
			return $categories [$what];
		else
			return $categories;
	}
	return array();
}

/**
 * flags are actually special categories
 * which are usually hidden.
 *
 * they can be set when editing your entries
 * to let flatpress perform special actions
 *
 * draft: Draft entry (hidden, awaiting publication)
 * static: Static entry (allows saving an alias, so you can reach it with
 * ?page=myentry)
 * commslock: Comments locked (comments disallowed for this entry)
 */
function entry_flags_get() {
	return array(
		'draft',
		// 'static',
		'commslock'
	);
}

// @TODO : check against schema ?
function entry_prepare(&$entry) { // prepare for serialization
	global $post;
	
	// fill in missing value
	if (!isset($entry ['date'])) {
		$entry ['date'] = date_time();
	}
	
	// import into global scope
	$post = $entry;
	
	// apply *_pre filters
	$entry ['content'] = apply_filters('content_save_pre', $entry ['content']);
	$entry ['subject'] = apply_filters('title_save_pre', $entry ['subject']);
	
	// prepare for serialization
	if (isset($entry ['categories'])) {
		
		if (!is_array($entry ['categories'])) {
			trigger_error("Expected 'categories' to be an array, found " . gettype($entry ['categories']), E_USER_WARNING);
			$entry ['categories'] = array();
		}
	} else {
		$entry ['categories'] = array();
	}
	
	return $entry;
}

/**
 *
 * @param
 *        	array entry contents
 * @param
 *        	string|null entry id, null if can be deducted from the date field of $entry;
 *        	defaults to null
 *        	
 * @param
 *        	bool updates entry index; defaults to true
 *        	
 *        	
 * @return integer -1 failure while storing preliminar draft, abort. Index not touched.
 *         -2 index updated succesfully, but draft doesn't exist anymore
 *         (should never happen!) OR
 *         failure while trying to move draft to entry path, draft does not exist anymore
 *         index not touched
 *         -3 error while moving draft still exists, index written succesfully but rolled back
 *         -4 failure while saving to index, aborted (draft still exists)
 *        
 *        
 */
function entry_save($entry, $id = null, $update_index = true) {
	
	// PHASE 1 : prepare entry
	if (!$id) {
		if (!@$entry ['date'])
			$entry ['date'] = date_time();
		$id = bdb_idfromtime(BDB_ENTRY, $entry ['date']);
	}
	
	// PHASE 2 : Store
	
	// secure data as DRAFT
	// (entry is also implicitly entry_prepare()'d here)
	$ret = draft_save($entry, $id);
	do_action('publish_post', $id, $entry);
	
	if ($ret === false) {
		return -1; // FAILURE: ABORT
	}
	
	// PHASE 3 : Update index
	$delete_cats = array();
	$all_cats = @$entry ['categories'];
	$update_title = true;
	if ($old_entry = entry_parse($id)) {
		if ($all_cats) {
			$delete_cats = array_diff($old_entry ['categories'], $all_cats);
		}
		$all_cats = $all_cats ? array_merge($all_cats, $old_entry ['categories']) : $old_entry ['categories'];
		$update_title = $entry ['subject'] != $old_entry ['subject'];
	}
	
	/*
	 * echo 'old';
	 * print_r($old_entry['categories']);
	 * echo 'new';
	 * print_r($entry['categories']);
	 * echo 'del';
	 * print_r($delete_cats);
	 * echo 'all';
	 * print_r($all_cats);
	 */
	
	$INDEX = & entry_init();
	$ok = ($update_index) ? $INDEX->add($id, $entry, $delete_cats, $update_title) : true;
	
	// PHASE 4 : index updated; let's move back the entry
	if ($ok) {
		
		$entryd = entry_dir($id, true);
		$entryf = $entryd . $id . EXT;
		$draftf = draft_exists($id);
		if ($draftf === false) { // this should never happen!
			if ($update_index) {
				$INDEX->delete($id, $all_cats);
			}
			return -2;
		}
		
		fs_delete($entryf);
		fs_mkdir($entryd);
		$ret = rename($draftf, $entryf);
		
		if (!$ret) {
			if (draft_exists($id)) {
				// rollback changes in the index
				// (keep the draft file)
				if ($update_index) {
					$INDEX->delete($id, $all_cats);
				}
				return -3;
			} else {
				return -2;
			}
		} else {
			// SUCCESS : delete draft, move comments along
			draft_to_entry($id);
			return $id;
		}
	}
	return -4;
}

function entry_delete($id) {
	if (!$f = entry_exists($id))
		return;
	
	/*
	 * $d = bdb_idtofile($id,BDB_COMMENT);
	 * fs_delete_recursive("$d");
	 *
	 * // thanks to cimangi for noticing this
	 * $f = dirname($d) . '/view_counter' .EXT;
	 * fs_delete($f);
	 *
	 *
	 * $f = bdb_idtofile($id);
	 */
	
	$d = entry_dir($id);
	fs_delete_recursive($d);
	
	$obj = & entry_init();
	$obj->delete($id, entry_parse($id));
	
	do_action('delete_post', $id);
	
	return fs_delete($f);
}

function entry_purge_cache() {
	$obj = & entry_init();
	$obj->purge();
}
// add_action('init',

?>
