<?php

namespace TablePress\PhpOffice\PhpSpreadsheet\Reader;

use TablePress\PhpOffice\PhpSpreadsheet\Cell\IValueBinder;
use TablePress\PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException;
use TablePress\PhpOffice\PhpSpreadsheet\Reader\Exception as ReaderException;
use TablePress\PhpOffice\PhpSpreadsheet\Reader\Security\XmlScanner;
use TablePress\PhpOffice\PhpSpreadsheet\Shared\File;
use TablePress\PhpOffice\PhpSpreadsheet\Spreadsheet;

abstract class BaseReader implements IReader2
{
	/**
	 * Read data only?
	 * Identifies whether the Reader should only read data values for cells, and ignore any formatting information;
	 *        or whether it should read both data and formatting.
	 */
	protected bool $readDataOnly = false;

	/**
	 * Read empty cells?
	 * Identifies whether the Reader should read data values for all cells, or should ignore cells containing
	 *         null value or empty string.
	 */
	protected bool $readEmptyCells = true;

	/**
	 * Read charts that are defined in the workbook?
	 * Identifies whether the Reader should read the definitions for any charts that exist in the workbook;.
	 */
	protected bool $includeCharts = false;

	/**
	 * Restrict which sheets should be loaded?
	 * This property holds an array of worksheet names to be loaded. If null, then all worksheets will be loaded.
	 * This property is ignored for Csv, Html, and Slk.
	 *
	 * @var null|string[]
	 */
	protected ?array $loadSheetsOnly = null;

	/**
	 * Ignore rows with no cells?
	 * Identifies whether the Reader should ignore rows with no cells.
	 *        Currently implemented only for Xlsx.
	 */
	protected bool $ignoreRowsWithNoCells = false;

	/**
	 * Allow external images. Use with caution.
	 * Improper specification of these within a spreadsheet
	 * can subject the caller to security exploits.
	 */
	protected bool $allowExternalImages = false;

	/**
	 * Create a blank sheet if none are read,
	 * possibly due to a typo when using LoadSheetsOnly.
	 */
	protected bool $createBlankSheetIfNoneRead = false;

	/**
	 * Enable drawing pass-through?
	 * Identifies whether the Reader should preserve unsupported drawing elements (shapes, grouped images, etc.)
	 * by storing the original XML for pass-through during write operations.
	 * When enabled, drawings cannot be modified programmatically but are preserved exactly.
	 */
	protected bool $enableDrawingPassThrough = false;

	/**
	 * IReadFilter instance.
	 */
	protected IReadFilter $readFilter;

	/** @var resource */
	protected $fileHandle;

	protected ?XmlScanner $securityScanner = null;

	protected ?IValueBinder $valueBinder = null;

	/** @var null|callable(string):bool function to return whether image path is okay */
	protected $isWhitelisted;

	public function __construct()
	{
		$this->readFilter = new DefaultReadFilter();
	}

	public function getReadDataOnly(): bool
	{
		return $this->readDataOnly;
	}

	/**
				 * @return static
				 */
				public function setReadDataOnly(bool $readCellValuesOnly)
	{
		$this->readDataOnly = $readCellValuesOnly;

		return $this;
	}

	public function getReadEmptyCells(): bool
	{
		return $this->readEmptyCells;
	}

	/**
				 * @return static
				 */
				public function setReadEmptyCells(bool $readEmptyCells)
	{
		$this->readEmptyCells = $readEmptyCells;

		return $this;
	}

	public function getIgnoreRowsWithNoCells(): bool
	{
		return $this->ignoreRowsWithNoCells;
	}

	/**
				 * @return static
				 */
				public function setIgnoreRowsWithNoCells(bool $ignoreRowsWithNoCells)
	{
		$this->ignoreRowsWithNoCells = $ignoreRowsWithNoCells;

		return $this;
	}

	public function getIncludeCharts(): bool
	{
		return $this->includeCharts;
	}

	/**
				 * @return static
				 */
				public function setIncludeCharts(bool $includeCharts)
	{
		$this->includeCharts = $includeCharts;

		return $this;
	}

	public function getEnableDrawingPassThrough(): bool
	{
		return $this->enableDrawingPassThrough;
	}

	/**
				 * @return static
				 */
				public function setEnableDrawingPassThrough(bool $enableDrawingPassThrough)
	{
		$this->enableDrawingPassThrough = $enableDrawingPassThrough;

		return $this;
	}

	/** @return null|string[] */
	public function getLoadSheetsOnly(): ?array
	{
		return $this->loadSheetsOnly;
	}

	/** @param null|string|string[] $sheetList
				 * @return static */
				public function setLoadSheetsOnly($sheetList)
	{
		if ($sheetList === null) {
			return $this->setLoadAllSheets();
		}

		$this->loadSheetsOnly = is_array($sheetList) ? $sheetList : [$sheetList];

		return $this;
	}

	/**
				 * @return static
				 */
				public function setLoadAllSheets()
	{
		$this->loadSheetsOnly = null;

		return $this;
	}

	public function getReadFilter(): IReadFilter
	{
		return $this->readFilter;
	}

	/**
				 * @return static
				 */
				public function setReadFilter(IReadFilter $readFilter)
	{
		$this->readFilter = $readFilter;

		return $this;
	}

