<?php
/**
 * WhGroupGridView class
 *
 * A Grid View that groups rows by any column(s)
 *
 * @author Antonio Ramirez <amigo.cobos@gmail.com>
 * @copyright Copyright &copy; 2amigos.us 2013-
 * @license http://www.opensource.org/licenses/bsd-license.php New BSD License
 * @package YiiWheels.widgets.grid
 * @uses Yiistrap.widgets.TbHtml
 * @uses Yiistrap.widgets.TbGridView
 *
 * @author         Vitaliy Potapov <noginsk@rambler.ru>
 * @version        1.1
 * @see            http://groupgridview.demopage.ru
 */

Yii::import('yiistrap_fork.widgets.TbGridView');

class WhGroupGridView extends TbGridView
{

	const MERGE_SIMPLE = 'simple';
	const MERGE_NESTED = 'nested';
	const MERGE_FIRSTROW = 'firstrow';

	/**
	 * @var array $mergeColumns the columns to merge on the grid
	 */
	public $mergeColumns = array();

	/**
	 * @var string $mergeType the merge type. Defaults to MERGE_SIMPLE
	 */
	public $mergeType = self::MERGE_SIMPLE;

	/**
	 * @var string $mergeCellsCss the styles to apply to merged cells
	 */
	public $mergeCellCss = 'text-align: center; vertical-align: middle';

	/**
	 * @var array $extraRowColumns the group column names
	 */
	public $extraRowColumns = array();

	/**
	 * @var string $extraRowExpression
	 */
	public $extraRowExpression;

	/**
	 * @var array the HTML options for the extrarow cell tag.
	 */
	public $extraRowHtmlOptions = array();

	/**
	 * @var string $extraRowCssClass the class to be used to be set on the extrarow cell tag.
	 */
	public $extraRowCssClass = 'extrarow';

	/**
	 * @var array the column data changes
	 */
	private $_changes;

	/**
	 * Widget initialization
	 */
	public function init()
	{
		parent::init();

		/**
		 * check whether we have extraRowColumns set, forbid filters
		 */
		if (!empty($this->extraRowColumns)) {
			foreach ($this->columns as $column) {
				if ($column instanceof CDataColumn && in_array($column->name, $this->extraRowColumns)) {
					$column->filterHtmlOptions = array('style' => 'display:none');
					$column->filter = false;
				}
			}
		}
		/**
		 * setup extra row options
		 */
		if (isset($this->extraRowHtmlOptions['class']) && !empty($this->extraRowCssClass)) {
			$this->extraRowHtmlOptions['class'] .= ' ' . $this->extraRowCssClass;
		} else {
			$this->extraRowHtmlOptions['class'] = $this->extraRowCssClass;
		}
	}

	/**
	 * Renders the table body.
	 */
	public function renderTableBody()
	{
		if (!empty($this->mergeColumns) || !empty($this->extraRowColumns)) {
			$this->groupByColumns();
		}

		parent::renderTableBody();
	}

	/**
	 * Find and store changing of group columns
	 */
	public function groupByColumns()
	{
		$data = $this->dataProvider->getData();
		if (count($data) == 0) {
			return;
		}

		if (!is_array($this->mergeColumns)) {
			$this->mergeColumns = array($this->mergeColumns);
		}
		if (!is_array($this->extraRowColumns)) {
			$this->extraRowColumns = array($this->extraRowColumns);
		}

		//store columns for group. Set object for existing columns in grid and string for attributes
		$groupColumns = array_unique(array_merge($this->mergeColumns, $this->extraRowColumns));
		foreach ($groupColumns as $key => $colName) {
			foreach ($this->columns as $column) {
				if (property_exists($column, 'name') && $column->name == $colName) {
					$groupColumns[$key] = $column;
					break;
				}
			}
		}


		//values for first row
		$lastStored = $this->getRowValues($groupColumns, $data[0], 0);
		foreach ($lastStored as $colName => $value) {
			$lastStored[$colName] = array(
				'value' => $value,
				'count' => 1,
				'index' => 0,
			);
		}

		//iterate data
		for ($i = 1; $i < count($data); $i++) {
			//save row values in array
			$current = $this->getRowValues($groupColumns, $data[$i], $i);

			//define is change occurred. Need this extra foreach for correctly proceed extraRows
			$changedColumns = array();
			foreach ($current as $colName => $curValue) {
				if ($curValue != $lastStored[$colName]['value']) {
					$changedColumns[] = $colName;
				}
			}

			/**
			 * if this flag = true -> we will write change (to $this->_changes) for all grouping columns.
			 * It's required when change of any column from extraRowColumns occurs
			 */
			$saveChangeForAllColumns = (count(array_intersect($changedColumns, $this->extraRowColumns)) > 0);

			/**
			 * this changeOccurred related to foreach below. It is required only for mergeType == self::MERGE_NESTED,
			 * to write change for all nested columns when change of previous column occurred
			 */
			$changeOccurred = false;
			foreach ($current as $colName => $curValue) {
				//value changed
				$valueChanged = ($curValue != $lastStored[$colName]['value']);
				//change already occurred in this loop and mergeType set to MERGETYPE_NESTED
				$saveChange = $valueChanged || ($changeOccurred && $this->mergeType == self::MERGE_NESTED);

				if ($saveChangeForAllColumns || $saveChange) {
					$changeOccurred = true;

					//store in class var
					$prevIndex = $lastStored[$colName]['index'];
					$this->_changes[$prevIndex]['columns'][$colName] = $lastStored[$colName];
					if (!isset($this->_changes[$prevIndex]['count'])) {
						$this->_changes[$prevIndex]['count'] = $lastStored[$colName]['count'];
					}

					//update lastStored for particular column
					$lastStored[$colName] = array(
						'value' => $curValue,
						'count' => 1,
						'index' => $i,
					);

				} else {
					$lastStored[$colName]['count']++;
				}
			}
		}

		//storing for last row
		foreach ($lastStored as $colName => $v) {
			$prevIndex = $v['index'];
			$this->_changes[$prevIndex]['columns'][$colName] = $v;

			if (!isset($this->_changes[$prevIndex]['count'])) {
				$this->_changes[$prevIndex]['count'] = $v['count'];
			}
		}
	}

