<?php

namespace Box\Spout\Writer;

use Box\Spout\Common\Exception\IOException;
use Box\Spout\Common\Exception\InvalidArgumentException;
use Box\Spout\Common\Exception\SpoutException;
use Box\Spout\Common\Helper\FileSystemHelper;
use Box\Spout\Writer\Exception\WriterAlreadyOpenedException;
use Box\Spout\Writer\Exception\WriterNotOpenedException;
use Box\Spout\Writer\Style\StyleBuilder;

/**
 * Class AbstractWriter
 *
 * @package Box\Spout\Writer
 * @abstract
 */
abstract class AbstractWriter implements WriterInterface {

	/** @var string Path to the output file */
	protected $outputFilePath;

	/** @var resource Pointer to the file/stream we will write to */
	protected $filePointer;

	/** @var bool Indicates whether the writer has been opened or not */
	protected $isWriterOpened = false;

	/** @var \Box\Spout\Common\Helper\GlobalFunctionsHelper Helper to work with global functions */
	protected $globalFunctionsHelper;

	/** @var Style\Style Style to be applied to the next written row(s) */
	protected $rowStyle;

	/** @var Style\Style Default row style. Each writer can have its own default style */
	protected $defaultRowStyle;

	/** @var string Content-Type value for the header - to be defined by child class */
	protected static $headerContentType;

	/**
	 * Opens the streamer and makes it ready to accept data.
	 *
	 * @return void
	 * @throws \Box\Spout\Common\Exception\IOException If the writer cannot be opened
	 */
	abstract protected function openWriter();

	/**
	 * Adds data to the currently openned writer.
	 *
	 * @param  array $dataRow Array containing data to be streamed.
	 *          Example $dataRow = ['data1', 1234, null, '', 'data5'];
	 * @param Style\Style $style Style to be applied to the written row
	 * @return void
	 */
	abstract protected function addRowToWriter( array $dataRow, $style);

	/**
	 * Closes the streamer, preventing any additional writing.
	 *
	 * @return void
	 */
	abstract protected function closeWriter();

	/**
	 *
	 */
	public function __construct() {
		 $this->defaultRowStyle = $this->getDefaultRowStyle();
		$this->resetRowStyleToDefault();
	}

	/**
	 * Sets the default styles for all rows added with "addRow".
	 * Overriding the default style instead of using "addRowWithStyle" improves performance by 20%.
	 * @see https://github.com/box/spout/issues/272
	 *
	 * @param Style\Style $defaultStyle
	 * @return AbstractWriter
	 */
	public function setDefaultRowStyle( $defaultStyle ) {
		$this->defaultRowStyle = $defaultStyle;
		$this->resetRowStyleToDefault();
		return $this;
	}

	/**
	 * @param \Box\Spout\Common\Helper\GlobalFunctionsHelper $globalFunctionsHelper
	 * @return AbstractWriter
	 */
	public function setGlobalFunctionsHelper( $globalFunctionsHelper ) {
		$this->globalFunctionsHelper = $globalFunctionsHelper;
		return $this;
	}

	/**
	 * Inits the writer and opens it to accept data.
	 * By using this method, the data will be written to a file.
	 *
	 * @api
	 * @param  string $outputFilePath Path of the output file that will contain the data
	 * @return AbstractWriter
	 * @throws \Box\Spout\Common\Exception\IOException If the writer cannot be opened or if the given path is not writable
	 */
	public function openToFile( $outputFilePath ) {
		 $this->outputFilePath = $outputFilePath;

		$this->filePointer = $this->globalFunctionsHelper->fopen( $this->outputFilePath, 'wb+' );
		$this->throwIfFilePointerIsNotAvailable();

		$this->openWriter();
		$this->isWriterOpened = true;

		return $this;
	}

