<?php

/**

 * Class for parsing ODS files

 *

 * @author Martins Pilsetnieks

 */

	class SpreadsheetReader_ODS implements Iterator, Countable

	{

		private $Options = array(

			'TempDir' => '',

			'ReturnDateTimeObjects' => false

		);



		/**

		 * @var string Path to temporary content file

		 */

		private $ContentPath = '';

		/**

		 * @var XMLReader XML reader object

		 */

		private $Content = false;



		/**

		 * @var array Data about separate sheets in the file

		 */

		private $Sheets = false;



		private $CurrentRow = null;



		/**

		 * @var int Number of the sheet we're currently reading

		 */

		private $CurrentSheet = 0;



		private $Index = 0;



		private $TableOpen = false;

		private $RowOpen = false;



		/**

		 * @param string Path to file

		 * @param array Options:

		 *	TempDir => string Temporary directory path

		 *	ReturnDateTimeObjects => bool True => dates and times will be returned as PHP DateTime objects, false => as strings

		 */

		public function __construct($Filepath, array $Options = null)

		{

			if (!is_readable($Filepath))

			{

				throw new Exception('SpreadsheetReader_ODS: File not readable ('.$Filepath.')');

			}



			$this -> TempDir = isset($Options['TempDir']) && is_writable($Options['TempDir']) ?

				$Options['TempDir'] :

				sys_get_temp_dir();



			$this -> TempDir = rtrim($this -> TempDir, DIRECTORY_SEPARATOR);

			$this -> TempDir = $this -> TempDir.DIRECTORY_SEPARATOR.uniqid().DIRECTORY_SEPARATOR;



			$Zip = new ZipArchive;

			$Status = $Zip -> open($Filepath);



			if ($Status !== true)

			{

				throw new Exception('SpreadsheetReader_ODS: File not readable ('.$Filepath.') (Error '.$Status.')');

			}



			if ($Zip -> locateName('content.xml') !== false)

			{

				$Zip -> extractTo($this -> TempDir, 'content.xml');

				$this -> ContentPath = $this -> TempDir.'content.xml';

			}



			$Zip -> close();



			if ($this -> ContentPath && is_readable($this -> ContentPath))

			{

				$this -> Content = new XMLReader;

				$this -> Content -> open($this -> ContentPath);

				$this -> Valid = true;

			}

		}



		/**

		 * Destructor, destroys all that remains (closes and deletes temp files)

		 */

		public function __destruct()

		{

			if ($this -> Content && $this -> Content instanceof XMLReader)

			{

				$this -> Content -> close();

				unset($this -> Content);

			}

			if (file_exists($this -> ContentPath))

			{

				@unlink($this -> ContentPath);

				unset($this -> ContentPath);

			}

		}



		/**

		 * Retrieves an array with information about sheets in the current file

		 *

		 * @return array List of sheets (key is sheet index, value is name)

		 */

		public function Sheets()

		{

			if ($this -> Sheets === false)

			{

				$this -> Sheets = array();



				if ($this -> Valid)

				{

					$this -> SheetReader = new XMLReader;

					$this -> SheetReader -> open($this -> ContentPath);



					while ($this -> SheetReader -> read())

					{

						if ($this -> SheetReader -> name == 'table:table')

						{

							$this -> Sheets[] = $this -> SheetReader -> getAttribute('table:name');

							$this -> SheetReader -> next();

						}

					}

					

					$this -> SheetReader -> close();

				}

			}

			return $this -> Sheets;

		}



		/**

		 * Changes the current sheet in the file to another

		 *

		 * @param int Sheet index

		 *

		 * @return bool True if sheet was successfully changed, false otherwise.

		 */

		public function ChangeSheet($Index)

		{

			$Index = (int)$Index;



			$Sheets = $this -> Sheets();

			if (isset($Sheets[$Index]))

			{

				$this -> CurrentSheet = $Index;

				$this -> rewind();



				return true;

			}



			return false;

		}



		// !Iterator interface methods

		/** 

		 * Rewind the Iterator to the first element.

		 * Similar to the reset() function for arrays in PHP

		 */ 

		public function rewind()

		{

			if ($this -> Index > 0)

			{

				// If the worksheet was already iterated, XML file is reopened.

				// Otherwise it should be at the beginning anyway

				$this -> Content -> close();

				$this -> Content -> open($this -> ContentPath);

				$this -> Valid = true;



				$this -> TableOpen = false;

				$this -> RowOpen = false;



				$this -> CurrentRow = null;

			}



			$this -> Index = 0;

		}



		/**

		 * Return the current element.

		 * Similar to the current() function for arrays in PHP

		 *

		 * @return mixed current element from the collection

		 */

		public function current()

		{

			if ($this -> Index == 0 && is_null($this -> CurrentRow))

			{

				$this -> next();

				$this -> Index--;

			}

			return $this -> CurrentRow;

		}



		/** 

		 * Move forward to next element. 

		 * Similar to the next() function for arrays in PHP 

		 */ 

		public function next()

		{

			$this -> Index++;



			$this -> CurrentRow = array();



			if (!$this -> TableOpen)

			{

				$TableCounter = 0;

				$SkipRead = false;



				while ($this -> Valid = ($SkipRead || $this -> Content -> read()))

				{

					if ($SkipRead)

					{

						$SkipRead = false;

					}



					if ($this -> Content -> name == 'table:table' && $this -> Content -> nodeType != XMLReader::END_ELEMENT)

					{

						if ($TableCounter == $this -> CurrentSheet)

						{

							$this -> TableOpen = true;

							break;

						}



						$TableCounter++;

						$this -> Content -> next();

						$SkipRead = true;

					}

				}

			}



			if ($this -> TableOpen && !$this -> RowOpen)

			{

				while ($this -> Valid = $this -> Content -> read())

				{

					switch ($this -> Content -> name)

					{

						case 'table:table':

							$this -> TableOpen = false;

							$this -> Content -> next('office:document-content');

							$this -> Valid = false;

							break 2;

						case 'table:table-row':

							if ($this -> Content -> nodeType != XMLReader::END_ELEMENT)

							{

								$this -> RowOpen = true;

								break 2;

							}

							break;

					}

				}

			}



			if ($this -> RowOpen)

			{

				$LastCellContent = '';



				while ($this -> Valid = $this -> Content -> read())

				{

					switch ($this -> Content -> name)

					{

						case 'table:table-cell':

							if ($this -> Content -> nodeType == XMLReader::END_ELEMENT || $this -> Content -> isEmptyElement)

							{

								if ($this -> Content -> nodeType == XMLReader::END_ELEMENT)

								{

									$CellValue = $LastCellContent;

								}

								elseif ($this -> Content -> isEmptyElement)

								{

									$LastCellContent = '';

									$CellValue = $LastCellContent;

								}



								$this -> CurrentRow[] = $LastCellContent;



								if ($this -> Content -> getAttribute('table:number-columns-repeated') !== null)

								{                                                                                            

									$RepeatedColumnCount = $this -> Content -> getAttribute('table:number-columns-repeated');

									// Checking if larger than one because the value is already added to the row once before

									if ($RepeatedColumnCount > 1)

									{

										$this -> CurrentRow = array_pad($this -> CurrentRow, count($this -> CurrentRow) + $RepeatedColumnCount - 1, $LastCellContent);

									}

								}

							}

							else

							{

								$LastCellContent = '';

							}

						case 'text:p':

							if ($this -> Content -> nodeType != XMLReader::END_ELEMENT)

							{

								$LastCellContent = $this -> Content -> readString();

							}

							break;

						case 'table:table-row':

							$this -> RowOpen = false;

							break 2;

					}

				}

			}



			return $this -> CurrentRow;

		}



		/** 

		 * Return the identifying key of the current element.

		 * Similar to the key() function for arrays in PHP

		 *

		 * @return mixed either an integer or a string

		 */ 

		public function key()

		{

			return $this -> Index;

		}



		/** 

		 * Check if there is a current element after calls to rewind() or next().

		 * Used to check if we've iterated to the end of the collection

		 *

		 * @return boolean FALSE if there's nothing more to iterate over

		 */ 

		public function valid()

		{

			return $this -> Valid;

		}



		// !Countable interface method

		/**

		 * Ostensibly should return the count of the contained items but this just returns the number

		 * of rows read so far. It's not really correct but at least coherent.

		 */

		public function count()

		{

			return $this -> Index + 1;

		}

	}

?>