<?php

/**
 * THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED
 * WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
 *
 * @package fusionLib
 * @copyright Copyright (c) 2007 - 2016 fusionLib. All rights reserved.
 * @link http://fusionlib.com
 */

/**
 * _flFormGroup Class
 *
 * Internal class to group elements within a form.
 *
 * @package fusionLib
 */
class _flFormGroup {

	/**
	 * The name of the group.
	 *
	 * @var string
	 */
	private $name;

	/**
	 * The label for the group.
	 *
	 * @param string
	 */
	private $label = null;

	/**
	 * The elements in the group.
	 *
	 * @var array
	 */
	private $elements = array();

	/**
	 * Group decorators.
	 *
	 * @var flFormDecoratorList
	 */
	private $grpDecorators;

	/**
	 * Elements decorators.
	 *
	 * @var flFormDecoratorList
	 */
	private $elemDecorators;

	/**
	 * Construct the object.
	 *
	 * @param string $name The name of the group.
	 * @param flFormDecoratorList $grpDecorators Decorators for the group.
	 * @param flFormDecoratorList $elemDecorators Decorators for group elements.
	 */
	function __construct($name, $grpDecorators, $elemDecorators) {
		$this->name = $name;
		$this->grpDecorators = $grpDecorators;
		$this->elemDecorators = $elemDecorators;
	}

	/**
	 * Set the group decorator.
	 *
	 * @param flFormDecoratorList $grpDecorators Decorators for the group.
	 */
	function setGrpDecorators($grpDecorators) {
		$this->grpDecorators = $grpDecorators;
	}

	/**
	 * Get the name of the group.
	 *
	 * @return string The group name.
	 */
	function getName() {
		return $this->name;
	}

	/**
	 * Set the label for the group.
	 *
	 * @param string $label The label.
	 */
	function setLabel($label) {
		$this->label = $label;
	}

	/**
	 * Return the label for the group.
	 *
	 * @return string The group label.
	 */
	function getLabel() {
		return $this->label;
	}

	/**
	 * Add an element to the group.
	 *
	 * @param flFormElement $elem The element to add to the group.
	 */
	function addElement($elem) {
		$this->elements[$elem->getName()] = $elem;
	}

	/**
	 * Remove the named element from the group.
	 *
	 * @param string $name The name of the element to remove.
	 * @return flFormElement The removed element or null if not known.
	 */
	function removeElement($name) {
		if(!isset($this->elements[$name]))
			return null;
		$e = $this->elements[$name];
		unset($this->elements[$name]);
		return $e;
	}

	/**
	 * Render the form group.
	 *
	 * @return string The rendered group.
	 */
	function render() {
		$content = '';
		if(count($this->elements)) {

			// Get & decorate elements
			foreach($this->elements as $e) {
				if(!$e->isHidden())
					$content .= $this->elemDecorators->decorate($e, $e->render());
			}

			// Decorate the group
			$content = $this->grpDecorators->decorate($this, $content);
		}
		return $content;
	}
}

/**
 * flForm Class
 *
 * Class to define a form and collect the form elements together.
 *
 * @package fusionLib
 */
class flForm {

	/**
	 * The name of the form.
	 *
	 * @var string
	 */
	private $name;

	/**
	 * The form action.
	 *
	 * @var string
	 */
	private $action;

	/**
	 * Flags if form using POST
	 *
	 * @var bool
	 */
	private $post;

	/**
	 * The form elements.
	 *
	 * @var array
	 */
	private $elements = array();

	/**
	 * Grouped elements.
	 *
	 * @var array
	 */
	private $groups = array();

	/**
	 * Hidden form elements
	 *
	 * @var array
	 */
	private $hidden = array();

	/**
	 * Flags if form has file input elements.
	 *
	 * @var bool
	 */
	private $hasFiles = false;

	/**
	 * Decorators for the form.
	 *
	 * @var flFormDecoratorList
	 */
	private $decorators;

	/**
	 * Decorators for the form groups.
	 *
	 * @var flFormDecoratorList
	 */
	private $grpDecorators;

	/**
	 * Flags if using custom set group decorators.
	 *
	 * @var bool
	 */
	private $custGrpDecorators;

