<?php
/*
 * Copyright (c) 2013, webvariants GbR, http://www.webvariants.de
 *
 * This file is released under the terms of the MIT license. You can find the
 * complete text in the attached LICENSE file or online at:
 *
 * http://www.opensource.org/licenses/mit-license.php
 */

/**
 * Datentyp "Auswahlfeld"
 *
 * Dieser Datentyp stellt eine Auswahl von möglichen Werten für den Benutzer
 * dar. Die Darstellung kann als Selectbox, Checkboxen oder mit Radiobuttons
 * erfolgen, je nach gewünschter Semantik. Die Werte können entweder von Hand
 * eingetragen werden oder mittels SQL-SELECT-Statement aus der Datenbank
 * ausgelesen werden. Dabei ist zu beachten, dass nicht sichergestellt wird,
 * dass die eingegebene SQL-Anfrage wirklich ein SELECT-Statement ist. Jede
 * beliebige Anfrage kann ausgeführt werden, daher sollte nur ein begrenzter
 * Personenkreis die Erlaubnis haben, Queries anzugeben.
 *
 * Ein SELECT-Statement sollte immer die Form
 *
 * @verbatim
 * SELECT key,value FROM mytable WHERE ...
 * @endverbatim
 *
 * haben. Dabei kann im gesamten Query \p %%CLANG%% verwendet werden, um die
 * aktuelle Sprach-ID automatisch in die Anfrage einsetzen zu lassen. Die Anfrage
 * sollte immer zwei Spalten zurückgeben, wobei die erste Schlüsseleigenschaften
 * (Eindeutigkeit) besitzen sollte.
 *
 * Jede Liste ist intern immer assoziativ. Werden beim Bearbeiten einer
 * Metainformation einzelne Elemente entfernt, so müssen nur die betroffenen
 * Daten und nicht sämtliche Metadaten entfernt werden.
 *
 * Listen können eine Minimal- und eine Maximalanzahl zugewiesen bekommen. Der
 * Datentyp stellt dann sicher, dass entsprechend viele Elemente vom Benutzer
 * ausgewählt werden.
 *
 * Als einziger Datentyp unterstützt er die Suche mittels
 * WV_Datatype::FILTER_BY_KEY.
 *
 * @ingroup datatypes
 */
class _WV_Datatype_Select extends _WV_Datatype_Base implements WV_IDatatype {
	/**
	 * Extrahiert aus einem Datentyp alle möglichen Werte
	 *
	 * Shorthand-Funktion für getSingleValues(). Wird dazu verwendet, um nicht
	 * die aktuell ausgewählten Elemente zu erhalten, sondern alle möglichen. Der
	 * Einfachheit halber gibt es daher das Shortcut, da das Belegen von
	 * deserializeValue() mit null weniger intuitiv ist.
	 *
	 * @param  string $params  die serialisierten Parameter aus der Datenbank
	 * @return array           assoziative Liste aller möglichen Werte einer Metainfo
	 */
	public function extractValuesFromParams(array $params) {
		$params['data_in_value'] = false;
		return $this->deserializeValue(null, $params);
	}

	/**
	 * Hilfsmethode für den Meta-Provider
	 *
	 * Normalerweise verwendet kein Datentyp assoziative Listen, sodass das
	 * Mergen zweier Listen per array_merge erfolgen kann. Da dieser Datentyp
	 * aber assoziative Listen verwendet, gibt er hier true zurück, um den
	 * Meta-Provider anzuweisen, dies entsprechend zu bearbeiten.
	 *
	 * @return boolean  immer true
	 */
	public function usesAssociativeResults() {
		return true;
	}

