<?php

require_once (LIB_PATH.'XLSXWriter'.SLASH.'XLSXWriter.class.php');

class ExcelExport{
	public $mime = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
	public $extension = 'xlsx';
	public $description = 'Fichier classeur de données Excel';
	public $tempFiles = array();
	public $source = '';
	public $spreadsheet;

	public  function sample($dataset, $level=0, $parent=''){
		$data = $this->recursive_sample($dataset,$level ,$parent);
		$stream = Excel::exportArray($data, null ,'Sans titre');
		return  $stream;
	}

	public function recursive_sample($dataset, $level=0, $parent=''){
		$stream = array();
		$parent = ($parent!=''?$parent.'.':'');
		$indentation = str_repeat("\t", $level);
		foreach($dataset as $macro => $infos){
			$infos['type'] = isset($infos['type']) ? $infos['type'] : '';
			switch($infos['type']){
				case 'list':
					$stream[] = array('Macros disponibles :' => $indentation.'-'.$parent.$macro.' '.(isset($infos['label'])?': '.$infos['label']:'').' (liste)'.PHP_EOL);
					$stream[] = array('Macros disponibles :' => $indentation.'{{#'.$macro.'}}'.PHP_EOL);
					if(is_array($infos['value']) && isset($infos['value'][0])) $stream = array_merge($stream, self::recursive_sample($infos['value'][0],$level+1));
					$stream[] = array('Macros disponibles :' =>$indentation.'{{/'.$macro.'}}');
				break;
				case 'object':
					$stream[] = array('Macros disponibles :' => $indentation.'-'.$parent.$macro.' '.(isset($infos['label'])?': '.$infos['label']:'').PHP_EOL);
					$stream = array_merge($stream, self::recursive_sample($infos['value'],$level+1,$parent.$macro));
				break;
				case 'image' :
					$stream[] = array('Macros disponibles :' => $indentation.'{{'.$parent.$macro.'::image}} : '.( !isset($infos['label']) ? '': $infos['label']).PHP_EOL);
				break;
				default :
					$stream[] = array('Macros disponibles :' => $indentation.'{{'.$parent.$macro.'}} : '.( !isset($infos['label']) ? '': $infos['label']).PHP_EOL);
				break;
			}
		}
		return $stream;
	}

	//Remplacement du tag d'image par l'image concernée
	//dans le fichier modèle
	public function add_image($worksheet, $macro, $value, $cellCoord, $scale=2){
		$finfo = new finfo(FILEINFO_MIME);
		$mime = $finfo->buffer($value);

		$ext = 'jpg';
		switch($mime){
			case 'image/jpeg': $ext = 'jpeg'; break;
			case 'image/png': $ext = 'png'; break;
			case 'image/gif': $ext = 'gif'; break;
		}
		if($mime == 'image/jpg') $mime = 'image/jpeg';

		$path = File::dir().'tmp'.SLASH.'template.'.time().'-'.rand(0,100).'.'.$ext;
		file_put_contents($path, $value);
		$this->tempFiles[] = $path;

		//On supprime le tag
		$cellVal = str_replace('{{'.$macro.'}}', '', $worksheet->getCell($cellCoord)->getValue());
		$worksheet->getCell($cellCoord)->setValue($cellVal);

		preg_match_all('~[A-Z]+|\d+~', $cellCoord, $cellMatches);
		$cellMatches = reset($cellMatches);
		$cellIndex = $cellMatches[0];
		$rowIndex = $cellMatches[1];

		//On récupère les infos de l'image
		list($width, $height) = getimagesize($path);

		//On définit la taille de la cellule à celle de l'image
		$cellWidth = $worksheet->getColumnDimension($cellIndex);
		$rowHeight = $worksheet->getRowDimension($rowIndex);
		//La largeur d'une colonne est définie en unité (Microsoft). 1px correpond à peu près à 0.2 unité
		$cellWidth->setWidth(($width/$scale)*0.2);
		//0.75 correspond à 1px en pt, et la hauteur d'une ligne est définie en pt
		$rowHeight->setRowHeight(($height/$scale)*0.75);

		//On ajoute l'image dans la feuille
		$drawing = new \PhpOffice\PhpSpreadsheet\Worksheet\Drawing();
		$drawing->setName($macro);
		$drawing->setPath($path);
		$drawing->setResizeProportional(true);
		$drawing->setWidthAndHeight($width/$scale,$height/$scale);
		$drawing->setCoordinates($cellCoord);
		$drawing->setWorksheet($worksheet);
	}

