<?php namespace ProcessWire;

/**
 * Class MarkupAdminDataTable
 * 
 * @method string render()
 * 
 * @property-read array $headerRow
 * @property-read array $footerRow
 * @property-read array $rows
 * @property-read array $rowClasses
 * @property-read array $rowAttrs
 * @property-read array $actions
 * @property bool $encodeEntities
 * @property bool $sortable
 * @property bool $resizable
 * @property string $class
 * @property string $caption
 * @property bool $responsive
 * @property array $settings
 * @property string $id
 * 
 */

class MarkupAdminDataTable extends ModuleJS {
	
	public static function getModuleInfo() {
		return array(
			'title' => 'Admin Data Table',
			'summary' => 'Generates markup for data tables used by ProcessWire admin',
			'version' => 107,
			'permanent' => true,
		);
	}

	const responsiveNo = 0; // responsive off
	const responsiveYes = 1; // each td becomes 1-row, each 2 columns with th + td side-by-side
	const responsiveAlt = 2; // each td becomes 1-row, with th + td stacked on top of each other

	/**
	 * Number of table instances, for unique id attributes
	 * 
	 * @var int
	 * 
	 */
	static protected $instanceCnt = 0;

	/**
	 * Table rows
	 * 
	 * @var array
	 * 
	 */
	protected $rows = array();
	
	protected $headerRow = array();
	
	protected $footerRow = array();
	
	protected $rowClasses = array();
	
	protected $rowAttrs = array();
	
	protected $actions = array();
	
	protected $colsNotSortable = array();
	
	protected $removeClasses = array();
	
	/**
	 * Initialize module and default settings
	 * 
	 */
	public function init() {
		
	
		// defaults for settings that are typically set globally for all tables
		$defaults = array(
			'class' => 'AdminDataTable AdminDataList',
			'addClass' => '', 
			'responsiveClass' => 'AdminDataTableResponsive',
			'responsiveAltClass' => 'AdminDataTableResponsiveAlt',
			'sortableClass' => 'AdminDataTableSortable',
			'resizableClass' => 'AdminDataTableResizable',
			'loadStyles' => true,
			'loadScripts' => true, 
		);
	
		// settings set globally for all tables (when present)
		$settings = $this->wire('config')->MarkupAdminDataTable;
		
		if(empty($settings)) {
			$settings = $defaults;
		} else {
			$settings = array_merge($defaults, $settings); 
		}
	
		if(!empty($settings['sortableClass'])) {
			$this->modules->get("JqueryTableSorter");
		}
		
		$this->loadStyles = $settings['loadStyles'];
		$this->loadScripts = $settings['loadScripts'];
		
		$this->set('encodeEntities', true);
		$this->set('sortable', true);
		$this->set('resizable', false);
		$this->set('class', ''); // extra class(es), populated by addClass() method
		$this->set('caption', '');
		$this->set('responsive', self::responsiveYes);
		$this->set('settings', $settings);
		$this->set('id', '');
		
		parent::init();
	}

	/**
	 * Get property
	 * 
	 * @param string $key
	 * @return mixed|null
	 * 
	 */
	public function get($key) {
		if($key === 'rows' || $key === 'headerRow' || $key === 'footerRow') return $this->$key;
		if($key === 'rowClasses' || $key === 'rowAttrs' || $key === 'actions') return $this->$key;
		return parent::get($key);
	}

	/**
	 * Set the header row for the table
	 * 
	 * Given $a array may be either array of labels (strings) or any of the array items may
	 * be an array, in which case index 0 is the label and index 1 is a class attribute to use 
	 * for the th element.
	 * 
	 * @param array $a Array of header row labels 
	 * @return $this
	 * 
	 */
	public function headerRow(array $a) {
		$this->headerRow = $a; 
		return $this; 
	}

	/**
	 * Set the footer row for the table
	 * 
	 * @param array $a Array of footer row labels (strings)
	 * @return $this
	 * 
	 */
	public function footerRow(array $a) {
		$this->footerRow = $this->setupRow($a);
		return $this;
	}

	/**
	 * Add a row to the table
	 * 
	 * @param array $a Array of columns that will each be a `<td>`, where each element may be one of the following:
	 *   - `string`: converts to `<td>string</td>`
	 *   - `$a["key"] = "value"`: converts to `<td><a href='value'>key</a></td>`
	 *   - `array('label' => 'url')`: converts to `<td><a href='url'>label</a></td>` (same as above, but in nested array with count 1)
	 *   - `array('label', 'class')`: converts to `<td class='class'>label</td>`
	 * @param array $options Optionally specify any one of the following:
	 *   - separator (bool): specify true to show a stronger visual separator above the column
	 *   - class (string): specify one or more class names to apply to the `<tr>`
	 *   - attrs (array): array of attr => value for attributes to add to the `<tr>`
	 * @return $this
	 * 
	 */
	public function row(array $a, array $options = array()) {
		$defaults = array(
			'separator' => false,
			'class' => '', 
			'attrs' => array(),
		);
		$options = array_merge($defaults, $options);
		$n = count($this->rows);
		if($options['separator']) {
			$options['class'] .= ($options['class'] ? ' ' : '') . 'AdminDataListSeparator';
		}
		$this->rows[$n] = $this->setupRow($a);
		if(!empty($options['class'])) $this->rowClasses[$n] = $options['class'];
		if(!empty($options['attrs'])) $this->rowAttrs[$n] = $options['attrs'];
		return $this; 
	}