	/**
	 * Decorators for the form elements.
	 *
	 * @var flFormDecoratorList
	 */
	private $elemDecorators;

	/**
	 * The request object.
	 *
	 * @var flRequest
	 */
	private $request;

	/**
	 * Flags if the form has been submitted.
	 *
	 * @var bool
	 */
	private $submitted = null;

	/**
	 * Flags if the form and it's elements are valid.
	 *
	 * @var bool
	 */
	private $isValid = null;

	/**
	 * String to show for AJAX waiting.
	 *
	 * @var string
	 */
	private $ajaxWaitingText = null;

	/**
	 * String to show for failed AJAX.
	 *
	 * @var string
	 */
	private $ajaxFailedText = null;

	/**
	 * JavaScript to execute on form submit.
	 *
	 * @var string
	 */
	private $onSubmitJS = null;

	/**
	 * The element to focus.
	 *
	 * @var string
	 */
	private $focusElem = null;

	/**
	 * Flags if only validation errors shown
	 *
	 * @var bool
	 */
	private $showOnlyErrors = true;

	/**
	 * The text to output for required elements.
	 *
	 * @var string
	 */
	private $requiredText;

	/**
	 * Text to return for valid elements.
	 *
	 * @var string
	 */
	private $isValidText;

	/**
	 * The code buffer being used during rendering.
	 *
	 * @var flBufferCode
	 */
	private $codeBuffer = null;

	/**
	 * The CSS class for the form.
	 *
	 * @var string
	 */
	private $css = null;

	/**
	 * The tabs.
	 *
	 * @var array
	 */
	private $tabs = array();

	/**
	 * Construct and initialise the form.
	 *
	 * @param flRequest $request The request object.
	 * @param string $name The name of the form.
	 * @param string $action The action or URI to send the form submission to.
	 * @param bool $post true if the form should be submitted with POST or false for GET
	 * @param flFormDecoratorList $frmDecorators Optional decorators for the form.
	 * @param flFormDecoratorList $grpDecorators Optional default decorators for groups.
	 * @param flFormDecoratorList $elemDecorators Optional default decorators for group elements.
	 */
	function __construct($request, $name, $action, $post = true, $frmDecorators = null, $grpDecorators = null, $elemDecorators = null) {
		$this->request = $request;
		$this->name = $name;
		$this->action = $action;
		$this->post = $post;

		$this->decorators = $frmDecorators !== null ? $frmDecorators : new flFormDecoratorList(new flFormDecoratorForm());
		$this->grpDecorators = $grpDecorators !== null ? $grpDecorators : new flFormDecoratorList(new flFormDecoratorDl(), new flFormDecoratorFieldset());
		$this->elemDecorators = $elemDecorators !== null ? $elemDecorators : new flFormDecoratorList(new flFormDecoratorDtDd());
		$this->custGrpDecorators = $grpDecorators !== null;

		$this->groups[$name] = new _flFormGroup($name, clone $this->grpDecorators, clone $this->elemDecorators);

		$this->requiredText = __('(Required)', 'fusionLib');
		$this->isValidText = __('Valid', 'fusionLib');
		$this->ajaxWaitingText = __('Please wait...', 'fusionLib');
		$this->ajaxFailedText = __('Unexpected failure.', 'fusionLib');
	}

	/**
	 * Add a form element to the form.
	 *
	 * @param flFormElement $elem The form element to add.
	 * @return flForm This object.
	 */
	function addElement($elem) {
		if($elem instanceof flFormElementHidden)
			$this->hidden[$elem->getName()] = $elem;
		else {
			if($elem instanceof flFormElementFile)
				$this->hasFiles = true;
			$this->groups[$this->name]->addElement($elem);
		}
		$this->elements[$elem->getName()] = $elem;
		return $this;
	}

	/**
	 * Get the nameed element.
	 *
	 * @param string $name The name of the element to get.
	 * @return flFormElement The element.
	 */
	function getElement($name) {
		return isset($this->elements[$name]) ? $this->elements[$name] : null;
	}

