<?php
namespace ModulesAbstract\PluginReports\Report\Model\Exporter;

abstract class ExporterAbstract
{
	/**
	 * List of filters
	 * @var \ModulesAbstract\PluginReports\Report\Model\Filters\Filters
	 */
	protected $filters = null;

	/**
	 * A list of field types and their associated config
	 * Default supported configs looks like this:
	 * 	'pnr' => 	Array( 'type'=>'S', 	'req'=>'M' ),
	 * 	type
	 * 		S - string
	 * 		N - numeric(but treated as-is, no conversions done)
	 * 		NI - numeric, integer
	 * 		NF - numeric, float (2 decimals by default)
	 * 		D - date
	 *
	 * 	req
	 * 		M mandatory
	 *
	 * 	requires - list of required items, if the required items are missing, then this items is considered optional
	 *
	 *
	 * @var array
	 */
	protected $fieldTypes= [];

	/**
	 * Internal stats, accesible via getStats()
	 * @var array
	 */
	protected $stats= [];

	/**
	 * Stores error report object
	 * @var \ModulesAbstract\PluginReports\Report\Model\ErrorReport
	 */
	protected $errorReport = null;

	/**
	 * The report base path
	 * @var string
	 */
	protected $basePath = null;

	/**
	 * Stores report configs list
	 * @var array
	 */
	protected $cfg= [];


	/**
	 * Stores the parent exporter - used for MergeExporter
	 * @var \ModulesAbstract\PluginReports\Report\Model\Exporter\MergeExporter
	 */
	protected $parentExporter = null;

	/**
	 * Base constructor
	 */
	public function __construct()
	{
	}

	/**
	 * Export
	 */
	abstract public function exportReport();

	/**
	 * Internal use for initialize exporter
	 */
	abstract protected function objectInitialize();

	/**
	 * Called for every row that should get exported
	 * If not implemented, then exportReport() should be implemented and able to do a "block" export, with a bigger memory footprint
	 * @return \ModulesAbstract\PluginReports\Report\Model\Exporter\ExporterAbstract
	 */
	abstract public function exportRow($row);

	/**
	 * Get a total of all exported rows by type
	 * @return int
	 */
	abstract public function getExportedRowsCount($type);

	/**
	 * Get a list of all exported rows (if possible, some of them might have been written to disk)
	 * @param String $type
	 * @return Array
	 */
	abstract public function getExportedRows($type);

	/**
	 * Get a list of report sections
	 * @return Array:string
	 */
	abstract public function getExportedRowsTypes();

	/**
	 * Internal function for destroy exporter. This function should never be used outside this class
	 */
	protected function objectDestroy()
	{
	}

	/**
	 * Set the internal config
	 * @param array $cfg
	 */
	public function setCfg($key, $value)
	{
		$this->cfg[$key] = $value;
		return $this;
	}

	/**
	 * Get the internal cfg, or a certain section
	 * @param string $key
	 */
	// TODO: eliminta $key=null by default
	public function getCfg($key = null)
	{
		if($key === null){
			return $this->cfg;
		}
		return $this->cfg[$key];
	}


	/**
	 * Find out if a config key is set
	 * @param string $key
	 * @return boolean
	 */
	public function hasCfg($key)
	{
		return isset($this->cfg[$key]);
	}

	/**
	 * Get the list of required filters
	 * @return Array:string
	 */
	public function getRequiredFilters()
	{
		return [];
	}

	public function initializeFieldTypes()
	{
	}


	protected function isDebugExport()
	{
		return false;
	}

	/**
	 * Set error report
	 * @param \ModulesAbstract\PluginReports\Report\Model\ErrorReport $errorReport
	 */
	public function setErrorReport(\ModulesAbstract\PluginReports\Report\Model\ErrorReport $errorReport)
	{
		$this->errorReport = $errorReport;
		return $this;
	}

	public function setReportBasePath($basePath)
	{
		$this->basePath = $basePath;
		return $this;
	}

	/**
	 * Get the configured report base path
	 * @return string
	 */
	public function getReportBasePath()
	{
		return $this->basePath;
	}

	/**
	 * Called when initialize a new report
	 */
	public function initialize()
	{
		$this->objectInitialize();
	}