	/**
	 * Renders a table body row.
	 * @param int $row
	 */
	public function renderTableRow($row)
	{
		$change = false;
		if ($this->_changes && array_key_exists($row, $this->_changes)) {
			$change = $this->_changes[$row];
			//if change in extracolumns --> put extra row
			$columnsInExtra = array_intersect(array_keys($change['columns']), $this->extraRowColumns);
			if (count($columnsInExtra) > 0) {
				$this->renderExtraRow($row, $change, $columnsInExtra);
			}
		}

		// original CGridView code
		if ($this->rowCssClassExpression !== null) {
			$data = $this->dataProvider->data[$row];
			echo '<tr class="' . $this->evaluateExpression(
					$this->rowCssClassExpression,
					array('row' => $row, 'data' => $data)
				) . '">';
		} else if (is_array($this->rowCssClass) && ($n = count($this->rowCssClass)) > 0) {
			echo '<tr class="' . $this->rowCssClass[$row % $n] . '">';
		} else {
			echo '<tr>';
		}


		if (!$this->_changes) { //standart CGridview's render
			foreach ($this->columns as $column) {
				$column->renderDataCell($row);
			}
		} else { //for grouping
			foreach ($this->columns as $column) {
				$isGroupColumn = property_exists($column, 'name') && in_array($column->name, $this->mergeColumns);
				if (!$isGroupColumn) {
					$column->renderDataCell($row);
					continue;
				}

				$isChangedColumn = $change && array_key_exists($column->name, $change['columns']);

				//for rowspan show only changes (with rowspan)
				switch ($this->mergeType) {
					case self::MERGE_SIMPLE:
					case self::MERGE_NESTED:
						if ($isChangedColumn) {
							$options = $column->htmlOptions;
							$column->htmlOptions['rowspan'] = $change['columns'][$column->name]['count'];
							$column->htmlOptions['class'] = 'merge';
							$style = $column->htmlOptions['style'] ?? '';
							$column->htmlOptions['style'] = $style . ';' . $this->mergeCellCss;
							$column->renderDataCell($row);
							$column->htmlOptions = $options;
						}
						break;

					case self::MERGE_FIRSTROW:
						if ($isChangedColumn) {
							$column->renderDataCell($row);
						} else {
							echo '<td></td>';
						}
						break;
				}

			}
		}

		echo "</tr>\n";
	}

	/**
	 * Returns array of rendered column values (TD)
	 * @param string[]|TbDataColumn[] $columns
	 * @param CActiveRecord $data
	 * @param mixed $rowIndex
	 * @throws CException
	 * @return mixed
	 */
	private function getRowValues($columns, $data, $rowIndex)
	{
		foreach ($columns as $column) {
			if ($column instanceOf TbDataColumn) {
				$result[$column->name] = $this->getDataCellContent($column, $data, $rowIndex);
			} elseif (is_string($column)) {
				if (is_array($data) && array_key_exists($column, $data)) {
					$result[$column] = $data[$column];
				} elseif ($data instanceOf CActiveRecord && $data->hasAttribute($column)) {
					$result[$column] = $data->getAttribute($column);
				} else {
					throw new CException('Column or attribute "' . $column . '" not found!');
				}
			}
		}
		return $result ?? false;
	}

	/**
	 * Renders extra row
	 * @param mixed $beforeRow
	 * @param mixed $change
	 * @param array $columnsInExtra
	 */
	private function renderExtraRow($beforeRow, $change, $columnsInExtra)
	{
		$data = $this->dataProvider->data[$beforeRow];
		if ($this->extraRowExpression) { //user defined expression, use it!
			$content = $this->evaluateExpression(
				$this->extraRowExpression,
				array('data' => $data, 'row' => $beforeRow, 'values' => $change['columns'])
			);
		} else { //generate value
			$values = array();
			foreach ($columnsInExtra as $c) {
				$values[] = $change['columns'][$c]['value'];
			}

			$content = '<strong>' . implode(' :: ', $values) . '</strong>';
		}

		$colspan = count($this->columns);

		echo '<tr>';
		$this->extraRowHtmlOptions['colspan'] = $colspan;
		echo CHtml::openTag('td', $this->extraRowHtmlOptions);
		echo $content;
		echo CHtml::closeTag('td');
		echo '</tr>';
	}

	/**
	 * need to rewrite this function as it is protected in CDataColumn: it is strange as all methods inside are public
	 *
	 * @param TbDataColumn $column
	 * @param mixed $row
	 * @param mixed $data
	 *
	 * @return string
	 */
	private function getDataCellContent($column, $data, $row)
	{
		if ($column->value !== null) {
			$value = $column->evaluateExpression($column->value, array('data' => $data, 'row' => $row));
		} else if ($column->name !== null) {
			$value = CHtml::value($data, $column->name);
		}

		return !isset($value)
			? $column->grid->nullDisplay
			: $column->grid->getFormatter()->format(
				$value,
				$column->type
			);
	}
}