	public function start($stream){
		require(LIB_PATH.'PhpSpreadsheet'.SLASH.'vendor'.SLASH.'autoload.php');
		$this->source = File::dir().'tmp'.SLASH.'template.'.time().'-'.rand(0,100).'.xlsx';
		file_put_contents($this->source,utf8_decode($stream));

		$this->spreadsheet = \PhpOffice\PhpSpreadsheet\IOFactory::load($this->source);
		$this->spreadsheet->getActiveSheet()->getPageMargins()->setTop(0.2);
		$this->spreadsheet->getActiveSheet()->getPageMargins()->setRight(0.2);
		$this->spreadsheet->getActiveSheet()->getPageMargins()->setLeft(0.2);
		$this->spreadsheet->getActiveSheet()->getPageMargins()->setBottom(0.2);
		$this->spreadsheet->getActiveSheet()->getPageMargins()->setFooter(0.5);
		$this->spreadsheet->getActiveSheet()->getPageMargins()->setHeader(0.5);
		$this->spreadsheet->getActiveSheet()->getPageSetup()->setHorizontalCentered(true);

	}
	//Récupère et gère la structure du remplacement
	//des données dans le fichier template fourni
	public function from_template($stream, $data){
		require(LIB_PATH.'PhpSpreadsheet'.SLASH.'vendor'.SLASH.'autoload.php');

		//Pour chaque feuille dans le classeur Excel
		foreach ($this->spreadsheet->getAllSheets() as  $worksheet) {

			//On récupère la zone de travail (pour ne pas se perdre dans des cellules vide)
			//Avec la colonne max et la ligne max
			$maxCol = 'A';
			$maxRow = 0;
			foreach ($worksheet->getCoordinates() as $coord) {
				preg_match_all("/[A-Z]+|\d+/", $coord, $matches);
				$matches = reset($matches);
				$currCol = PhpOffice\PhpSpreadsheet\Cell\Coordinate::columnIndexFromString($matches[0]);
				$currRow = $matches[1];
				$currValue = $worksheet->getCell($coord)->getValue();

				if($maxCol < $currCol && !empty($currValue)) $maxCol = $currCol;
				if($maxRow < $currRow && !empty($currValue)) $maxRow = $currRow;
			}

			//On parcourt une fois le contenu du la feuille
			//pour avoir les différents bords des boucles, si
			//il y en a dans le fichier.
			$rows = $worksheet->toArray('', true, true, true);
			$loopDatas = array('startLine'=>0, 'endLine'=>0);
			$finalValues = array();

			foreach ($rows as $rowIdx => $cell) {
				if($rowIdx>$maxRow) continue;

				foreach ($cell as $cellIdx => $content) {
					if(empty($content) && PhpOffice\PhpSpreadsheet\Cell\Coordinate::columnIndexFromString($cellIdx)>$maxCol) continue;

					//@TODO: Faire en récursif !
					if(preg_match("/\{\{\#([^\/\#}]*)\}\}/is", $content, $matches)){
						$loopDatas['key'] = $matches[1];
						$loopDatas['startLine'] = $rowIdx;
						$loopDatas['startColumn'] = $cellIdx;
						$loopDatas['startCoord'] = $cellIdx.$rowIdx;
					}
					if(preg_match("/\{\{\/([^\/\#}]*)\}\}/is", $content, $matches)){
						$loopDatas['endLine'] = $rowIdx;
						$loopDatas['endColumn'] = $cellIdx;
						$loopDatas['endCoord'] = $cellIdx.$rowIdx;
						$loopDatas['totalRow'] = $loopDatas['endLine'] - $loopDatas['startLine'];
						$loopDatas['totalCol'] = letters_to_numbers($loopDatas['endColumn']) - letters_to_numbers($loopDatas['startColumn']);
					}
				}
			}

			//Définition type de boucle
			if($loopDatas['startLine'] != 0 && $loopDatas['endLine'] != 0)
				$loopDatas['loopType'] = ($loopDatas['startLine'] != $loopDatas['endLine']) ? 'vertical' : 'horizontal';

			//Récupération des infos en fonction du type de boucle
			if(isset($loopDatas['loopType'])){
				if($loopDatas['loopType'] == 'vertical'){
					//On parcourt par ligne puis par colonne
					foreach ($rows as $rowIdx => $cell) {
						if($rowIdx>$maxRow) continue;
						foreach ($cell as $cellIdx => $content) {
							if(PhpOffice\PhpSpreadsheet\Cell\Coordinate::columnIndexFromString($cellIdx)>$maxCol || ($rowIdx <= $loopDatas['startLine'] || $rowIdx >= $loopDatas['endLine'])) continue;
							$finalValues[$rowIdx][$cellIdx][] = $content;
						}
					}
				} else {
					//On parcourt par colonne puis par ligne
					for($col = $loopDatas['startColumn']; $col <= $loopDatas['endColumn']; ++$col){
						if($col == $loopDatas['startColumn'] || $col == $loopDatas['endColumn']) continue;
						for($row = $loopDatas['startLine']; $row <= $maxRow; ++$row) {
							$finalValues[$col][$row][] = $worksheet->getCell($col.$row)->getValue();
						}
					}
				}
			}

			//On remplace les données à l'intérieur
			//des boucles si des boucles sont présentes
			if(isset($finalValues) && !empty($finalValues)){
				$values = $this->decomposeKey($data, $loopDatas['key']);

				//Pour chaque entité
				foreach ($values as $i => $entity) {
					$rowIterator = $colIterator = 0;

					if($loopDatas['loopType'] == 'vertical'){
						unset($finalValues[$loopDatas['endLine']]);

						//On ajoute 1 car on insère AVANT la ligne, et non APRÈS
						$rowWhereInsert = $i==0 ? $loopDatas['endLine']+1 : $rowWhereInsert += 1;
						$worksheet->insertNewRowBefore($rowWhereInsert, 1);

						//Pour chaque ligne
						foreach ($finalValues as $rIdx => $cell) {
							$lineIdx = ($loopDatas['startLine']+1) + (($loopDatas['totalRow']-1)*$i) + $rowIterator;
							$maxRow += 1;

							//Pour chaque cellule
							foreach ($cell as $cIdx => $content) {
								$currCoord = $cIdx.$lineIdx;
								$lineRef = $lineIdx-($loopDatas['totalRow']-1);
								//Dans le cas particulier où $i vaut 0, on remplace directement les données dans la feuille
								$referentCell = $i==0 ? $currCoord : $cIdx.$lineRef;

								$worksheet->setCellValue($currCoord, $content[0]);
								$worksheet->duplicateStyle($worksheet->getStyle($referentCell),$currCoord);
								$maxCol += 1;

								//On remplace les données
								ExcelExport::replace_data($worksheet, $entity, $currCoord, 4);
							}
							$rowIterator++;
						}
					} else {
						//On ajoute 1 car on insère AVANT la ligne, et non APRÈS
						$colWhereInsert = $i==0 ? numbers_to_letters(letters_to_numbers($loopDatas['endColumn'])+1) : numbers_to_letters(letters_to_numbers($colWhereInsert)+1);
						$worksheet->insertNewColumnBefore($colWhereInsert, 1);

						//Pour chaque colonne
						foreach ($finalValues as $cIdx => $col) {
							$colIdx = numbers_to_letters((letters_to_numbers($loopDatas['startColumn'])+1)+(($loopDatas['totalCol']-1)*$i)+$colIterator);
							$maxCol += 1;

							//Pour chaque cellule
							foreach ($col as $rIdx => $content) {
								$currCoord = $colIdx.$rIdx;
								//Dans le cas où $i vaut 0, cas particulier, on remplace directement les données dans la feuille
								$referentCell = $i==0 ? $currCoord : $cIdx.$rIdx;

								$worksheet->setCellValue($currCoord, $content[0]);
								$worksheet->duplicateStyle($worksheet->getStyle($referentCell),$currCoord);
								$maxRow += 1;

								//On remplace les données
								ExcelExport::replace_data($worksheet, $entity, $currCoord, 4);
							}
							$colIterator++;
						}
					}
				}
			}

			//On remplace le reste des tag présents
			//sur la feuille de calcul du fichier template
			foreach ($worksheet->getRowIterator() as $rowIdx => $row) {
				if($rowIdx>$maxRow) continue;

				foreach ($row->getCellIterator() as $cellIdx => $cell) {
					if(empty($cell->getValue()) && PhpOffice\PhpSpreadsheet\Cell\Coordinate::columnIndexFromString($cellIdx)>$maxCol) continue;
					$this->replace_data($worksheet, $data, $cellIdx.$rowIdx);
				}
			}

			//@TODO: Gérer plusieurs boucles sur le document, et décrémenter pour chaque boucle présente les début et fin par suppression de lignes/colonnes
			//(cf. fin de fichier)
			//En fct du type de boucle on supprime les lignes/colonnes de début et fin de boucle
			if(isset($loopDatas['loopType'])){
				if($loopDatas['loopType'] == 'vertical'){
					//On supprime la ligne de début de boucle (la ligne avec le tag de début de loop)
					$worksheet->removeRow($loopDatas['startLine']);

					//Si le nb d'itération sur les lignes est supérieur au nb de lignes de données à remplacer pour 1 itération
					//Alors on indique à -1 sinon -2
					$rowToDelete = $rowIterator > $loopDatas['endLine']-$loopDatas['startLine']-1 ? $rowWhereInsert-1 : $rowWhereInsert-2;
					//On supprime la ligne de fin de boucle (la ligne avec le tag de fin de loop)
					$worksheet->removeRow($rowToDelete);

					//On supprime les N lignes insérées à chaque itération de boucle
					for ($i=1; $i<=$loopDatas['endLine']-$loopDatas['startLine']-1; $i++)
						$worksheet->removeRow($rowToDelete+$i);

				} else if($loopDatas['loopType'] == 'horizontal'){
					// On supprime la colonne de début de boucle
					$worksheet->removeColumn($loopDatas['startColumn']);

					//Si le nb d'itération sur les colonnes est supérieur au nb de colonnes de données à remplacer pour 1 itération
					//Alors on indique à -1 sinon -2
					$colToDelete = $colIterator > letters_to_numbers($loopDatas['endColumn'])-letters_to_numbers($loopDatas['startColumn'])-1 ? letters_to_numbers($colWhereInsert)-1 : letters_to_numbers($colWhereInsert)-2;
					//On supprime la colonne de fin de boucle (la colonne avec le tag de fin de loop)
					$worksheet->removeColumn(numbers_to_letters($colToDelete));

					for ($i=1; $i<=$loopDatas['endColumn']-$loopDatas['startColumn']-1; $i++)
						$worksheet->removeColumn(numbers_to_letters($colToDelete+$i));
				}
			}
		}
	}

