<?php namespace Maatwebsite\Excel\Readers;

use Illuminate\Support\Facades\Cache;
use Illuminate\Contracts\Bus\Dispatcher;
use Illuminate\Support\Facades\Queue;
use Maatwebsite\Excel\Classes\PHPExcel;
use PHPExcel_Cell;
use PHPExcel_IOFactory;
use PHPExcel_Cell_IValueBinder;
use PHPExcel_Cell_DefaultValueBinder;
use Illuminate\Filesystem\Filesystem;
use Maatwebsite\Excel\Parsers\ExcelParser;
use Maatwebsite\Excel\Classes\FormatIdentifier;
use Maatwebsite\Excel\Exceptions\LaravelExcelException;

/**
 * LaravelExcel Excel reader
 *
 * @category   Laravel Excel
 * @version    1.0.0
 * @package    maatwebsite/excel
 * @copyright  Copyright (c) 2013 - 2014 Maatwebsite (http://www.maatwebsite.nl)
 * @author     Maatwebsite <info@maatwebsite.nl>
 * @license    http://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt    LGPL
 */
class LaravelExcelReader
{
    /**
     * Excel object
     *
     * @var \PHPExcel
     */
    public $excel;

    /**
     * Spreadsheet writer
     *
     * @var object
     */
    public $reader;

    /**
     * The file to read
     *
     * @var string
     */
    public $file;

    /**
     * Selected columns
     *
     * @var array
     */
    public $columns = [];

    /**
     * Spreadsheet title
     *
     * @var string
     */
    public $title;

    /**
     * Default extension
     *
     * @var string
     */
    public $ext = 'xls';

    /**
     * Encoding
     *
     * @var boolean
     */
    public $encoding = false;

    /**
     * Default format
     *
     * @var stirng
     */
    public $format;

    /**
     * The parsed file
     *
     * @var SheetCollection|RowCollection
     */
    public $parsed;

    /**
     * Calculate [true/false]
     *
     * @var boolean
     */
    public $calculate;

    /**
     * Limit data
     *
     * @var boolean
     */
    protected $limit = false;

    /**
     * Amount of rows to skip
     *
     * @var integer
     */
    protected $skip = 0;

    /**
     * Slug separator
     *
     * @var string
     */
    public $separator = false;

    /**
     * Ignore empty cells
     *
     * @var boolean
     */
    public $ignoreEmpty = false;

    /**
     * Format dates
     *
     * @var boolean
     */
    public $formatDates = true;

    /**
     * The date columns
     *
     * @var array
     */
    public $dateColumns = [];

    /**
     * If the file has a heading or not
     *
     * @var boolean
     */
    public $noHeading = false;

    /**
     * Default date format
     *
     * @var string
     */
    public $dateFormat;

    /**
     * Whether the results are cached or not
     *
     * @var boolean
     */
    public $remembered = false;

    /**
     * Amount of minutes the results will remain cached
     *
     * @var integer
     */
    public $cacheMinutes = 10;

    /**
     * Selected sheets
     *
     * @var array
     */
    public $selectedSheets = [];

    /**
     * Selected sheet indices
     *
     * @var array
     */
    public $selectedSheetIndices = [];

    /**
     * Active filter
     *
     * @var PHPExcel_Reader_IReadFilter
     */
    protected $filter;

    /**
     * Filters
     *
     * @var array
     */
    public $filters = [
        'registered' => []
    ];

    /**
     * @var LaravelExcelWorksheet
     */
    protected $sheet;

    /**
     * @var LaravelExcelWriter
     */
    protected $writer;

    /**
     * @var bool|string
     */
    protected $delimiter;

    /**
     * @var bool|string
     */
    protected $enclosure;

    /**
     * @var \PHPExcel
     */
    protected $original;

    /**
     * @var Dispatcher
     */
    protected $dispatcher;

    /**
     * Construct new reader
     *
     * @param Filesystem       $filesystem
     * @param FormatIdentifier $identifier
     * @param Dispatcher       $dispatcher
     */
    public function __construct(Filesystem $filesystem, FormatIdentifier $identifier, Dispatcher $dispatcher)
    {
        $this->filesystem = $filesystem;
        $this->identifier = $identifier;
        $this->dispatcher = $dispatcher;
    }