	/**
	 * Inits the writer and opens it to accept data.
	 * By using this method, the data will be outputted directly to the browser.
	 *
	 * @codeCoverageIgnore
	 *
	 * @api
	 * @param  string $outputFileName Name of the output file that will contain the data. If a path is passed in, only the file name will be kept
	 * @return AbstractWriter
	 * @throws \Box\Spout\Common\Exception\IOException If the writer cannot be opened
	 */
	public function openToBrowser( $outputFileName ) {
		$this->outputFilePath = $this->globalFunctionsHelper->basename( $outputFileName );

		$this->filePointer = $this->globalFunctionsHelper->fopen( 'php://output', 'w' );
		$this->throwIfFilePointerIsNotAvailable();

		// Clear any previous output (otherwise the generated file will be corrupted)
		// @see https://github.com/box/spout/issues/241
		$this->globalFunctionsHelper->ob_end_clean();

		// Set headers
		$this->globalFunctionsHelper->header( 'Content-Type: ' . static::$headerContentType );
		$this->globalFunctionsHelper->header( 'Content-Disposition: attachment; filename="' . $this->outputFilePath . '"' );

		/*
		 * When forcing the download of a file over SSL,IE8 and lower browsers fail
		 * if the Cache-Control and Pragma headers are not set.
		 *
		 * @see http://support.microsoft.com/KB/323308
		 * @see https://github.com/liuggio/ExcelBundle/issues/45
		 */
		$this->globalFunctionsHelper->header( 'Cache-Control: max-age=0' );
		$this->globalFunctionsHelper->header( 'Pragma: public' );

		$this->openWriter();
		$this->isWriterOpened = true;

		return $this;
	}

	/**
	 * Checks if the pointer to the file/stream to write to is available.
	 * Will throw an exception if not available.
	 *
	 * @return void
	 * @throws \Box\Spout\Common\Exception\IOException If the pointer is not available
	 */
	protected function throwIfFilePointerIsNotAvailable() {
		if ( ! $this->filePointer ) {
			throw new IOException( 'File pointer has not be opened' );
		}
	}

	/**
	 * Checks if the writer has already been opened, since some actions must be done before it gets opened.
	 * Throws an exception if already opened.
	 *
	 * @param string $message Error message
	 * @return void
	 * @throws \Box\Spout\Writer\Exception\WriterAlreadyOpenedException If the writer was already opened and must not be.
	 */
	protected function throwIfWriterAlreadyOpened( $message ) {
		if ( $this->isWriterOpened ) {
			throw new WriterAlreadyOpenedException( $message );
		}
	}

	/**
	 * Write given data to the output. New data will be appended to end of stream.
	 *
	 * @param  array $dataRow Array containing data to be streamed.
	 *                        If empty, no data is added (i.e. not even as a blank row)
	 *                        Example: $dataRow = ['data1', 1234, null, '', 'data5', false];
	 * @api
	 * @return AbstractWriter
	 * @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If this function is called before opening the writer
	 * @throws \Box\Spout\Common\Exception\IOException If unable to write data
	 * @throws \Box\Spout\Common\Exception\SpoutException If anything else goes wrong while writing data
	 */
	public function addRow( array $dataRow ) {
		if ( $this->isWriterOpened ) {
			// empty $dataRow should not add an empty line
			if ( ! empty( $dataRow ) ) {
				try {
					$this->addRowToWriter( $dataRow, $this->rowStyle );
				} catch ( SpoutException $e ) {
					// if an exception occurs while writing data,
					// close the writer and remove all files created so far.
					$this->closeAndAttemptToCleanupAllFiles();

					// re-throw the exception to alert developers of the error
					throw $e;
				}
			}
		} else {
			throw new WriterNotOpenedException( 'The writer needs to be opened before adding row.' );
		}

		return $this;
	}

	/**
	 * Write given data to the output and apply the given style.
	 * @see addRow
	 *
	 * @api
	 * @param array $dataRow Array of array containing data to be streamed.
	 * @param Style\Style $style Style to be applied to the row.
	 * @return AbstractWriter
	 * @throws \Box\Spout\Common\Exception\InvalidArgumentException If the input param is not valid
	 * @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If this function is called before opening the writer
	 * @throws \Box\Spout\Common\Exception\IOException If unable to write data
	 */
	public function addRowWithStyle( array $dataRow, $style ) {
		if ( ! $style instanceof Style\Style ) {
			throw new InvalidArgumentException( 'The "$style" argument must be a Style instance and cannot be NULL.' );
		}

		$this->setRowStyle( $style );
		$this->addRow( $dataRow );
		$this->resetRowStyleToDefault();

		return $this;
	}