	/**
	 * Group elements, the elements must already be added.
	 *
	 * @param string $name The name of the group.
	 * @param array $elements The list of elements to put in the group.
	 * @param flFormDecoratorList $grpDecorators Optional decorators for the group.
	 * @param flFormDecoratorList $elemDecorators Optional decorators for the group elements.
	 * @return flForm This object.
	 * @throws flFormException
	 */
	function groupElements($name, $elements, $grpDecorators = null, $elemDecorators = null) {
		if(isset($this->groups[$name]))
			throw new flFormException("Duplicate element group of '$name' in form '" . $this->name . "'.");

		// Create the group
		$this->groups[$name] = new _flFormGroup($name, $grpDecorators !== null ? $grpDecorators : clone $this->grpDecorators, $elemDecorators !== null ? $elemDecorators : clone $this->elemDecorators);

		// Add elements to the group unless they don't exist or already grouped.
		foreach($elements as $el) {
			$e = $el->getName();
			if(!isset($this->hidden[$e])) {
				if(!isset($this->elements[$e]))
					throw new flFormException("Unable to group unknown element '$e' in form '" . $this->name . "'.");
				$this->groups[$this->name]->removeElement($e);
				$this->groups[$name]->addElement($this->elements[$e]);
			}
		}

		return $this;
	}

	/**
	 * Test if the form has a group of the given name.
	 *
	 * @param string $name The name of the group to add.
	 * @return bool true if the group is present; else false
	 */
	function hasGroup($name) {
		return isset($this->groups[$name]);
	}

	/**
	 * Add element to an existing group.
	 *
	 * @param string $name The name of the group.
	 * @param flFormElement $element The element to put in the group.
	 * @return flForm This object.
	 * @throws flFormException
	 */
	function addToGroup($element, $name) {
		if(!isset($this->groups[$name]))
			throw new flFormException("Unknown element group of '$name' in form '" . $this->name . "'.");

		$e = $element->getName();
		if(!isset($this->hidden[$e])) {
			if(!isset($this->elements[$e]))
				throw new flFormException("Unable to group unknown element '$e' in form '" . $this->name . "'.");
			$this->groups[$this->name]->removeElement($e);
			$this->groups[$name]->addElement($this->elements[$e]);
		}

		return $this;
	}

	/**
	 * Add a tab containing the specified groups.
	 *
	 * @param string $label The label for the tab.
	 * @param array $groups Array of groups names to add to the tab, or single group name.
	 * @return flForm This object.
	 */
	function addTab($label, $groups) {
		if(!is_array($groups)) $groups = array($groups);
		$g = array();
		foreach($groups as $name) {
			$g[] = $this->groups[$name];
			unset($this->groups[$name]);
		}

		$this->tabs[] = array(
			'label' => $label,
			'groups' => $g
		);

		return $this;
	}

	/**
	 * Set the label for a group.
	 *
	 * @param string $name The name of the group to set the label for or null for the default.
	 * @param string $label The HTML formatted label for the group.
	 * @return flFrom This object
	 */
	function setGroupLabel($name, $label) {
		if($name === null)
			$name = $this->name;
		if(isset($this->groups[$name]))
			$this->groups[$name]->setLabel($label);

		if($name == $this->name) $this->custGrpDecorators = true;

		return $this;
	}

	/**
	 * Get if the form has been submitted.
	 *
	 * If an AJAX validation request is detected then the response is sent back and
	 * the function never returns.
	 *
	 * @return bool true if the form has been submitted else false.
	 */
	function isSubmitted() {
		// Only test if 1st run
		if($this->submitted === null) {
			$this->submitted = (($this->post && $this->request->isPost()) || (!$this->post && $this->request->isGet()));

			if($this->request->isXmlHttpRequest()) {
				if($this->submitted = $this->submitted && $this->name == $this->request->getParam('form') && $this->request->hasParam('elem') && $this->request->hasParam('value')) {
					$this->request->setParam($elem = $this->request->getParam('elem'), $this->request->getParam('value'));
					if(isset($this->elements[$elem])) {
						$this->elements[$elem]->processRequest($this->request);
						$result = array(
							'valid' => $this->isValid = $this->elements[$elem]->isValid(),
							'msg' => $this->elements[$elem]->getError()
						);

						// Send AJAX response
						$response = new flResponse($this->request);
						$response->setContentType('text/plain')
							->noCache()
							->setBody(json_encode($result))
							->sendResponse();

						// Stop processing the script
						die;
					}
					else
						$this->isValid = false;
				}
			}
			else {
				if($this->submitted = $this->submitted && $this->request->getParam($this->name, '') == md5(serialize(array_keys($this->elements)) . __FILE__)) {
					// Submitted so make elements read data
					foreach($this->elements as $e)
						$e->processRequest($this->request);
				}
			}
		}
		return $this->submitted;
	}