    /**
     * Load a file
     *
     * @param  string        $file
     * @param string|boolean $encoding
     * @param bool           $noBasePath
     *
     * @return LaravelExcelReader
     */
    public function load($file, $encoding = false, $noBasePath = false, $callbackConfigReader = null)
    {
        // init the loading
        $this->_init($file, $encoding, $noBasePath);

        if (is_callable($callbackConfigReader)) {
            call_user_func($callbackConfigReader, $this);
        }

        // Only fetch selected sheets if necessary
        if ($this->sheetsSelected()) {
            $this->reader->setLoadSheetsOnly($this->selectedSheets);
        }

        // Load the file
        $this->excel = $this->reader->load($this->file);

        // Return itself
        return $this;
    }

    /**
     * @param integer|callable|string $sheetID
     * @param null                    $callback
     *
     * @return $this
     * @throws \PHPExcel_Exception
     */
    public function sheet($sheetID, $callback = null)
    {
        // Default
        $isCallable = false;

        // Init a new PHPExcel instance without any worksheets
        if (!$this->excel instanceof PHPExcel) {
            $this->original = $this->excel;
            $this->initClonedExcelObject($this->excel);

            // Clone all connected sheets
            foreach ($this->original->getAllSheets() as $sheet) {
                $this->excel->createSheet()->cloneParent($sheet);
            }
        }

        // Copy the callback when needed
        if (is_callable($sheetID)) {
            $callback   = $sheetID;
            $isCallable = true;
        } elseif (is_callable($callback)) {
            $isCallable = true;
        }

        // Clone the loaded excel instance
        $this->sheet = $this->getSheetByIdOrName($sheetID);

        // Do the callback
        if ($isCallable) {
            call_user_func($callback, $this->sheet);
        }

        // Return the sheet
        return $this->sheet;
    }

    /**
     * Set csv delimiter
     *
     * @param $delimiter
     *
     * @return $this
     */
    public function setDelimiter($delimiter)
    {
        $this->delimiter = $delimiter;

        return $this;
    }

    /**
     * Set csv enclosure
     *
     * @param $enclosure
     *
     * @return $this
     */
    public function setEnclosure($enclosure)
    {
        $this->enclosure = $enclosure;

        return $this;
    }

    /**
     * set selected sheets
     *
     * @param array $sheets
     */
    public function setSelectedSheets($sheets)
    {
        $this->selectedSheets = $sheets;
    }

    /**
     * Check if sheets were selected
     *
     * @return integer
     */
    public function sheetsSelected()
    {
        return count($this->selectedSheets) > 0;
    }

    /**
     * Check if the file was selected by index
     *
     * @param  $index
     *
     * @return boolean
     */
    public function isSelectedByIndex($index)
    {
        $selectedSheets = $this->getSelectedSheetIndices();
        if (empty($selectedSheets)) {
            return true;
        }

        return in_array($index, $selectedSheets) ? true : false;
    }

    /**
     * Set the selected sheet indices
     *
     * @param  $sheets
     *
     * @return $this
     */
    public function setSelectedSheetIndices($sheets)
    {
        $this->selectedSheetIndices = $sheets;

        return $this;
    }

    /**
     * Return the selected sheets
     *
     * @return array
     */
    public function getSelectedSheetIndices()
    {
        return $this->selectedSheetIndices;
    }

    /**
     * Remember the results for x minutes
     *
     * @param  integer $minutes
     *
     * @return LaravelExcelReader
     */
    public function remember($minutes)
    {
        $this->remembered   = true;
        $this->cacheMinutes = $minutes;

        return $this;
    }

    /**
     * Read the file through a config file
     *
     * @param  string        $config
     * @param  callback|null $callback
     *
     * @return SheetCollection
     */
    public function byConfig($config, $callback = null)
    {
        $config = new ConfigReader($this->excel, $config, $callback);

        return $config->getSheetCollection();
    }

    /**
     * Take x rows
     *
     * @param  integer $amount
     *
     * @return LaravelExcelReader
     */
    public function take($amount)
    {
        // Set limit
        $this->limit = $amount;

        return $this;
    }

