<?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);
        }
    }
}
