<?php


/**
 * Improved MO file reader for Wordpress.
 *
 * @author Thomas Urban <thomas.urban@toxa.de>
 */


include_once( dirname( __FILE__ ) . '/translations.php' );


class MO extends Translations
{

	var $nplurals = 2;

	var $pluralTerm = '';


	var $strOverloaded = false;



	function MO( $headers = array(), $hash = array() )
	{

		$this->headers = $headers;
		$this->entries = $hash;

		$this->strOverloaded = ( ini_get( 'mbstring.func_overload' ) & 2 ) &&
								is_callable( 'mb_substr' );

		if ( $this->headers['Plural-Forms'] )
		{

			$sep  = $this->_strpos( $this->headers['Plural-Forms'], ';' );
			$temp = $this->_substr( $this->headers['Plural-Forms'], 0, $sep );

			if ( preg_match( '/^nplurals\s*=\s*(\d+)$/i', trim( $temp ), $m ) )
			{
				$this->nplurals   = intval( $m[1] );
				$this->pluralTerm = trim( $this->_substr( $temp, $sep + 1 ) );
			}
		}
	}


	function _strlen( $string )
	{
		return $this->strOverloaded ? mb_strlen( $string, 'ascii' )
									: strlen( $string );
	}

	function _strpos( $haystack, $needle, $offset = null )
	{
		return $this->strOverloaded ? mb_strpos( $haystack, $needle, $offset, 'ascii' )
									: strpos( $haystack, $needle, $offset );
	}

	function _substr( $string, $offset, $length = null )
	{

		if ( $this->strOverloaded )
			return mb_substr( $string, $offset, $length, 'ascii' );

		if ( is_null( $length ) )
			return substr( $string, $offset );

		return substr( $string, $offset, $length );

	}

	function _str_split( $string, $chunkSize )
	{

		if ( $this->strOverloaded )
		{

			$length = mb_strlen( $string, 'ascii' );

			$out = array();

			for ( $i = 0; $i < $length; $i += $chunkSize )
				$out[] = mb_substr( $string, $i, $chunkSize, 'ascii' );

			return $out;

		}
		else
			return str_split( $string, $chunkSize );

	}


	/**
	 * Reads provided MO file.
	 *
	 * @param string $filename name of MO file to read
	 * @return MO
	 */

	function import_from_file( $filename )
	{

		/*
		 * check cache for existing record first
		 */

		$set = wp_cache_get( $filename, 'l10n' );
		if ( is_array( $set ) )
		{

			list( $this->headers, $this->entries ) = $set;

			return true;

		}



		/**
		 * read header from file
		 */

		$file = fopen( $filename, 'r' );
		if ( !$file )
			return false;

		$header = fread( $file, 28 );
		if ( $this->_strlen( $header ) != 28 )
			return false;

		// detect endianess
		$endian = unpack( 'Nendian', $this->_substr( $header, 0, 4 ) );
		if ( $endian['endian'] == intval( hexdec( '950412de' ) ) )
			$endian = 'N';
		else if ( $endian['endian'] == intval( hexdec( 'de120495' ) ) )
			$endian = 'V';
		else
			return false;

		// parse header
		$header = unpack( "{$endian}Hrevision/{$endian}Hcount/{$endian}HposOriginals/{$endian}HposTranslations/{$endian}HsizeHash/{$endian}HposHash", $this->_substr( $header, 4 ) );
		if ( !is_array( $header ) )
			return false;

		extract( $header );

		// support revision 0 of MO format specs, only
		if ( $Hrevision != 0 )
			return false;



		/*
		 * read index tables on originals and translations
		 */

		// read originals' index
		fseek( $file, $HposOriginals, SEEK_SET );

		$originals = fread( $file, $Hcount * 8 );
		if ( $this->_strlen( $originals ) != $Hcount * 8 )
			return false;

		// read translations index
		fseek( $file, $HposTranslations, SEEK_SET );

		$translations = fread( $file, $Hcount * 8 );
		if ( $this->_strlen( $translations ) != $Hcount * 8 )
			return false;

		// transform raw data into set of indices
		$originals    = $this->_str_split( $originals, 8 );
		$translations = $this->_str_split( $translations, 8 );



		/*
		 * read set of strings to separate string
		 */

		// find position of first string in file
		$HposStrings = 0x7FFFFFFF;

		for ( $i = 0; $i < $Hcount; $i++ )
		{

			// parse index records on original and related translation
			$o = unpack( "{$endian}length/{$endian}pos", $originals[$i] );
			$t = unpack( "{$endian}length/{$endian}pos", $translations[$i] );

			if ( !$o || !$t )
				return false;

			$originals[$i]    = $o;
			$translations[$i] = $t;

			$HposStrings = min( $HposStrings, $o['pos'], $t['pos'] );

		}

		// read strings expected in rest of file
		fseek( $file, $HposStrings, SEEK_SET );

		$strings = '';
		while ( !feof( $file ) )
			$strings .= fread( $file, 4096 );

		fclose( $file );



		// collect hash records
		$hash = $header = array();

		for ( $i = 0; $i < $Hcount; $i++ )
		{

			// adjust offset due to reading strings to separate space before
			$originals[$i]['pos']    -= $HposStrings;
			$translations[$i]['pos'] -= $HposStrings;

			// extract original and translations
			$original    = $this->_substr( $strings, $originals[$i]['pos'], $originals[$i]['length'] );
			$translation = $this->_substr( $strings, $translations[$i]['pos'], $translations[$i]['length'] );



			if ( $original === '' )
			{
				// got header --> store separately

				$header = array();

				foreach ( explode( "\n", $translation ) as $line )
				{

					$sep = $this->_strpos( $line, ':' );
					if ( $sep !== false )
						$header[trim($this->_substr( $line, 0, $sep ))] = trim( $this->_substr( $line, $sep + 1 ));

				}
			}
			else
			{

				// detect context in original
				$sep = $this->_strpos( $original, "\04" );
				if ( $sep !== false )
				{
					$context  = $this->_substr( $original, 0, $sep );
					$original = $this->_substr( $original, $sep + 1 );
				}
				else
					$context  = null;


				$original     = explode( "\00", $original );
				$translation  = explode( "\00", $translation );

				$singularFrom = array_shift( $original );
				$singularTo   = array_shift( $translation );

				$record = array(
								'C' => $context,		// context
								'S' => $singularFrom,	// singular original
								'X' => $original,		// plural orignal
								'T' => $singularTo,		// singular translation
								'P' => $translation,	// plural translations
								);

				$key = is_null( $context ) ? $singularFrom
										   : "$context\04$singularFrom";


				$hash[$key] = $record;

			}
		}


		$this->headers = $header;
		$this->entries = $hash;



		/*
		 * write result to cache
		 */

		wp_cache_set( $filename, array( $header, $hash ), 'l10n' );



		return true;

	}