	/**
	 * Deserialisieren (Nachbearbeiten) von Metadaten
	 *
	 * Wird für $value ein null übergeben, so gibt diese Methode für die
	 * Parameter des Datentyps nicht die zurück, die der Artikel angenommen hat,
	 * sondern einfach alle.
	 *
	 * Als Ergebnis erhält man ein assoziatives Array mit den Elementen des
	 * Auswahlfelds.
	 *
	 * @param  string $value   der (noch serialisierte) Wert eines Metadatums aus der Datenbank
	 * @param  string $params  die (noch serialisierten) Datentyp-Parameter
	 * @return array           Liste von (ggf. nur den angenommenen) Werten
	 */
	public function deserializeValue($value, array $params) {
		$elements = array();

		if ($value !== null) {
			$selected = mb_strlen($value) !== 0 ? array_filter(array_map('strval', explode('|', $value))) : array();
		}

		// Werte werden übernommen, wenn sie entweder in der Liste ($values) sind
		// oder gar keine Liste vorgegeben wurde (in diesem Fall wollte der Aufrufer
		// eine Liste der verfügbaren Optionen, nicht der ausgewählten).

		extract($this->deserializeParams($params));

		foreach ($sourceValues as $key => $v) {
			if (!isset($selected) || in_array(strval($key), $selected, true)) {
				if ($dataInValue) {
					$elements[] = strval($key);
				}
				else {
					$elements[strval($key)] = $v;
				}
			}
		}

		return $elements;
	}

	public function serializeValue($value, array $params) {
		$dataInValue = !empty($params['data_in_value']);
		return is_array($value) ? implode('|', $dataInValue ? $value : array_keys($value)) : '';
	}

	/**
	 * Parameter auf Kompatibilität prüfen
	 *
	 * Diese Methode entscheidet, ob die gemachten Änderungen am Datentyp (an
	 * den Parametern) dazu führen, dass Daten konvertiert oder gelöscht werden
	 * müssen.
	 *
	 * Eine Nachbearbeitung ist notwendig, wenn:
	 *
	 * - Die Maximalanzahl auszuwählender Elemente reduziert wurde (überhängende Elemente).
	 * - Die Datenquelle sich von SQL zu Liste oder umgekehrt geändert hat.
	 * - Elemente aus einer normalen Listen entfernt wurden.
	 *
	 * @param  array $oldParameters  die alten Parameter
	 * @param  array $newParameters  die neuen Parameter
	 * @return mixed                 true, wenn die Daten kompatibel sind, sonst ein array(Fehlermeldung, betroffene Objekte)
	 */
	public function checkParamCompatibility(array $oldParameters, array $newParameters) {
		extract($this->deserializeParams($oldParameters, 'old'));
		extract($this->deserializeParams($newParameters, 'new'));

		$messages  = array();
		$whereStmt = null;

		// Prüfen, ob die maximale Anzahl an Elementen reduziert wurde.

		if ($newMax < $oldMax) {
			$messages[] = t('devutils_datatype_select_shrinked');
			$expr       = implode('|', array_fill(0, $newMax+1, '%')); // %|%|%...
			$whereStmt  = '$$$value_column$$$ LIKE "'.$expr.'"';
		}

		// Hat sich die Datenquelle auch noch geändert?

		if ($oldSource !== $newSource && ($oldSource === 'values' || $newSource === 'values')) {
			$messages[] = t('devutils_datatype_select_source_changed');
			return array($messages, '1');
		}

		// OK, die Datenquelle ist gleich geblieben. Wenn es sich um eine Werteliste
		// handelt, prüfen wir, ob Schlüssel fehlen.

		if ($newSource === 'values') {
			$newKeys = array_map('strval', array_keys($newSourceValues));
			$sql     = WV_SQL::getInstance();

			foreach ($oldSourceValues as $key => $value) {
				if (!in_array(strval($key), $newKeys, true)) {
					if (empty($whereStmt)) $whereStmt = '0';

					// Ist der Wert in der Mitte, am Anfang oder am Ende der Liste enthalten?

					$whereStmt .= ' OR $$$value_column$$$ LIKE '.$sql->quote("%|$key|%");
					$whereStmt .= ' OR $$$value_column$$$ LIKE '.$sql->quote("$key|%");
					$whereStmt .= ' OR $$$value_column$$$ LIKE '.$sql->quote("%|$key");

					$messages[] = t('devutils_datatype_select_key_missing', $key);
				}
			}

			if ($messages) {
				return array($messages, $whereStmt);
			}
		}

		// Falls die Datenquelle nicht geändert wurde, aber andere
		// Probleme ausstehen, geben wir diese nun zurück.

		if ($messages) return array($messages, $whereStmt);
		return true;
	}

