<?php
/**
 * This file is part of phpMorphy library
 *
 * Copyright c 2007-2008 Kamaev Vladimir <heromantor@users.sourceforge.net>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the
 * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
 * Boston, MA 02111-1307, USA.
 */

interface phpMorphy_Fsa_Interface {
	/**
	 * Return root transition of fsa
	 * @return array
	 */
	function getRootTrans();
	
	/**
	 * Returns root state object
	 * @return
	 */
	function getRootState();
	
	/**
	 * Returns alphabet i.e. all chars used in automat
	 * @return array
	 */
	function getAlphabet();
	
	/**
	 * Return annotation for given transition(if annotation flag is set for given trans)
	 *
	 * @param array $trans
	 * @return string
	 */
	function getAnnot($trans);
	
	/**
	 * Find word in automat
	 *
	 * @param mixed $trans starting transition
	 * @param string $word
	 * @param bool $readAnnot read annot or simple check if word exists in automat
	 * @return bool TRUE if word is found, FALSE otherwise
	 */
	function walk($trans, $word, $readAnnot = true);
	
	/**
	 * Traverse automat and collect words
	 * For each found words $callback function invoked with follow arguments:
	 * call_user_func($callback, $word, $annot)
	 * when $readAnnot is FALSE then $annot arg is always NULL
	 *
	 * @param mixed $startNode
	 * @param mixed $callback callback function(in php format callback i.e. string or array(obj, method) or array(class, method)
	 * @param bool $readAnnot read annot
	 * @param string $path string to be append to all words
	 */
	function collect($startNode, $callback, $readAnnot = true, $path = '');
	
	/**
	 * Read state at given index
	 *
	 * @param int $index
	 * @return array
	 */
	function readState($index);
	
	/**
	 * Unpack transition from binary form to array
	 *
	 * @param mixed $rawTranses may be array for convert more than one transitions
	 * @return array
	 */
	function unpackTranses($rawTranses);
}

abstract class phpMorphy_Fsa implements phpMorphy_Fsa_Interface {
	const HEADER_SIZE = 60;
	
	protected
		$resource,
		$header,
		$fsa_start,
		$root_trans,
		$alphabet;	
	
	protected function phpMorphy_Fsa($resource, $header) {
		$this->resource = $resource;
		$this->header = $header;
		$this->fsa_start = $header['fsa_offset'];
		$this->root_trans = $this->readRootTrans();
	}

	// static
	static function create(phpMorphy_Storage $storage) {
		$header = phpMorphy_Fsa::readHeader(
			$storage->read(0, self::HEADER_SIZE)
		);
		
		if(!phpMorphy_Fsa::validateHeader($header)) {
			throw new phpMorphy_Exception('Invalid fsa format');
		}
		
		if($header['flags']['is_sparse']) {
			$type = 'sparse';
		} else if($header['flags']['is_tree']) {
			$type = 'tree';
		} else {
			throw new phpMorphy_Exception('Only sparse or tree fsa`s supported');
		}
		
		$storage_type = phpMorphy_Fsa::getStorageString($storage->getType());
		$file_path = dirname(__FILE__) . "/access/fsa_{$type}_{$storage_type}.php";
		$clazz = 'phpMorphy_Fsa_' . ucfirst($type) . '_' . ucfirst($storage_type);
		
		require_once($file_path);
		return new $clazz(
			$storage->getResource(),
			$header
		);
	}
	
	function getRootTrans() { return $this->root_trans; }
	
	function getRootState() {
		return $this->createState($this->getRootStateIndex());
	}
	
	function getAlphabet() {
		if(!isset($this->alphabet)) {
			$this->alphabet = str_split($this->readAlphabet());
		}
		
		return $this->alphabet;
	}
	
	protected function createState($index) {
		require_once(PHPMORPHY_DIR . '/fsa/fsa_state.php');
		return new phpMorphy_State($this, $index);
	}
	