	/**
	 * Test if the form and its elements are valid.
	 *
	 * @return bool true if the form and its elements are valid; else false.
	 * true is also returned if isSubmitted() returns false.
	 */
	function isValid() {
		if($this->isValid === null) {
			$this->isValid = true;

			// Validate if form submitted
			if($this->isSubmitted()) {
				foreach($this->elements as $e) {
					if(!$e->isValid())
						$this->isValid = false;
				}
			}
		}
		return $this->isValid;
	}

	/**
	 * Test if the form has been invalidated by one of the elements changing state from
	 * valid to invalid.
	 *
	 * @return bool true if the form has been invalidated; else false.
	 */
	function isInvalidated() {
		if($this->isValid === null || !$this->isSubmitted())
			return false;

		$valid = true;
		foreach($this->elements as $e)
			$valid = $valid && $e->isValid();

		return $this->isValid != $valid;
	}

	/**
	 * Set the text to use for AJAX waiting messages.
	 *
	 * @param string $text The text to show.
	 */
	function setAJAXWaitText($text) {
		$this->ajaxWaitingText = str_replace("'", "\'", $text);
	}

	/**
	 * 	Set the text to use for the AJAX failed message.
	 *
	 * @param string $text The text to show.
	 */
	function setAJAXFailText($text) {
		$this->ajaxFailedText = str_replace("'", "\'", $text);
	}

	/**
	 * Set the JavaScript to run on form submit, if the function returns false then
	 * the form is not submitted.
	 *
	 * @param string $js The JavaScript to run on form submit.
	 */
	function setOnSubmitJS($js) {
		$this->onSubmitJS = $js;
	}

	/**
	 * The element to set focus to.
	 *
	 * @param flFormElement $elem The element to focus by default.
	 */
	function focusElement($elem) {
		$this->focusElem = $elem->getName();
	}

	/**
	 * Get the form tag attributes, used by decorators to open the form.
	 *
	 * @return string The form open tag.
	 */
	function getFormTagAttr() {
		return "id=\"$this->name\" action=\"$this->action\" method=\"" . ($this->post || $this->hasFiles ? 'post' : 'get') ."\" enctype=\"" . ($this->hasFiles ? 'multipart/form-data' : 'application/x-www-form-urlencoded') . "\" accept-charset=\"utf-8\"" . ($this->css !== null ? " class=\"$this->css\"" : '');
	}

	/**
	 * Set the CSS class for the form.
	 *
	 * @param string $class The class name of null for no class.
	 */
	function setCSS($class) {
		$this->css = $class;
	}

	/**
	 * Flags if to only show validation errors rather than also showing a message for valid fields.
	 *
	 * @param bool $showOnlyErrors true, the default, shows only errors or false to show errors and valid fields.
	 */
	function showOnlyErrors($showOnlyErrors = true) {
		$this->showOnlyErrors = $showOnlyErrors;
	}

	/**
	 * Get if showing only errors on validation.
	 *
	 * @return bool true if showing only errors; else false.
	 */
	function getShowOnlyErrors() {
		return $this->showOnlyErrors;
	}

	/**
	 * Get the forms request object.
	 *
	 * @return flRequest The request object.
	 */
	function getRequest() {
		return $this->request;
	}

	/**
	 * Set the decoration to apply to required elements, default is '(Required)'.
	 *
	 * @param string $required The decoration text.
	 */
	function setRequiredText($required) {
		$this->requiredText = $required;
	}