	/**
	 * Inkompatible Datensätze verarbeiten
	 *
	 * Diese Methode löscht oder korrigiert durch das Bearbeiten des Datentyps
	 * inkompatibel gewordene Datensätze. Dabei werden nicht mehr vorhandene
	 * Schlüssel aus Objekten entfernt und überstehende Elemente abgeschnitten.
	 *
	 * Wenn sich die Datenquelle geändert hat, werden alle Metadaten dieser
	 * Metainformation entfernt. Wurde ein Element aus der Auswahlliste entfernt,
	 * so wird nur dieser entsprechende Eintrag von den Objekten entfernt.
	 * Wenn die Maximalanzahl an erlaubten Elementen reduziert wurde, werden die
	 * überstehenden x Elemente entfernt. Dabei zählt die Reihenfolge der
	 * Elemente, wie sie im Datentyp definiert wurden.
	 *
	 * @param array $oldParameters  die alten Parameter
	 * @param array $newParameters  die neuen Parameter
	 */
	public function getIncompatibilityUpdateStatement(array $oldParameters, array $newParameters) {
		extract($this->deserializeParams($oldParameters, 'old'));
		extract($this->deserializeParams($newParameters, 'new'));

		$actions = array();

		// Wenn sich die Datenquelle ändert, müssen wir die Daten vollständig löschen.

		if ($oldSource !== $newSource && ($oldSource === 'values' || $newSource === 'values')) {
			return array(array('UPDATE', '$$$value_column$$$ = ""', '1'));
		}

		// Nicht mehr vorhandene Schlüssel der Liste aus den Metadaten entfernen.
		// Das machen wir, bevor wir nachher die überhängenden Werte entfernen. Auf
		// diese Weise müssen wir nicht mehr löschen als wirklich nötig.

		if ($newSource === 'values') {
			$newKeys = array_keys($newSourceValues);
			$sql     = WV_SQL::getInstance();

			foreach ($oldSourceValues as $key => $value) {
				if (!in_array((string) $key, $newKeys, true)) {
					// Da wo der Wert in der Mitte steht, ersetzt du ihn in der Mitte.

					$actions[] = array(
						'UPDATE',
						'$$$value_column$$$ = REPLACE($$$value_column$$$, '.$sql->quote("|$key|").', "|")',
						'$$$value_column$$$ LIKE '.$sql->quote("%|$key|%")
					);

					// Da wo er vorn steht, ...

					$actions[] = array(
						'UPDATE',
						'$$$value_column$$$ = REPLACE($$$value_column$$$, '.$sql->quote("$key|").', "")',
						'$$$value_column$$$ LIKE '.$sql->quote("$key|%")
					);

					// Da wo er hinten steht, ...

					$actions[] = array(
						'UPDATE',
						'$$$value_column$$$ = REPLACE($$$value_column$$$, '.$sql->quote("|$key").', "")',
						'$$$value_column$$$ LIKE '.$sql->quote("%|$key")
					);
				}
			}
		}

		// Hat sich die Maximalanzahl verringert, müssen wir die Angaben auf die neue Länge kürzen.

		if ($newMax < $oldMax) {
			$expr = implode('|', array_fill(0, $newMax+1, '%')); // %|%|%...
			$actions[] = array(
				/* Operation   */ 'UPDATE',
				/* What to do? */ '$$$value_column$$$ = SUBSTRING_INDEX($$$value_column$$$, "|", '.$newMax.')',
				/* And where?  */ '$$$value_column$$$ LIKE "'.$expr.'"'
			);
		}

		return $actions;
	}