	static protected function readHeader($headerRaw) {
		if(strlen($headerRaw) != self::HEADER_SIZE) {
			throw new phpMorphy_Exception('Invalid header string given');
		}
		
		$fields = array(
			'fourcc' => 'a4',
			'ver' => 'V',
			'flags' => 'V',
			
			'alphabet_offset' => 'V',
			'fsa_offset' => 'V',
			'annot_offset' => 'V',
			
			'alphabet_size' => 'V',
			'transes_count' => 'V',
			
			'annot_size_len' => 'V',
			'annot_chunk_size' => 'V',
			'annot_chunks_count' => 'V',
			
			'char_size' => 'V',
			'padding_size' => 'V',
			'dest_size' => 'V',
			'hash_size' => 'V',
		);
		
		// build line
		$unpack_str = '';
		foreach($fields as $k => $v) {
			$unpack_str .= "$v$k/";
		}
		
		$header = unpack(substr($unpack_str, 0, -1), $headerRaw);

		if(false === $header) {
			throw new phpMorphy_Exception('Can`t unpack header');
		}

		$flags = array();
		$raw_flags = $header['flags'];
		$flags['is_tree'] =  $raw_flags & 0x01 ? true : false;
		$flags['is_hash'] =  $raw_flags & 0x02 ? true : false;
		$flags['is_sparse'] = $raw_flags & 0x04 ? true : false;
		$flags['is_be'] =  $raw_flags & 0x08 ? true : false;

		$header['flags'] = $flags;
		
		return $header;
	}
	
	// static
	static protected function validateHeader($header) {
		if(
			'meal' != $header['fourcc'] ||
			2 != $header['ver'] ||
			$header['char_size'] != 1 ||
			$header['padding_size'] > 0 ||
			$header['dest_size'] != 3 ||
			$header['hash_size'] != 0 ||
			$header['annot_size_len'] != 1 ||
			$header['annot_chunk_size'] != 1 ||
			$header['flags']['is_be'] ||
			$header['flags']['is_hash'] ||
			1 == 0
		) {
			return false;
		}
		
		return true;
	}
	
	// static
	static protected function getStorageString($type) {
		$types_map = array(
			PHPMORPHY_STORAGE_FILE => 'file',
			PHPMORPHY_STORAGE_MEM => 'mem',
			PHPMORPHY_STORAGE_SHM => 'shm'
		);
		
		if(!isset($types_map[$type])) {
			throw new phpMorphy_Exception('Unsupported storage type ' . $storage->getType());
		}
		
		return $types_map[$type];
	}
	
	protected function getRootStateIndex() { return 0; }
	
	abstract protected function readRootTrans();
	abstract protected function readAlphabet();
};

class phpMorphy_Fsa_Decorator implements phpMorphy_Fsa_Interface {
	protected $fsa;
	
	function phpMorphy_Fsa_Decorator(phpMorphy_Fsa_Interface $fsa) {
		$this->fsa = $fsa;
	}
	
	function getRootTrans() { return $this->fsa->getRootTrans(); }
	function getRootState() { return $this->fsa->getRootState(); }
	function getAlphabet() { return $this->fsa->getAlphabet(); }
	function getAnnot($trans) { return $this->fsa->getAnnot($trans); }
	function walk($start, $word, $readAnnot = true) { return $this->fsa->walk($start, $word, $readAnnot); }
	function collect($start, $callback, $readAnnot = true, $path = '') { return $this->fsa->collect($start, $callback, $readAnnot, $path); }
	function readState($index) { return $this->fsa->readState($index); }
	function unpackTranses($transes) { return $this->fsa->unpackTranses($transes); }
};

class phpMorphy_Fsa_WordsCollector {
	protected
		$items = array(),
		$limit;
	
	function phpMorphy_Fsa_WordsCollector($collectLimit) {
		$this->limit = $collectLimit;
	}
	
	function collect($word, $annot) {
		if(count($this->items) < $this->limit) {
			$this->items[$word] = $annot;
			return true;
		} else {
			return false;
		}
	}
	
	function getItems() { return $this->items; }
	function clear() { $this->items = array(); }
	function getCallback() { return array($this, 'collect'); }
};
