/*\
title: $:/core/modules/editor/factory.js
type: application/javascript
module-type: library

Factory for constructing text editor widgets with specified engines for the toolbar and non-toolbar cases

\*/
(function(){

/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";

var DEFAULT_MIN_TEXT_AREA_HEIGHT = "100px"; // Minimum height of textareas in pixels

// Configuration tiddlers
var HEIGHT_MODE_TITLE = "$:/config/TextEditor/EditorHeight/Mode";
var ENABLE_TOOLBAR_TITLE = "$:/config/TextEditor/EnableToolbar";

var Widget = require("$:/core/modules/widgets/widget.js").widget;

function editTextWidgetFactory(toolbarEngine,nonToolbarEngine) {

	var EditTextWidget = function(parseTreeNode,options) {
		// Initialise the editor operations if they've not been done already
		if(!this.editorOperations) {
			EditTextWidget.prototype.editorOperations = {};
			$tw.modules.applyMethods("texteditoroperation",this.editorOperations);
		}
		this.initialise(parseTreeNode,options);
	};

	/*
	Inherit from the base widget class
	*/
	EditTextWidget.prototype = new Widget();

	/*
	Render this widget into the DOM
	*/
	EditTextWidget.prototype.render = function(parent,nextSibling) {
		// Save the parent dom node
		this.parentDomNode = parent;
		// Compute our attributes
		this.computeAttributes();
		// Execute our logic
		this.execute();
		// Create the wrapper for the toolbar and render its content
		if(this.editShowToolbar) {
			this.toolbarNode = this.document.createElement("div");
			this.toolbarNode.className = "tc-editor-toolbar";
			parent.insertBefore(this.toolbarNode,nextSibling);
			this.renderChildren(this.toolbarNode,null);
			this.domNodes.push(this.toolbarNode);
		}
		// Create our element
		var editInfo = this.getEditInfo(),
			Engine = this.editShowToolbar ? toolbarEngine : nonToolbarEngine;
		this.engine = new Engine({
				widget: this,
				value: editInfo.value,
				type: editInfo.type,
				parentNode: parent,
				nextSibling: nextSibling
			});
		// Call the postRender hook
		if(this.postRender) {
			this.postRender();
		}
		// Fix height
		this.engine.fixHeight();
		// Focus if required
		if(this.editFocus === "true" || this.editFocus === "yes") {
			this.engine.focus();
		}
		// Add widget message listeners
		this.addEventListeners([
			{type: "tm-edit-text-operation", handler: "handleEditTextOperationMessage"}
		]);
	};

	/*
	Get the tiddler being edited and current value
	*/
	EditTextWidget.prototype.getEditInfo = function() {
		// Get the edit value
		var self = this,
			value,
			type = "text/plain",
			update;
		if(this.editIndex) {
			value = this.wiki.extractTiddlerDataItem(this.editTitle,this.editIndex,this.editDefault);
			update = function(value) {
				var data = self.wiki.getTiddlerData(self.editTitle,{});
				if(data[self.editIndex] !== value) {
					data[self.editIndex] = value;
					self.wiki.setTiddlerData(self.editTitle,data);
				}
			};
		} else {
			// Get the current tiddler and the field name
			var tiddler = this.wiki.getTiddler(this.editTitle);
			if(tiddler) {
				// If we've got a tiddler, the value to display is the field string value
				if(tiddler.hasField(this.editField)) {
					value = tiddler.getFieldString(this.editField);
				} else {
					value = this.editDefault || "";
				}
				if(this.editField === "text") {
					type = tiddler.fields.type || "text/vnd.tiddlywiki";
				}
			} else {
				// Otherwise, we need to construct a default value for the editor
				switch(this.editField) {
					case "text":
						value = "Type the text for the tiddler '" + this.editTitle + "'";
						type = "text/vnd.tiddlywiki";
						break;
					case "title":
						value = this.editTitle;
						break;
					default:
						value = "";
						break;
				}
				if(this.editDefault !== undefined) {
					value = this.editDefault;
				}
			}
			update = function(value) {
				var tiddler = self.wiki.getTiddler(self.editTitle),
					updateFields = {
						title: self.editTitle
					};
				updateFields[self.editField] = value;
				self.wiki.addTiddler(new $tw.Tiddler(self.wiki.getCreationFields(),tiddler,updateFields,self.wiki.getModificationFields()));
			};
		}
		if(this.editType) {
			type = this.editType;
		}
		return {value: value || "", type: type, update: update};
	};

	/*
	Handle an edit text operation message from the toolbar
	*/
	EditTextWidget.prototype.handleEditTextOperationMessage = function(event) {
		// Prepare information about the operation
		var operation = this.engine.createTextOperation();
		// Invoke the handler for the selected operation
		var handler = this.editorOperations[event.param];
		if(handler) {
			handler.call(this,event,operation);
		}
		// Execute the operation via the engine
		var newText = this.engine.executeTextOperation(operation);
		// Fix the tiddler height and save changes
		this.engine.fixHeight();
		this.saveChanges(newText);
	};

	/*
	Compute the internal state of the widget
	*/
	EditTextWidget.prototype.execute = function() {
		// Get our parameters
		this.editTitle = this.getAttribute("tiddler",this.getVariable("currentTiddler"));
		this.editField = this.getAttribute("field","text");
		this.editIndex = this.getAttribute("index");
		this.editDefault = this.getAttribute("default");
		this.editClass = this.getAttribute("class");
		this.editPlaceholder = this.getAttribute("placeholder");
		this.editSize = this.getAttribute("size");
		this.editRows = this.getAttribute("rows");
		this.editAutoHeight = this.wiki.getTiddlerText(HEIGHT_MODE_TITLE,"auto");
		this.editAutoHeight = this.getAttribute("autoHeight",this.editAutoHeight === "auto" ? "yes" : "no") === "yes";
		this.editMinHeight = this.getAttribute("minHeight",DEFAULT_MIN_TEXT_AREA_HEIGHT);
		this.editFocusPopup = this.getAttribute("focusPopup");
		this.editFocus = this.getAttribute("focus");
		this.editTabIndex = this.getAttribute("tabindex");
		this.editCancelPopups = this.getAttribute("cancelPopups","") === "yes";
		this.editInputActions = this.getAttribute("inputActions");
		this.editRefreshTitle = this.getAttribute("refreshTitle");
		this.editAutoComplete = this.getAttribute("autocomplete");
		this.isDisabled = this.getAttribute("disabled","no");
		this.isFileDropEnabled = this.getAttribute("fileDrop","no") === "yes";
		// Get the default editor element tag and type
		var tag,type;
		if(this.editField === "text") {
			tag = "textarea";
		} else {
			tag = "input";
			var fieldModule = $tw.Tiddler.fieldModules[this.editField];
			if(fieldModule && fieldModule.editTag) {
				tag = fieldModule.editTag;
			}
			if(fieldModule && fieldModule.editType) {
				type = fieldModule.editType;
			}
			type = type || "text";
		}
		// Get the rest of our parameters
		this.editTag = this.getAttribute("tag",tag) || "input";
		this.editType = this.getAttribute("type",type);
		// Make the child widgets
		this.makeChildWidgets();
		// Determine whether to show the toolbar
		this.editShowToolbar = this.wiki.getTiddlerText(ENABLE_TOOLBAR_TITLE,"yes");
		this.editShowToolbar = (this.editShowToolbar === "yes") && !!(this.children && this.children.length > 0) && (!this.document.isTiddlyWikiFakeDom);
	};

	/*
	Selectively refreshes the widget if needed. Returns true if the widget or any of its children needed re-rendering
	*/
	EditTextWidget.prototype.refresh = function(changedTiddlers) {
		var changedAttributes = this.computeAttributes();
		// Completely rerender if any of our attributes have changed
		if(changedAttributes.tiddler || changedAttributes.field || changedAttributes.index || changedAttributes["default"] || changedAttributes["class"] || changedAttributes.placeholder || changedAttributes.size || changedAttributes.autoHeight || changedAttributes.minHeight || changedAttributes.focusPopup ||  changedAttributes.rows || changedAttributes.tabindex || changedAttributes.cancelPopups || changedAttributes.inputActions || changedAttributes.refreshTitle || changedAttributes.autocomplete || changedTiddlers[HEIGHT_MODE_TITLE] || changedTiddlers[ENABLE_TOOLBAR_TITLE] || changedAttributes.disabled || changedAttributes.fileDrop) {
			this.refreshSelf();
			return true;
		} else if (changedTiddlers[this.editRefreshTitle]) {
			this.engine.updateDomNodeText(this.getEditInfo().value);
		} else if(changedTiddlers[this.editTitle]) {
			var editInfo = this.getEditInfo();
			this.updateEditor(editInfo.value,editInfo.type);
		}
		this.engine.fixHeight();
		if(this.editShowToolbar) {
			return this.refreshChildren(changedTiddlers);
		} else {
			return false;
		}
	};

	/*
	Update the editor with new text. This method is separate from updateEditorDomNode()
	so that subclasses can override updateEditor() and still use updateEditorDomNode()
	*/
	EditTextWidget.prototype.updateEditor = function(text,type) {
		this.updateEditorDomNode(text,type);
	};

	/*
	Update the editor dom node with new text
	*/
	EditTextWidget.prototype.updateEditorDomNode = function(text,type) {
		this.engine.setText(text,type);
	};

	/*
	Save changes back to the tiddler store
	*/
	EditTextWidget.prototype.saveChanges = function(text) {
		var editInfo = this.getEditInfo();
		if(text !== editInfo.value) {
			editInfo.update(text);
		}
	};

	/*
	Handle a dom "keydown" event, which we'll bubble up to our container for the keyboard widgets benefit
	*/
	EditTextWidget.prototype.handleKeydownEvent = function(event) {
		// Check for a keyboard shortcut
		if(this.toolbarNode) {
			var shortcutElements = this.toolbarNode.querySelectorAll("[data-tw-keyboard-shortcut]");
			for(var index=0; index<shortcutElements.length; index++) {
				var el = shortcutElements[index],
					shortcutData = el.getAttribute("data-tw-keyboard-shortcut"),
					keyInfoArray = $tw.keyboardManager.parseKeyDescriptors(shortcutData,{
						wiki: this.wiki
					});
				if($tw.keyboardManager.checkKeyDescriptors(event,keyInfoArray)) {
					var clickEvent = this.document.createEvent("Events");
				    clickEvent.initEvent("click",true,false);
				    el.dispatchEvent(clickEvent);
					event.preventDefault();
					event.stopPropagation();
					return true;
				}
			}
		}
		// Propogate the event to the container
		if(this.propogateKeydownEvent(event)) {
			// Ignore the keydown if it was already handled
			event.preventDefault();
			event.stopPropagation();
			return true;
		}
		// Otherwise, process the keydown normally
		return false;
	};

	/*
	Propogate keydown events to our container for the keyboard widgets benefit
	*/
	EditTextWidget.prototype.propogateKeydownEvent = function(event) {
		var newEvent = this.cloneEvent(event,["keyCode","which","metaKey","ctrlKey","altKey","shiftKey"]);
		return !this.parentDomNode.dispatchEvent(newEvent);
	};

	EditTextWidget.prototype.cloneEvent = function(event,propertiesToCopy) {
		var propertiesToCopy = propertiesToCopy || [],
			newEvent = this.document.createEventObject ? this.document.createEventObject() : this.document.createEvent("Events");
		if(newEvent.initEvent) {
			newEvent.initEvent(event.type, true, true);
		}
		$tw.utils.each(propertiesToCopy,function(prop){
			newEvent[prop] = event[prop];
		});
		return newEvent;
	};

	EditTextWidget.prototype.dispatchDOMEvent = function(newEvent) {
		var dispatchNode = this.engine.iframeNode || this.engine.parentNode;
		return dispatchNode.dispatchEvent(newEvent);
	};

	/*
	Propogate drag and drop events with File data to our container for the dropzone widgets benefit.
	If there are no Files, let the browser handle it.
	*/
	EditTextWidget.prototype.handleDropEvent = function(event) {
		if(event.dataTransfer.files.length) {
			event.preventDefault();
			event.stopPropagation();
			this.dispatchDOMEvent(this.cloneEvent(event,["dataTransfer"]));
		} 
	};

	EditTextWidget.prototype.handlePasteEvent = function(event) {
		if(event.clipboardData && event.clipboardData.files && event.clipboardData.files.length) {
			event.preventDefault();
			event.stopPropagation();
			this.dispatchDOMEvent(this.cloneEvent(event,["clipboardData"]));
		}
	};

	EditTextWidget.prototype.handleDragEnterEvent = function(event) {
		if($tw.utils.dragEventContainsFiles(event)) {
			// Ignore excessive events fired by FF when entering and leaving text nodes in a text area.
			if( event.relatedTarget && (event.relatedTarget.nodeType === 3 || event.target === event.relatedTarget)) {
				return true;
			}
			event.preventDefault();
			return this.dispatchDOMEvent(this.cloneEvent(event,["dataTransfer"]));
		}
		return true;
	};

	EditTextWidget.prototype.handleDragOverEvent = function(event) {
		if($tw.utils.dragEventContainsFiles(event)) {
			// Call preventDefault() in browsers that default to not allowing drop events on textarea
			if($tw.browser.isFirefox || $tw.browser.isIE) {
				event.preventDefault();
			}
			event.dataTransfer.dropEffect = "copy";
			return this.dispatchDOMEvent(this.cloneEvent(event,["dataTransfer"]));
		}
		return true;
	};

	EditTextWidget.prototype.handleDragLeaveEvent = function(event) {
		// Ignore excessive events fired by FF when entering and leaving text nodes in a text area.
		if(event.relatedTarget && ((event.relatedTarget.nodeType === 3) || (event.target === event.relatedTarget))) {
			return true;
		}
		event.preventDefault();
		this.dispatchDOMEvent(this.cloneEvent(event,["dataTransfer"]));
	};

	EditTextWidget.prototype.handleDragEndEvent = function(event) {
		this.dispatchDOMEvent(this.cloneEvent(event));
	};

	EditTextWidget.prototype.handleClickEvent = function(event) {
		return !this.dispatchDOMEvent(this.cloneEvent(event));
	};

	return EditTextWidget;
}

exports.editTextWidgetFactory = editTextWidgetFactory;

})();