	/**
	 * Retrieves translation of single entry.
	 *
	 * The provided entry is constructed manually and thus providing proper
	 * key for lookup. The method returns a matching entry from internal pool
	 * of translations.
	 *
	 * NOTE! Passing by reference is required as long as parent class is using
	 *       it (obviously due to keeping things compatible with PHP4).
	 *
	 * @param Translate_Entry $entry entry to look up
	 * @return Translate_Entry resulting entry
	 */

	function translate_entry( &$entry )
	{

		if ( !isset( $this->entries[$entry->key()] ) )
			return false;

		if ( is_array( $this->entries[$entry->key()] ) )
		{
			// convert entry to instance of Translate_Entry on demand

			// use internally managed record
			$args = $this->entries[$entry->key()];

			// add structures required by Translation_Entry
			$args['translations'] = array( $args['T'] );
			if ( count( $args['P'] ) )
				$args['translations'] = array_merge( $args['translations'], array_values( $args['P'] ) );

			if ( count( $args['P'] ) )
				$args['plural'] = array_shift( array_keys( $args['P'] ) );

			// temporarily transform entry
			return new Translation_Entry( $args );

		}

		if ( $this->entries[$entry->key()] instanceof Translation_Entry )
			return $this->entries[$entry->key()];

		return false;

	}


	function translate( $singular, $context = null )
	{

		$key = $this->key( $singular, $context );

		if ( is_array( $this->entries[$key] ) )
			if ( is_string( $this->entries[$key]['T'] ) )
				return $this->entries[$key]['T'];

		return $singular;

	}


	function translate_plural( $singular, $plural, $count, $context = null )
	{

		$key = $this->key( $singular, $context );

		$translated = $this->entries[$key];
		if ( is_array( $translated ) )
		{

			$index    = $this->select_plural_form( $count );
			$nplurals = $this->nplurals;

			if ( ( $index >= 0 ) && ( $index < $nplurals ) )
			{

				if ( ( $index == 0 ) && is_string( $translated['T'] ) )
					// retrieve singular form
					return $translated['T'];

				if ( is_string( $translated['P'][--$index] ) )
					// retrieve selected plural form
					return $translated['P'][$index];

			}
		}


		// retrieve one of the provided original forms depending on $count
		return ( $count == 1 ) ? $singular : $plural;

	}


	function key( $singular, $context = null )
	{
		return is_null( $context ) ? $singular : "$context\04$singular";
	}


	function get_plural_forms_count()
	{
		return $this->nplurals;
	}


	function select_plural_form( $count )
	{

		$count = intval( $count );

		if ( trim( $this->pluralTerm ) === '' )
			return ( $count === 1 ) ? 0 : 1;

		return intval( eval( 'return (' . str_replace( 'n', '$count', $this->pluralTerm ) . ');' ) );

	}
}


?>