    /**
     * Skip x rows
     *
     * @param  integer $amount
     *
     * @return LaravelExcelReader
     */
    public function skip($amount)
    {
        // Set skip amount
        $this->skip = $amount;

        return $this;
    }

    /**
     * Limit the results by x
     *
     * @param  integer $take
     * @param  integer $skip
     *
     * @return LaravelExcelReader
     */
    public function limit($take, $skip = 0)
    {
        // Skip x records
        $this->skip($skip);

        // Take x records
        $this->take($take);

        return $this;
    }

    /**
     * Select certain columns
     *
     * @param  array $columns
     *
     * @return LaravelExcelReader
     */
    public function select($columns = [])
    {
        $this->columns = array_merge($this->columns, $columns);

        return $this;
    }

    /**
     * Return all sheets/rows
     *
     * @param  array $columns
     *
     * @return LaravelExcelReader
     */
    public function all($columns = [])
    {
        return $this->get($columns);
    }

    /**
     * Get first row/sheet only
     *
     * @param  array $columns
     *
     * @return SheetCollection|RowCollection
     */
    public function first($columns = [])
    {
        return $this->take(1)->get($columns)->first();
    }

    /**
     * Get all sheets/rows
     *
     * @param array $columns
     *
     * @return SheetCollection|RowCollection
     */
    public function get($columns = [])
    {
        if ($this->remembered) {
            // Return cached results
            return Cache::remember(md5($this->file), $this->cacheMinutes, function () use (&$columns) {
                $this->_parseFile($columns);

                return $this->parsed;
            });
        } else {
            // return parsed file
            $this->_parseFile($columns);

            return $this->parsed;
        }
    }

    /**
     * Parse the file in chunks and queues the processing of each chunk
     *
     * @param int           $size
     * @param callable      $callback
     * @param bool|string   $shouldQueue
     */
    public function chunk($size = 10, callable $callback, $shouldQueue = true)
    {
        // Get total rows
        $totalRows = $this->getTotalRowsOfFile();

        $break = false;

        for ($startRow = 0; $startRow < $totalRows; $startRow += $chunkSize) {

            // Set start index
            $startIndex = ($startRow == 0) ? $startRow : $startRow - 1;
            $chunkSize  = ($startRow == 0) ? $size + 1 : $size;

            $job = new ChunkedReadJob(
                $this->file,
                $this->reader->getLoadSheetsOnly(),
                $startRow,
                $startIndex,
                $chunkSize,
                $callback,
                $shouldQueue
            );

            if ($shouldQueue) {
                // If a string is passed (which also evaluates to true if not empty), assign to that named queue
                if(is_string($shouldQueue)) {
                    $job->onQueue($shouldQueue);
                }
                $this->dispatcher->dispatch($job);
            } else {
                $break = $job->handle();
            }

            if ($break === true) {
                break;
            }
        }
    }

    /**
     * Each
     *
     * @param  callback $callback
     *
     * @return SheetCollection|RowCollection
     */
    public function each($callback)
    {
        return $this->get()->each($callback);
    }

    /**
     *  Parse the file to an array.
     *
     * @param  array $columns
     *
     * @return array
     */
    public function toArray($columns = [])
    {
        return (array)$this->get($columns)->toArray();
    }

    /**
     *  Parse the file to an object.
     *
     * @param array $columns
     *
     * @return SheetCollection|RowCollection
     */
    public function toObject($columns = [])
    {
        return $this->get($columns);
    }

    /**
     *  Dump the parsed file to a readable array
     *
     * @param  array   $columns
     * @param  boolean $die
     *
     * @return string
     */
    public function dump($columns = [], $die = false)
    {
        echo '<pre class="container" style="background: #f5f5f5; border: 1px solid #e3e3e3; padding:15px;">';
        $die ? dd($this->get($columns)) : var_dump($this->get($columns));
        echo '</pre>';
    }

    /**
     * Die and dump
     *
     * @param array $columns
     *
     * @return string
     */
    public function dd($columns = [])
    {
        return $this->dump($columns, true);
    }