	/**
				 * USE WITH CAUTION (and in conjunction with setIsWhiteListed)!
				 * Allow external images;
				 * these can be specified within a spreadsheet
				 * in a way that can subject the caller to security exploits.
				 * @return static
				 */
				public function setAllowExternalImages(bool $allowExternalImages)
	{
		$this->allowExternalImages = $allowExternalImages;

		return $this;
	}

	public function getAllowExternalImages(): bool
	{
		return $this->allowExternalImages;
	}

	/**
				 * USE WITH CAUTION!
				 * Supply a callback to determine whether a path should be whitelisted,
				 * used in conjunction with setAllowExternalImages;
				 * supplying a method which might return true
				 * can subject the caller to security exploits.
				 *
				 * @param callable(string):bool $isWhitelisted
				 * @return static
				 */
				public function setIsWhitelisted(callable $isWhitelisted)
	{
		$this->isWhitelisted = $isWhitelisted;

		return $this;
	}

	/**
				 * Create a blank sheet if none are read,
				 * possibly due to a typo when using LoadSheetsOnly.
				 * @return static
				 */
				public function setCreateBlankSheetIfNoneRead(bool $createBlankSheetIfNoneRead)
	{
		$this->createBlankSheetIfNoneRead = $createBlankSheetIfNoneRead;

		return $this;
	}

	public function getSecurityScanner(): ?XmlScanner
	{
		return $this->securityScanner;
	}

	public function getSecurityScannerOrThrow(): XmlScanner
	{
		if ($this->securityScanner === null) {
			throw new ReaderException('Security scanner is unexpectedly null');
		}

		return $this->securityScanner;
	}

	protected function processFlags(int $flags): void
	{
		if (((bool) ($flags & self::LOAD_WITH_CHARTS)) === true) {
			$this->setIncludeCharts(true);
		}
		if (((bool) ($flags & self::READ_DATA_ONLY)) === true) {
			$this->setReadDataOnly(true);
		}
		if (((bool) ($flags & self::IGNORE_EMPTY_CELLS)) === true) {
			$this->setReadEmptyCells(false);
		}
		if (((bool) ($flags & self::IGNORE_ROWS_WITH_NO_CELLS)) === true) {
			$this->setIgnoreRowsWithNoCells(true);
		}
		if (((bool) ($flags & self::ALLOW_EXTERNAL_IMAGES)) === true) {
			$this->setAllowExternalImages(true);
		}
		if (((bool) ($flags & self::DONT_ALLOW_EXTERNAL_IMAGES)) === true) {
			$this->setAllowExternalImages(false);
		}
		if (((bool) ($flags & self::CREATE_BLANK_SHEET_IF_NONE_READ)) === true) {
			$this->setCreateBlankSheetIfNoneRead(true);
		}
	}

	protected function loadSpreadsheetFromFile(string $filename): Spreadsheet
	{
		throw new PhpSpreadsheetException('Reader classes must implement their own loadSpreadsheetFromFile() method');
	}

	/**
	 * Loads Spreadsheet from file.
	 *
	 * @param int $flags the optional second parameter flags may be used to identify specific elements
	 *                       that should be loaded, but which won't be loaded by default, using these values:
	 *                            IReader::LOAD_WITH_CHARTS - Include any charts that are defined in the loaded file
	 */
	public function load(string $filename, int $flags = 0): Spreadsheet
	{
		$this->processFlags($flags);

		try {
			return $this->loadSpreadsheetFromFile($filename);
		} catch (ReaderException $e) {
			throw $e;
		}
	}

	/**
	 * Open file for reading.
	 */
	protected function openFile(string $filename): void
	{
		$fileHandle = false;
		if ($filename) {
			File::assertFile($filename);

			// Open file
			$fileHandle = fopen($filename, 'rb');
		}
		if ($fileHandle === false) {
			throw new ReaderException('Could not open file ' . $filename . ' for reading.');
		}

		$this->fileHandle = $fileHandle;
	}

	/**
	 * Return worksheet info (Name, Last Column Letter, Last Column Index, Total Rows, Total Columns).
	 *
	 * @return array<int, array{worksheetName: string, lastColumnLetter: string, lastColumnIndex: int, totalRows: int, totalColumns: int, sheetState: string}>
	 */
	public function listWorksheetInfo(string $filename): array
	{
		throw new PhpSpreadsheetException('Reader classes must implement their own listWorksheetInfo() method');
	}

	/**
	 * Returns names of the worksheets from a file,
	 * possibly without parsing the whole file to a Spreadsheet object.
	 * Readers will often have a more efficient method with which
	 * they can override this method.
	 *
	 * @return string[]
	 */
	public function listWorksheetNames(string $filename): array
	{
		$returnArray = [];
		$info = $this->listWorksheetInfo($filename);
		foreach ($info as $infoArray) {
			$returnArray[] = $infoArray['worksheetName'];
		}

		return $returnArray;
	}

	public function getValueBinder(): ?IValueBinder
	{
		return $this->valueBinder;
	}

	/**
				 * @return static
				 */
				public function setValueBinder(?IValueBinder $valueBinder)
	{
		$this->valueBinder = $valueBinder;

		return $this;
	}

	protected function newSpreadsheet(): Spreadsheet
	{
		return new Spreadsheet();
	}
}