	public function isValueContained($valueToSearch, $valueFromDatabase, array $params, $operator) {
		extract($this->deserializeParams($params));

		if ($operator === null) $operator = WV_Datatype::FILTER_EXACTMATCH;

		if ($operator & WV_Datatype::FILTER_NOCASE) {
			$valueFromDatabase = mb_strtolower($valueFromDatabase);
			$valueToSearch     = mb_strtolower($valueToSearch);
		}

		$elements         = empty($valueFromDatabase) ? array() : explode('|', $valueFromDatabase);
		$elementsToSearch = empty($valueToSearch)     ? array() : explode('|', $valueToSearch);

		foreach ($elements as $element) {
			$key   = $element;
			$value = $sourceValues[$key];

			if ($operator & WV_Datatype::FILTER_NOCASE) {
				$value = mb_strtolower($value);
			}

			if ($operator & WV_Datatype::FILTER_EXACTMATCH) {
				if (strval($valueToSearch) == strval($operator & WV_Datatype::FILTER_BY_KEY ? $key : $value)) return true;
			}
			else {
				$yes = false;

				if ($operator & WV_Datatype::FILTER_BY_KEY) {
					$yes = in_array(strval($key), $elementsToSearch, true);
				}
				else {
					$yes = in_array(strval($value), $elementsToSearch, true);
				}

				if ($yes) return true;
			}
		}

		return false;
	}

	protected function getDefaultParams() {
		return array(
			'source'        => 'values',
			'query'         => '',
			'values'        => array(),
			'sourceValues'  => array(),
			'style'         => 'select',
			'size'          => 1,
			'min'           => 1,
			'max'           => 10000,
			'addStyle'      => '',
			'addClass'      => '',
			'setAttr'       => array(),
			'groupAttr'     => array(),
			'data_in_value' => false
		);
	}

	protected function deserializeParams(array $params, $prefix = '') {
		$result = parent::deserializeParams($params, '');

		// fix inconsistent naming
		$result['dataInValue'] = !empty($result['data_in_value']);
		unset($result['data_in_value']);

		// retrieve source values
		switch ($result['source']) {
			case 'values':
				$result['sourceValues'] = sly_makeArray($result['values']);
				break;

			case 'query':
			case 'datatype':
				$result['sourceValues'] = $this->query($result['query']);
				break;

			case 'callback':
				if (is_callable($result['callback'])) {
					$params = isset($result['params']) ? sly_makeArray($result['params']) : array();
					$result['sourceValues'] = sly_makeArray(call_user_func_array($result['callback'], $params));
				}
				else {
					$result['sourceValues'] = array();
				}

				break;

			default:
				$result['sourceValues'] = array();
				break;
		}

		return parent::deserializeParams($result, $prefix);
	}

	/**
	 * Datenbank-Abfrage ausführen
	 *
	 * Diese Methode führt eine SQL-Anfrage aus. Vorher wird %%CLANG%% durch die
	 * aktuelle Sprache ersetzt sowie die entsprechende Datenbank ausgewählt.
	 *
	 * @param  string $query  die auszuführende Anfrage
	 * @param  int    $clang  die gewünschte Sprache
	 * @return array          eine Liste mit den geholten Datensätzen (max. ein Wert pro Key, selbst wenn mehr selektiert wurden)
	 */
	protected function query($query, $clang = null) {
		if (!$query) return array();

		static $data = null;

		$clang = WV_Sally::clang($clang);
		$query = str_replace('%%PREFIX%%', WV_SQL::getPrefix(), $query);
		$query = str_replace('%%CLANG%%', $clang, $query);
		$hash  = md5($query);

		if (!isset($data[$hash])) {
			$sql         = WV_SQL::getInstance();
			$data[$hash] = $sql->getArray($query);
		}

		return $data[$hash];
	}