	/**
	 * Write given data to the output. New data will be appended to end of stream.
	 *
	 * @api
	 * @param  array $dataRows Array of array containing data to be streamed.
	 *                         If a row is empty, it won't be added (i.e. not even as a blank row)
	 *                         Example: $dataRows = [
	 *                             ['data11', 12, , '', 'data13'],
	 *                             ['data21', 'data22', null, false],
	 *                         ];
	 * @return AbstractWriter
	 * @throws \Box\Spout\Common\Exception\InvalidArgumentException If the input param is not valid
	 * @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If this function is called before opening the writer
	 * @throws \Box\Spout\Common\Exception\IOException If unable to write data
	 */
	public function addRows( array $dataRows ) {
		if ( ! empty( $dataRows ) ) {
			$firstRow = reset( $dataRows );
			if ( ! is_array( $firstRow ) ) {
				throw new InvalidArgumentException( 'The input should be an array of arrays' );
			}

			foreach ( $dataRows as $dataRow ) {
				$this->addRow( $dataRow );
			}
		}

		return $this;
	}

	/**
	 * Write given data to the output and apply the given style.
	 * @see addRows
	 *
	 * @api
	 * @param array $dataRows Array of array containing data to be streamed.
	 * @param Style\Style $style Style to be applied to the rows.
	 * @return AbstractWriter
	 * @throws \Box\Spout\Common\Exception\InvalidArgumentException If the input param is not valid
	 * @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If this function is called before opening the writer
	 * @throws \Box\Spout\Common\Exception\IOException If unable to write data
	 */
	public function addRowsWithStyle( array $dataRows, $style ) {
		if ( ! $style instanceof Style\Style ) {
			throw new InvalidArgumentException( 'The "$style" argument must be a Style instance and cannot be NULL.' );
		}

		$this->setRowStyle( $style );
		$this->addRows( $dataRows );
		$this->resetRowStyleToDefault();

		return $this;
	}

	/**
	 * Returns the default style to be applied to rows.
	 * Can be overriden by children to have a custom style.
	 *
	 * @return Style\Style
	 */
	protected function getDefaultRowStyle() {
		return ( new StyleBuilder() )->build();
	}

	/**
	 * Sets the style to be applied to the next written rows
	 * until it is changed or reset.
	 *
	 * @param Style\Style $style
	 * @return void
	 */
	private function setRowStyle( $style ) {
		// Merge given style with the default one to inherit custom properties
		$this->rowStyle = $style->mergeWith( $this->defaultRowStyle );
	}

	/**
	 * Resets the style to be applied to the next written rows.
	 *
	 * @return void
	 */
	private function resetRowStyleToDefault() {
		 $this->rowStyle = $this->defaultRowStyle;
	}

	/**
	 * Closes the writer. This will close the streamer as well, preventing new data
	 * to be written to the file.
	 *
	 * @api
	 * @return void
	 */
	public function close() {
		if ( ! $this->isWriterOpened ) {
			return;
		}

		$this->closeWriter();

		if ( is_resource( $this->filePointer ) ) {
			$this->globalFunctionsHelper->fclose( $this->filePointer );
		}

		$this->isWriterOpened = false;
	}

	/**
	 * Closes the writer and attempts to cleanup all files that were
	 * created during the writing process (temp files & final file).
	 *
	 * @return void
	 */
	private function closeAndAttemptToCleanupAllFiles() {
		// close the writer, which should remove all temp files
		$this->close();

		// remove output file if it was created
		if ( $this->globalFunctionsHelper->file_exists( $this->outputFilePath ) ) {
			$outputFolderPath = dirname( $this->outputFilePath );
			$fileSystemHelper = new FileSystemHelper( $outputFolderPath );
			$fileSystemHelper->deleteFile( $this->outputFilePath );
		}
	}
}