	/**
	 * Destroy the exporter, close files handlers
	 */
	public function destroy()
	{
		$this->objectDestroy();
	}

	/**
	 * Get field types
	 * @return Array
	 */
	public function getFieldTypes()
	{
		return $this->fieldTypes;
	}


	/**
	 * Get the internal list of gathered stats
	 * @return array
	 */
	public function getInternalStats()
	{
		return $this->stats;
	}


	/**
	 * Log export messages
	 * @param \App\MessagesCollector $msgs
	 */
	public function addFieldValidatorExceptionData(\App\MessagesCollector $msgs, $newValue, $reportName, $field, $fieldCfg, $rawRow, $validatedRow)
	{
		foreach ($msgs->getMessages() as $msgs) {
			$context = ['validatedRow' => $validatedRow];
			$this->errorReport->log('error', $reportName, $field, $msgs->getText(), $newValue, (array)$rawRow['_identifiers'], $context);
		}
	}

	/**
	 * Add a filter param
	 * @param string $key
	 * @param mixed $value
	 * @return \ModulesAbstract\PluginReports\Report\Model\Gatherer\GathererAbstract
	 */
	public function addFilter($key, $value)
	{
		$this->filters[$key] = $value;
		return $this;
	}

	/**
	 * Format a value accordint to config type
	 * @param string $value
	 * @param array $cfg
	 * @throws \App\MessagesException
	 */
	protected function preFormat($value, array $cfg, array $rawRow=[])
	{

		// instantiate messge collector
		$msgs = new \App\MessagesCollector();

		$typeArr = explode(":", $cfg['type']);
		$type = reset($typeArr);

		// just to be sure. Some data comes htmlencoded from the DB
		if ($value!==null) {
			$value = htmlUnclean($value);
		}

		/**
		 * handle arrays
		 * 	arrays are mostly sent for internal purposes ( like '_identifiers' )
		 * 	this block simply serializes the array in a human-readable format
		 * 	!! this format can be changed anytime, and should not be relied upon anyway for real report data, only for internal purposes !!
		*/
		if (is_array($value)) {
			$vArr = $value;
			$value = "";
			foreach ($vArr as $k => $v) {
				$value .= sprintf("%s=>'%s'\n", $k, $v);
			}
		}

		// determine how we should format this field
		switch ($type) {
			case 'S':
				if ($value===null) {
					$value = '';
				}

				// apply operations
				if ($cfg['op']) {
					foreach ($cfg['op'] as $operation) {
						// ASCII
						if ($operation == 'ascii') {
							$value = \ModulesAbstract\PluginReports\Report\Model\Exporter\Transliterator::getInstance()->toASCII($value);
						}

						// UPPPER
						if ($operation == 'upper') {
							$value = mb_strtoupper($value, "UTF-8");
						}
					}
				}

			break;

			// bool
			case 'B':
				$value = ($value?'1':'0');
			break;

			case 'D':
				if ($value) {
					$value = date("Y-m-d H:i:s", strtotime($value));
				}
				else {
					$value = '0000-00-00 00:00:00';
				}
			break;

			case 'NI':
				$value = (int)$value;
			break;

			case 'NF':
				$decimals = ($typeArr[1]?$typeArr[1]:2);
				$value = sprintf("%.{$decimals}f", (float)$value);
			break;

			case 'N':
				$value = $value;
			break;

			default:
				throw new \ModulesAbstract\PluginReports\Report\Model\Exporter\MessagesException($msgs->addError(sprintf("Invalid config type: '%s'", $type)));
		}

		return $value;
	}