	/**
	 * Setup/prepare a table row for rendering (internal)
	 * 
	 * @param array $a
	 * @return array
	 * 
	 */
	protected function setupRow(array $a) {
		$row = array();

		foreach($a as $k => $v) {
			if(is_array($v) && count($v) === 1 && is_int($k)) {
				// array('label' => 'href')
				$key = key($v);
				$v = reset($v);
				if(is_string($key)) $k = $key;
			}
			if(is_string($k)) {
				// Associative arrays get converted to: 
				// Anchor Text => URL
				$v = $this->encode($v);
				$k = $this->encode($k);
				$v = "<a href='$v'>$k</a>";
			} else if(is_array($v)) {
				// class name is specified in $v[1]
				foreach($v as $kk => $vv) {
					$v[$kk] = $this->encode($vv);
				}
			} else {
				$v = $this->encode($v); 
			}
			$row[] = $v; 
		}
		return $row;
	}

	/**
	 * Add action(s) button underneath the table
	 * 
	 * @param array $action Array in format array('button-label' => 'url')
	 * @return $this
	 * 
	 */
	public function action(array $action) {
		foreach($action as $label => $url) { 
			$this->actions[$label] = $url;
		}
		return $this;
	}

	/**
	 * Render the table
	 * 
	 * @return string
	 * 
	 */
	public function ___render() {

		$tableClass = trim($this->settings('class') . ' ' . $this->class); 
		if($this->responsive == self::responsiveYes) {
			$tableClass .= ' ' . $this->settings('responsiveClass'); 
		} else if($this->responsive == self::responsiveAlt) {
			$tableClass .= ' ' . $this->settings('responsiveClass') . ' ' . $this->settings('responsiveAltClass'); 
		}
		if($this->sortable) $tableClass .= ' ' . $this->settings('sortableClass');
		if($this->resizable) {
			$tableClass .= ' ' . $this->settings('resizableClass');
			/** @var JqueryTableSorter $tableSorter */
			$tableSorter = $this->modules->get('JqueryTableSorter');
			$tableSorter->use('widgets');
		}
		if($this->settings('addClass')) $tableClass .= ' ' . $this->settings('addClass');
		$out = '';
		$maxCols = 0;
		$id = $this->id ? $this->id : "AdminDataTable" . (++self::$instanceCnt);
		
		if(count($this->removeClasses)) {
			$tableClasses = explode(' ', $tableClass);
			foreach($tableClasses as $key => $cls) {
				if(in_array($cls, $this->removeClasses)) unset($tableClasses[$key]);
			}
			$tableClass = implode(' ', $tableClasses);
		}

		if(count($this->rows)) { 
			$out = "\n<table id='$id' class='$tableClass'>";

			if($this->caption) $out .= "\n\t<caption>{$this->caption}</caption>";

			if(count($this->headerRow)) {
				$out .= "\n\t<thead>\n\t<tr>";
				foreach($this->headerRow as $th) {
					$class = '';
					if(is_array($th)) list($th, $class) = $th;
					if(isset($this->colsNotSortable[$maxCols])) $class = trim("$class sorter-false");
					$th = $this->encode($th);
					if($class) $class = " class='" . $this->encode($class) .  "'";
					$out .= "\n\t\t<th$class>$th</th>";
					$maxCols++;
				}
				$out .= "\n\t</tr>\n\t</thead>";
			}

			if(count($this->footerRow)) {
				$out .= "\n\t<tfoot>\n\t<tr>";
				foreach($this->footerRow as $td) {
					$out .= "\n\t\t\t<td>$td</td>"; 
				}
				$out .= "\n\t</tr>\n\t</tfoot>";
			}

			$out .= "\n\t<tbody>";
			foreach($this->rows as $n => $row) {
				$cols = count($row);
				if($cols > $maxCols) $maxCols = $cols; 
				if($cols < $maxCols) for(; $cols < $maxCols; $cols++) $row[] = '&nbsp;';
				$attrs = isset($this->rowAttrs[$n]) ? $this->rowAttrs[$n] : array();
				if(isset($this->rowClasses[$n])) $attrs['class'] = $this->rowClasses[$n];
				$attrStr = '';
				foreach($attrs as $attrName => $attrVal) {
					$attrStr .= " $attrName='" . $this->wire('sanitizer')->entities($attrVal) . "'";
				}
				$out .= "\n\t\t<tr$attrStr>";
				foreach($row as $td) {
					$class = '';
					if(is_array($td)) list($td, $class) = $td;
					if(strlen("$td") == 0 || $td === '&nbsp;') $class .= ($class ? ' ' : '') . 'blank';
					if($class) $class = " class='$class'";
					$out .= "\n\t\t\t<td$class>$td</td>"; 
				}
				$out .= "\n\t\t</tr>";
			}
			$out .= "\n\t</tbody>";
			$out .= "\n</table>";
			
			if($this->responsive && strpos($this->settings('responsiveClass'), 'AdminDataTableResponsive') === 0) {
				$out .= "\n<script>if(typeof AdminDataTable != 'undefined') AdminDataTable.initTable($('#$id'));</script>";
			}
		}

		if(count($this->actions)) {
			$out .= "\n<p>";
			foreach($this->actions as $label => $url) {
				/** @var InputfieldButton $button */
				$button = $this->modules->get("InputfieldButton"); 
				$button->href = $url;
				$button->value = $label;
				$out .= $button->render();
			}
			$out .= "\n</p>";
		}
		
		return $out; 
	}