    /**
     * Init the loading
     *
     * @param      $file
     * @param bool $encoding
     * @param bool $noBasePath
     */
    public function _init($file, $encoding = false, $noBasePath = false)
    {
        // Set the extension
        $this->_setFile($file, $noBasePath)
             ->setExtension()
             ->setTitle()
             ->_setFormat()
             ->_setReader()
             ->_enableFilters()
             ->_setInputEncoding($encoding);
    }

    /**
     * Inject the excel object
     *
     * @param  PHPExcel $excel
     *
     * @return void
     */
    public function injectExcel($excel)
    {
        $this->excel = $excel;
        $this->_reset();
    }

    /**
     * Set filters
     *
     * @param array $filters
     */
    public function setFilters($filters = [])
    {
        $this->filters = $filters;
    }

    /**
     * @return PHPExcel_Reader_IReadFilter
     */
    public function getFilter()
    {
        return $this->filter;
    }

    /**
     * Enable filters
     *
     * @return $this
     */
    protected function _enableFilters()
    {
        // Loop through the registered filters
        foreach ($this->filters['registered'] as $key => $class) {
            // Set the filter inside the reader when enabled and the class exists
            if (in_array($key, $this->filters['enabled']) && class_exists($class)) {
                // init new filter (and overrule the current)
                $this->filter = new $class;

                // Set default rows
                if (method_exists($this->filter, 'setRows')) {
                    $this->filter->setRows(0, 1);
                }

                // Set the read filter
                $this->reader->setReadFilter($this->filter);
            }
        }

        return $this;
    }

    /**
     * Set the file
     *
     * @param string $file
     * @param bool   $noBasePath
     *
     * @return $this
     */
    protected function _setFile($file, $noBasePath = false)
    {
        // check if we have a correct path
        if (!$noBasePath && !realpath($file)) {
            $file = base_path($file);
        }

        $this->file = $file;

        return $this;
    }

    /**
     * Set the spreadsheet title
     *
     * @param string|boolean $title
     *
     * @return LaraveExcelReader
     */
    public function setTitle($title = false)
    {
        $this->title = $title ? $title : basename($this->file, '.' . $this->ext);

        return $this;
    }

    /**
     * Set extension
     *
     * @param string|boolean $ext
     *
     * @return LaraveExcelReader
     */
    public function setExtension($ext = false)
    {
        $this->ext = $ext ? $ext : $this->filesystem->extension($this->file);

        return $this;
    }

    /**
     * Set custom value binder
     *
     * @param string|boolean $ext
     *
     * @return void
     */
    public function setValueBinder(PHPExcel_Cell_IValueBinder $binder)
    {
        PHPExcel_Cell::setValueBinder($binder);

        return $this;
    }

    /**
     * Reset the value binder back to default
     *
     * @return void
     */
    public function resetValueBinder()
    {
        PHPExcel_Cell::setValueBinder(new PHPExcel_Cell_DefaultValueBinder);

        return $this;
    }

    /**
     * Set the date format
     *
     * @param bool|string $format The date format
     *
     * @return LaraveExcelReader
     */
    public function setDateFormat($format = false)
    {
        $this->formatDates = $format ? true : false;
        $this->dateFormat  = $format;

        return $this;
    }

    /**
     * Enable/disable date formating
     *
     * @param  boolean $boolean True/false
     * @param  boolean $format
     *
     * @return LaraveExcelReader
     */
    public function formatDates($boolean = true, $format = false)
    {
        $this->formatDates = $boolean;
        $this->setDateFormat($format);

        return $this;
    }

    /**
     * Set the date columns
     *
     * @return LaraveExcelReader
     */
    public function setDateColumns()
    {
        $this->formatDates = true;
        $columns           = func_get_args();
        $this->dateColumns = array_merge($this->dateColumns, array_flatten($columns));

        return $this;
    }

    /**
     * If the file has a table heading or not
     *
     * @param  boolean $boolean
     *
     * @return LaraveExcelReader
     */
    public function noHeading($boolean = true)
    {
        $this->noHeading = $boolean;

        return $this;
    }

    /**
     * Set the cell name word separator
     *
     * @param string $separator
     *
     * @return LaraveExcelReader
     */
    public function setSeparator($separator)
    {
        $this->separator = $separator;

        return $this;
    }