	/**
	 * Validate value according to configs
	 * @param string $value
	 * @param Array $cfg
	 */
	protected function validate($value, $field, $cfg)
	{
		$this->stats['validate:calls']++;
		// instantiate message collector
		$msgs = new \App\MessagesCollector();

		// bool fields are allowed to be missing....
		if ($cfg['type'] != 'B' ){
			if ($cfg['req'] == 'M') {
				if (in_array($cfg['type'], ['S'])) {
					if(!strlen((String)$value)){
						$msgs->addError(sprintf("Mandatory field"));
					}
				}
				else {
					if(!$value){
						$msgs->addError(sprintf("Mandatory field"));
					}
				}
			}
		}

		if($cfg['len'] && mb_strlen((string)$value) > $cfg['len']){
			$msgs->addError(sprintf("Field %s has been truncated. Value length is %s", $field, mb_strlen($value)));
		}

		// check if value len is in interval
		if (isset($cfg['minLen']) && mb_strlen((string)$value) < $cfg['minLen']) {
			$msgs->addError(sprintf("Field %s is too short. Value length is %s", $field, mb_strlen($value)));
		}

		// check if value len is in interval [minLen, maxLen]
		if (isset($cfg['maxLen']) && mb_strlen((string)$value) > $cfg['maxLen']) {
			$msgs->addError(sprintf("Field %s is too long. Value length is %s", $field, mb_strlen($value)));
		}

		// throw messages if any
		if ($msgs->hasError()) {
			$this->stats['validate:throws']++;
			throw new \ModulesAbstract\PluginReports\Report\Model\Exporter\MessagesException($msgs);
		}
 	}

 	/**
 	 * @param array $cfg
 	 * @return string
 	 */
	protected function postFormat($value, array $cfg, array $rawRow=[])
	{
		// add padding left or right
		if ($cfg['op']) {
			foreach ($cfg['op'] as $operation) {
				if ($cfg['len']) {
					$spaces = str_repeat((isset($cfg['alignChar'])?$cfg['alignChar']:" "), $cfg['len']);
					if ($operation == 'alignLeft') {
						$value = mb_substr((string)$value . $spaces, 0, $cfg['len']);
					}
					if ($operation == 'alignRight') {
						$value = mb_substr($spaces . (string)$value, -1 * $cfg['len']);
					}
				}
			}
		}

		// truncate the value
		if ($cfg['len'] && mb_strlen((string)$value) > $cfg['len']) {
			$value = mb_substr((string)$value, 0, $cfg['len']);
		}

		return $value;
	}

	protected function formatRow($reportName, $rawRow, $columns, $cfg)
	{
		if ($this->hasCfg('referenceColumns') && $this->getCfg('referenceColumns')[$reportName]) {
			$referenceColumns =  $this->getCfg('referenceColumns');
			$return = [];
			foreach ($columns as $column) {
				if (in_array($column, $referenceColumns[$reportName])) {
					$newVal = $this->formatValue($reportName, $column, $rawRow, $cfg);
					$return[] = $newVal;
				}
			}

		}
		else {
			$return = [];
			foreach ($columns as $column) {
				$newVal = $this->formatValue($reportName, $column, $rawRow, $cfg);
				$return[] = $newVal;
			}
		}

		return $return;
	}

	protected function formatValue($reportName, $column, $rawRow, $cfg)
	{
		$oval = $rawRow[$column];
		$newVal = $this->preFormat($oval, ($cfg[$column]?: []), $rawRow);

		try {
			$this->validate($newVal, $column, ($cfg[$column]?: []));
		}
		catch ( \App\MessagesException $ex ) {
			$this->addFieldValidatorExceptionData ( $ex->getMessagesCollector (), $newVal, $reportName, $column, $cfg[$column], $rawRow, $oval );
		}

		$newVal = $this->postFormat($newVal, ($cfg[$column]?: []), $rawRow);

		return $newVal;
	}

	/**
	 * @see https://gist.github.com/nebiros/226350
	 * @param string $pad_string
	 * @param string $pad_type
	 * @return string
	 */
	protected function mb_str_pad($input, $pad_length, $pad_string = ' ', $pad_type = STR_PAD_RIGHT)
	{
		$input = (string)$input;

		$diff = strlen( $input ) - mb_strlen( $input );

		return str_pad( $input, $pad_length + $diff, $pad_string, $pad_type );
	}

	/**
	 * Set parent exporter
	 * @param \ModulesAbstract\PluginReports\Report\Model\Builder\BuilderAbstract $builder
	 * @return \ModulesAbstract\PluginReports\Report\Model\Builder\BuilderAbstract
	 */
	public function setParentExporter(\ModulesAbstract\PluginReports\Report\Model\Exporter\ExporterAbstract $exporter)
	{
		$this->parentExporter = $exporter;
		return $this;
	}
}