	/**
	 * Entity encode string (when entity encoding enabled)
	 * 
	 * @param string $str
	 * @return string
	 * 
	 */
	protected function encode($str) {
		if(!$this->encodeEntities) return $str; 
		return htmlspecialchars($str, ENT_QUOTES, 'UTF-8'); 
	}

	/**
	 * Set whether or not entity encoding is enabled
	 * 
	 * @param bool $encodeEntities
	 * 
	 */
	public function setEncodeEntities($encodeEntities = true) {
		$this->encodeEntities = $encodeEntities ? true : false; 
	}

	/**
	 * Set class(es) to add to table
	 * 
	 * @param string $class
	 * 
	 */
	public function setClass($class) {
		$this->class = $this->encode($class); 
	}

	/**
	 * Add a class to the table (without replacing existing ones)
	 * 
	 * @param string $class
	 * 
	 */
	public function addClass($class) {
		$this->class = trim($this->class . " " . $this->encode($class)); 
	}
	
	/**
	 * Remove a class from the table
	 *
	 * @param string $class
	 * @since 3.0.232
	 *
	 */
	public function removeClass($class) {
		$classes = strrpos($class, ' ') ? explode(' ', $class) : array($class);
		$this->removeClasses = array_merge($this->removeClasses, $classes);
	}

	/**
	 * Set whether or not table is sortable
	 * 
	 * @param bool $sortable
	 * 
	 */
	public function setSortable($sortable) {
		$this->sortable = $sortable ? true : false;
	}

	/**
	 * Set whether or not table is resizable
	 * 
	 * @param bool $resizable
	 * 
	 */
	public function setResizable($resizable) {
		$this->resizable = $resizable ? true : false;
	}

	/**
	 * Set table caption
	 * 
	 * @param string $caption
	 * 
	 */
	public function setCaption($caption) {
		$this->caption = $this->encode($caption); 
	}

	/**
	 * Set table id attribute
	 * 
	 * @param string $id
	 * 
	 */
	public function setID($id) {
		$this->id = $id; 
	}

	/**
	 * Set a column as not sortable (first column is 0)
	 * 
	 * @param int $index
	 * @since 3.0.215
	 * 
	 */
	public function setColNotSortable($index) {
		$index = (int) $index;
		$this->colsNotSortable[$index] = $index;
	}

	/**
	 * Set the responsive mode of this table
	 * 
	 * Default behavior is responsiveYes. Specify false or 0 to disable responsive.
	 * Or specify MarkupAdminDataTable::responsiveAlt for stacked th + td. 
	 * 
	 * @param int|bool $responsive
	 * 
	 */
	public function setResponsive($responsive = true) {
		$this->responsive = (int) $responsive; 
	}

	/**
	 * Get or set an internal setting 
	 * 
	 * @pw-internal
	 * 
	 * @param string $key Setting to get or set
	 * @param mixed $value Optional value to set
	 * @return string|int|array|null|MarkupAdminDataTable
	 * 
	 */
	public function settings($key, $value = null) {
		$settings = parent::get('settings');
		if(is_null($value)) {
			return isset($settings[$key]) ? $settings[$key] : null;
		} else {
			$settings[$key] = $value;
			parent::set('settings', $settings);
			return $this;
		}
	}

	public function isSingular() {
		return false; 
	}

	public function isAutoload() {
		return false; 
	}

}