	//Remplace les données d'un jeu de données en fonction
	//des tags rencontrés et fournis par le jeu de données
	public function replace_data($worksheet, $data=array(), $cellCoord, $imgScale=2){
		$cell = $worksheet->getCell($cellCoord);
		$cellVal = $cell->getValue();

		//gestion des simples variables
		$cellVal = preg_replace_callback('/{{([^#\/}]*)}}/',function($matches) use ($data,$cellCoord,$imgScale,$worksheet) {
			$key = $matches[1];
			$keyInfos = explode('::',$key);
			$key = $keyInfos[0];
			$type = isset($keyInfos[1]) ? $keyInfos[1] : 'string';

			$value = $this->decomposeKey($data,$key);
			if($type =='image'){
				if(isset($value)) ExcelExport::add_image($worksheet, $key, $value, $cellCoord, $imgScale);
				return;
			}

			if(is_numeric($value))
				$worksheet->getStyle($cellCoord)->getNumberFormat()->setFormatCode(PhpOffice\PhpSpreadsheet\Style\NumberFormat::FORMAT_NUMBER);
			else if(preg_match("/\d+(?:\.\d{1,2})? €/",$value))
				$worksheet->getStyle($cellCoord)->getNumberFormat()->setFormatCode(PhpOffice\PhpSpreadsheet\Style\NumberFormat::FORMAT_CURRENCY_EUR_SIMPLE);

			if(!isset($value)) return $this->formatValue($type,$matches[0]);
			if(is_array($value)) return 'Array';
			return $this->formatValue($type,$value);
		},$cellVal);

		$cell->setValue($cellVal);
	}