	/**
	 * Get the decoration to apply to required elements.
	 *
	 * @return string The required text.
	 */
	function getRequiredText() {
		return $this->requiredText;
	}

	/**
	 * Set the text to return with getError() for valid elements.
	 *
	 * @param string $text The text to set.
	 */
	function setValidText($text) {
		$this->isValidText = $text;
	}

	/**
	 * Get the text for valid elements.
	 *
	 * @return string The valid string.
	 */
	function getValidText() {
		return $this->isValidText;
	}

	/**
	 * Get the code buffer being used during rendering.
	 *
	 * @return flBufferCode The code buffer if any.
	 */
	function getCodeBuffer() {
		return $this->codeBuffer;
	}

	/**
	 * Render the form.
	 *
	 * If an AJAX validation request is detected then it is replied to
	 * and the function never returns.
	 *
	 * @see flForm::isSubmitted
	 *
	 * @param flBufferCode $codeBuffer The optional code buffer helper if not given then JavaScript is put inline.
	 * @return string The HTML for the form.
	 */
	function render($codeBuffer = null) {
		$this->codeBuffer = $codeBuffer;

		// If only 1 group and decorators not explicitly set then style without fieldset
		if(!$this->custGrpDecorators && count($this->groups) == 1) {
			$this->groups[$this->name]->setGrpDecorators(new flFormDecoratorList(new flFormDecoratorDl()));
		}

		// Trigger elements to collect their data and run validation
		$this->isSubmitted();

		// Form value
		$content = "<input type=\"hidden\" name=\"$this->name\" value=\"" . md5(serialize(array_keys($this->elements)) . __FILE__) . "\" />\n";

		// Hidden elements
		foreach($this->hidden as $name => $e)
			$content .= $e->render();

		// Draw tabs
		if(count($this->tabs)) {
			$tabs = new flWidgetTabs($this->name . '-tabs');
			foreach($this->tabs as $d) {
				$panel = '';
				foreach($d['groups'] as $g)
					$panel .= $g->render();
				$tabs->addTab($d['label'], $panel);
			}
			$content .= $tabs->render($this->getCodeBuffer());
		}

		// Draw elements by group
		foreach($this->groups as $groupName => $group)
			$content .= $group->render();

		// Decorate
		$content = $this->decorators->decorate($this, $content);

		// For all elements get javascript and validation rules
		$js = '';
		$rules = array();
		$validText = array();
		foreach($this->elements as $e) {
			$js .= $e->getJavaScript();

			$r = $e->getClientRules();
			if(!empty($r))
				$rules[] = $r;

			if(($r = $e->getValidText()) !== null)
				$validText[] = $r;
		}

		if(count($rules)) {
			$js .= "\n\$fl('#$this->name').attachValidation({\n";
 			$js .= "validText: '" . str_replace("'", "\'", $this->isValidText) . "',\n";

			if($this->ajaxWaitingText !== null) $js .= "ajaxWaitingText: '" . $this->ajaxWaitingText . "',\n";
			if($this->ajaxFailedText !== null) $js .= "ajaxFailedText: '" . $this->ajaxFailedText . "',\n";
			if($this->onSubmitJS !== null) $js .= "onSubmit: $this->onSubmitJS,\n";
			if(!$this->showOnlyErrors) $js .= "showOnlyErrors: false,\n";

			$js .= "rules: {\n" . join(",\n", $rules) . "\n}";

			if(count($validText))
				$js .= ",\nelemValidText: {\n" . join(",\n", $validText) . "\n}";

			$js .= "\n});\n\n";
		}

		// Focus element if set
		if($this->focusElem !== null)
			$js .= "\$fl('#{$this->focusElem}').focus();\n\n";

		if(!empty($js)) {
			if($codeBuffer === null)
				$content .= "<script type=\"text/javascript\">\n//<![CDATA[\n\n\$fl(document).ready(function() {\n$js});\n//]]>\n</script>\n";
			else
				$codeBuffer->requireValidator()->bufferReadyJS($js);
		}

		return $content;
	}

	/**
	 * Get the form name.
	 *
	 * @return string The name of the form.
	 */
	function getName() {
		return $this->name;
	}
}