    /**
     * Spelling mistake backwards compatibility
     *
     * @param  $separator
     *
     * @return \Maatwebsite\Excel\Readers\LaraveExcelReader
     */
    public function setSeperator($separator)
    {
        return $this->setSeparator($separator);
    }

    /**
     *  Set default calculate
     *
     * @param bool $boolean Calculate yes or no
     *
     * @return LaraveExcelReader
     */
    public function calculate($boolean = true)
    {
        $this->calculate = $boolean;

        return $this;
    }

    /**
     * Ignore empty cells
     *
     * @param  boolean $boolean
     *
     * @return LaraveExcelReader
     */
    public function ignoreEmpty($boolean = true)
    {
        $this->ignoreEmpty = $boolean;

        return $this;
    }

    /**
     * Check if the file has een heading
     *
     * @return boolean
     */
    public function hasHeading()
    {
        if (!$this->noHeading) {
            $config = config('excel.import.heading', true);

            return $config !== false && $config !== 'numeric';
        }

        return $this->noHeading ? false : true;
    }

    /**
     * Get the separator
     *
     * @return string
     */
    public function getSeparator()
    {
        if ($this->separator) {
            return $this->separator;
        }

        return config('excel.import.separator', config('excel.import.seperator', '_'));
    }

    /**
     * Get the dateFormat
     *
     * @return string
     */
    public function getDateFormat()
    {
        return $this->dateFormat;
    }

    /**
     * Get the date columns
     *
     * @return array
     */
    public function getDateColumns()
    {
        return $this->dateColumns;
    }

    /**
     * Check if we need to calculate the formula inside the cell
     *
     * @return boolean
     */
    public function needsCalculation()
    {
        return $this->calculate;
    }

    /**
     * Check if we need to ignore the empty cells
     *
     * @return boolean
     */
    public function needsIgnoreEmpty()
    {
        return $this->ignoreEmpty;
    }

    /**
     * Check if we need to format the dates
     *
     * @return boolean
     */
    public function needsDateFormatting()
    {
        return $this->formatDates ? true : false;
    }

    /**
     * Return the amount of rows to skip
     *
     * @return integer
     */
    public function getSkip()
    {
        return $this->skip;
    }

    /**
     * Return the amount of rows to take
     *
     * @return integer
     */
    public function getLimit()
    {
        return $this->limit;
    }

    /**
     * Get total rows of file
     *
     * @return integer
     */
    public function getTotalRowsOfFile()
    {
        // Get worksheet info
        $spreadsheetInfo = $this->getSheetInfoForActive();

        // return total rows
        return $spreadsheetInfo['totalRows'];
    }

    /**
     * Get sheet info for active sheet
     *
     * @return mixed
     */
    public function getSheetInfoForActive()
    {
        $spreadsheetInfo = $this->reader->listWorksheetInfo($this->file);

        // Loop through the info
        foreach ($spreadsheetInfo as $key => $value) {
            // When we hit the right worksheet
            if ($value['worksheetName'] == $this->getActiveSheet()->getTitle()) {
                $index = $key;
            }
        }

        // return total rows
        return $spreadsheetInfo[$index];
    }

    /**
     * @param $clone
     */
    protected function initClonedExcelObject($clone)
    {
        $this->excel = new PHPExcel();
        $this->excel->cloneParent(clone $clone);
        $this->excel->disconnectWorksheets();
    }

    /**
     * Get the sheet by id or name, else get the active sheet
     *
     * @param callable|integer|string $sheetID
     *
     * @return \PHPExcel_Worksheet
     */
    protected function getSheetByIdOrName($sheetID)
    {
        // If is a string, return the sheet by name
        if (is_string($sheetID)) {
            return $this->excel->getSheetByName($sheetID);
        }

        // Else it should be the sheet index
        return $this->excel->getSheet($sheetID);
    }

    /**
     * Get the file title
     *
     * @return string
     */
    public function getTitle()
    {
        return $this->excel->getProperties()->getTitle();
    }

    /**
     * Get the current filename
     *
     * @return mixed
     */
    public function getFileName()
    {
        return pathinfo($this->file, PATHINFO_FILENAME);
    }