	public function end($stream,$data){
		$writer = \PhpOffice\PhpSpreadsheet\IOFactory::createWriter($this->spreadsheet, 'Xlsx');
		$writer->save($this->source);
		$stream = file_get_contents($this->source);
		unlink($this->source);

		//suppression des images temporaires
		foreach ($this->tempFiles as $file) {
			if(file_exists($file)) unlink($file);
		}
		return $stream;
	}

	public function formatValue($type,$value){
		return $value;
	}

	public function decomposeKey($datas,$key){
		if(array_key_exists($key, $datas)) return isset($datas[$key]) ? $datas[$key] : '' ;

		$attributes = explode('.',$key);
		$current = $datas;
		$value = null;

		foreach ($attributes as $attribute) {
			if(!array_key_exists($attribute, $current))  break;
			$current = $current[$attribute];
			$value = isset($current) ?  $current :  '';
		}
		return $value;
	}

	//Copie l'intégralité de la ligne depuis des
	//positions données à l'endroit voulu
	public static function copy_full_row(&$ws_from, &$ws_to, $row_from, $row_to) {
		$ws_to->getRowDimension($row_to)->setRowHeight($ws_from->getRowDimension($row_from)->getRowHeight());
		$lastColumn = $ws_from->getHighestColumn();
		++$lastColumn;
		for($c = 'A'; $c != $lastColumn; ++$c) {
			$cell_from = $ws_from->getCell($c.$row_from);
			$cell_to = $ws_to->getCell($c.$row_to);
			$cell_to->setXfIndex($cell_from->getXfIndex()); // black magic here
			$cell_to->setValue($cell_from->getValue());
		}
	}


}