	protected function decodeFormValue(WV_IProperty $property, $value) {
		$value       = parent::decodeFormValue($property, $value);
		$params      = $property->getParams();
		$dataInValue = !empty($params['data_in_value']);

		if ($property->isMultilingual()) {
			foreach ($value as $clang => $list) {
				if ($list === null) {
					$list = '';
				}

				if ($dataInValue) {
					$value[$clang] = $this->deserializeValue($list, $params);
				}
				else {
					$value[$clang] = array_keys($this->deserializeValue($list, $params));
				}
			}
		}
		else {
			if ($value === null) {
				$value = '';
			}

			if ($dataInValue) {
				$value = $this->deserializeValue($value, $params);
			}
			else {
				$value = array_keys($this->deserializeValue($value, $params));
			}
		}

		return $value;
	}

	public function renderForm(sly_Form $form, WV_IProperty $property, $value) {
		$value  = $this->decodeFormValue($property, $value);
		$params = $property->getParams();
		$propID = $property->getID();
		$title  = $property->getTitle();
		$id     = 'property_'.$propID.'_value';

		extract($this->deserializeParams($params));

		switch ($style) {
			case 'select':
			case 'selectbox':
				$select = new sly_Form_Select_DropDown($id, $title, $value, $sourceValues);
				$select->setSize($size);
				$select->setMultiple($max > 1 || $min > 1);
				break;

			case 'checkbox':
			case 'checkboxes':
				$select = new sly_Form_Select_Checkbox($id, $title, $value, $sourceValues);
				break;

			case 'radiobox':
			case 'radioboxes':
				$select = new sly_Form_Select_Radio($id, $title, $value, $sourceValues);
				break;

			default:
				throw new WV_Exception_Datatype(t('devutils_datatype_select_invalid_style'));
		}

		$select->setMultilingual($property->isMultilingual());

		WV_Datatype::setHelpTextOnElement($form, $select, $property);

		if ($addStyle) {
			if (is_array($addStyle)) {
				foreach ($addStyle as $addStyle_k => $addStyle_v) {
					$select->addStyle($addStyle_k . ':' . $addStyle_v);
				}
			}
			else {
				$select->addStyle($addStyle);
			}
		}
		if ($addClass) {
			$select->addClass(is_array($addClass) ? implode(' ', $addClass) : $addClass);
		}
		if ($setAttr) {
			foreach ($setAttr as $attr_name => $attr_value) {
				$select->setAttribute($attr_name, $attr_value);
			}
		}

		if ($groupAttr && is_array($groupAttr)) {
			$select->setGroupAttr($groupAttr);
		}

		$form->add($select);

		return $form;
	}

	public function serializeFormFilter($list, WV_IProperty $property) {
		$params = $property->getParams();
		extract($this->deserializeParams($params));

		$allowed = array_map('strval', array_keys($sourceValues));

		foreach ($list as $clangID => $values) {
			$values = sly_makeArray($values);
			$result = array();

			// Gültige Elemente auswählen

			foreach ($values as $element) {
				if (in_array(strval($element), $allowed, true)) $result[] = $element;
			}

			if (count($result) < $min && $min != 0) throw new WV_Exception_Datatype(t('devutils_datatype_select_too_few', $min));
			if (count($result) > $max && $max > 0)  throw new WV_Exception_Datatype(t('devutils_datatype_select_too_many', $max));

			$list[$clangID] = implode('|', $result);
		}

		return $list;
	}

	// Sonderanfertigung für varisale

	public function createStaticParams(array $params) {
		extract($this->deserializeParams($params));

		return array(
			'source'        => 'values',
			'values'        => $sourceValues,
			'style'         => $style,
			'size'          => $size,
			'min'           => $min,
			'max'           => $max,
			'data_in_value' => $dataInValue
		);
	}
}