    /**
     * Check if the writer has the called method
     *
     * @param $method
     *
     * @return bool
     */
    protected function writerHasMethod($method)
    {
        $this->initNewWriterWhenNeeded();

        return method_exists($this->writer, $method) ? true : false;
    }

    /**
     * Init a new writer instance when it doesn't exist yet
     */
    protected function initNewWriterWhenNeeded()
    {
        if (!$this->writer) {
            $this->writer = app('excel.writer');
            $this->writer->injectExcel($this->excel, false);
            $this->writer->setFileName($this->getFileName());
            $this->writer->setTitle($this->getTitle());
        }
    }

    /**
     * Set the write format
     *
     * @return LaraveExcelReader
     */
    protected function _setFormat()
    {
        $this->format = $this->identifier->getFormatByFile($this->file);

        return $this;
    }

    /**
     * Parse the file
     *
     * @param  array $columns
     *
     * @return void
     */
    protected function _parseFile($columns = [])
    {
        // Merge the selected columns
        $columns = array_merge($this->columns, $columns);

        // Parse the file
        $parser       = new ExcelParser($this);
        $this->parsed = $parser->parseFile($columns);
    }

    /**
     * Set the writer
     *
     * @return LaraveExcelReader
     */
    protected function _setReader()
    {
        // Init the reader
        $this->reader = PHPExcel_IOFactory::createReader($this->format);
        $this->_setReaderDefaults();

        return $this;
    }

    /**
     * Set the input encoding
     *
     * @param boolean $encoding
     *
     * @return LaraveExcelReader
     */
    protected function _setInputEncoding($encoding = false)
    {
        if ($this->format == 'CSV') {
            // If no encoding was given, use the config value
            $encoding = $encoding ? $encoding : config('excel.import.encoding.input', 'UTF-8');
            $this->reader->setInputEncoding($encoding);
        }

        return $this;
    }

    /**
     * Set reader defaults
     *
     * @return void
     */
    protected function _setReaderDefaults()
    {
        // Set CSV delimiter
        if ($this->format == 'CSV') {
            // If no delimiter was given, take from config
            if (!$this->delimiter) {
                $this->reader->setDelimiter(config('excel.csv.delimiter', ','));
            } else {
                $this->reader->setDelimiter($this->delimiter);
            }

            if (!$this->enclosure) {
                $this->reader->setEnclosure(config('excel.csv.enclosure', '"'));
            } else {
                $this->reader->setEnclosure($this->enclosure);
            }
        }

        // Set default calculate
        $this->calculate = config('excel.import.calculate', true);

        // Set default for ignoring empty cells
        $this->ignoreEmpty = config('excel.import.ignoreEmpty', true);

        // Set default date format
        $this->dateFormat = config('excel.import.dates.format', 'Y-m-d');

        // Date formatting disabled/enabled
        $this->formatDates = config('excel.import.dates.enabled', true);

        // Set default date columns
        $this->dateColumns = config('excel.import.dates.columns', []);

        // Set default include charts
        $this->reader->setIncludeCharts(config('excel.import.includeCharts', false));
    }

    /**
     * Reset the writer
     *
     * @return void
     */
    public function _reset()
    {
        $this->excel->disconnectWorksheets();
        $this->resetValueBinder();
        unset($this->parsed);
    }

    /**
     * Get excel object
     *
     * @return PHPExcel
     */
    public function getExcel()
    {
        return $this->excel;
    }

    /**
     * Dynamically call methods
     *
     * @param  string $method
     * @param  array  $params
     *
     * @throws LaravelExcelException
     */
    public function __call($method, $params)
    {
        // Call a php excel method
        if (method_exists($this->excel, $method)) {
            // Call the method from the excel object with the given params
            return call_user_func_array([$this->excel, $method], $params);
        } // If it's a reader method
        elseif (method_exists($this->reader, $method)) {
            // Call the method from the excel object with the given params
            return call_user_func_array([$this->reader, $method], $params);
        } elseif ($this->writerHasMethod($method)) {
            return call_user_func_array([$this->writer, $method], $params);
        }

        throw new LaravelExcelException('[ERROR] Reader method [' . $method . '] does not exist.');
    }

}
