/* Copyright (c) 2011, Geert Bergman (geert@scrivo.nl)
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 * 3. Neither the name of "Scrivo" nor the names of its contributors may be
 *    used to endorse or promote products derived from this software without
 *    specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 *
 * $Id: PasteData.js 738 2013-07-12 18:38:39Z geert $
 */

"use strict";

SUI.control.PasteData = SUI.defineClass(
	/** @lends SUI.control.PasteData.prototype */{

	/**
	 * @private
	 *
	 * @class
	 * <p>The SUI.control.PasteData is a part of the HTML editor that deals
	 * with pasting data. We want to offer different ways of pasting text:
	 * plain text, filtered (just a limited set of tags and attributes) or
	 * with all markup.
	 * </p>
	 * <p>The latter case is obviously very simple: that is how normal pasting
	 * works, but is not what we prefer as standard actions. We want to remove
	 * all useless and interfering markup that editors as Word use.
	 * </p>
	 * <p>Since we're not allowed to access the paste buffer using JavaScript
	 * we fool the browser here. When pasting we direct the focus to an div
	 * element that we create for the purpose of intercepting the paste
	 * action. We use the 'onbeforepaste' (IE) and 'onpaste' (others) event
	 * handlers to do that.
	 * </p>
	 * <p>We also monitor the key strokes and if we discover that there is data
	 * to paste we finish the paste action by cleaning the pasted data using
	 * the requested method and inserting it into the content.
	 * </p>
	 *
	 * @description
	 * The SUI.control.PasteData implements the pasting logic for the HTML
	 * editor. It is a fundamental part of the HTML editor and therefore both
	 * are able to access each other's private data members.
	 *
	 * @constructs
	 * @param {SUI.control._HTMLEditControl} arg.editor A reference to an
	 *     instance of the SUI.control.HTMLEditControl.
	 */
	initializer: function(arg) {
		this._editor = arg.editor;
	},

	/**
	 * The listener method to use on the 'keyup' event handler of the HTML
	 * editor to finish an intercepted the paste actions. If it is detected
	 * that there is data to be pasted the data will we cleaned using the
	 * currently set paste method and inserted into the content.
	 */
	finishPaste: function() {

		// If we're currently intercepting then finish that
		if (this._intercepting) {
			// Make sure we're not executing this method twice
			if (!this._finishing) {
				this._finishing = true;

				/*if (SUI.browser.isGecko) {
					//this._finishPaste(); < generates exception FF ??
					this._div.blur();
				} else */ if (SUI.browser.isIE) {
					this._finishPasteIE();
				} else {
					this._finishPaste();
				}

				// free the locks
				this._finishing = this._intercepting = false;
			}
		}
	},

	/**
	 * The listener method to use on the 'onbeforepaste' (IE) and 'onpaste'
	 * (others) event handlers to intercept the paste actions. If the paste
	 * method was set to 'html' the method returns true so that pasting will
	 * continue as default. In the case of 'text' or 'filtered' the data
	 * will be pasted to an offscreen div to process it by finishPaste later.
	 */
	interceptPaste: function() {

		if (this._pasteMethod !== "html") {

			// make sure we're not executing while not have completed
			// _finishedPaste
			if (!this._intercepting) {
				this._intercepting = true;

				if (SUI.browser.isIE) {
					this._interceptPasteIE();
				} else {
					this._interceptPaste();
				}
			}

			// prevent event propagation
			return false;

		} else {

			// when used in scrivo event handling: true means propagate (i.e.
			// behave normally)
			return true;

		}

	},

	/**
	 * Get or set the paste method to use when pasting.
	 * @param {String} [p] The desired method for pasting text: 'text',
	 *    'filtered' or 'html' (or none to use this method as a getter).
	 * @return {String} The current paste method setting (or null if this
	 *    method was used as a setter).
	 */
	pasteMethod: function(p) {
		return p !== undefined
			? (this._pasteMethod = p) && null : this._pasteMethod;
	},

	/**
	 * The offscreen div to paste the data to.
	 * @type HTMLElementNode
	 * @private
	 */
	_div: null,

	/**
	 * A reference to the editor object.
	 * @type SUI.control._HTMLEditControl
	 * @private
	 */
	_editor: null,

	/**
	 * Flag to indicate that we're currently are busy with finishing a paste
	 * action.
	 * @type boolean
	 * @private
	 */
	_finishing: false,

	/**
	 * Flag to indicate that we're currently are busy with intercepting a paste
	 * action and that there is data ready to be inserted.
	 * @type boolean
	 * @private
	 */
	_intercepting: false,

	/**
	 * The paste method to use: 'text', 'filtered' or 'html'
	 * @type String
	 * @private
	 */
	_pasteMethod: "filtered",

	/**
	 * The stored range (or bookmark in IE) to be able to restore the cursor
	 * position after the editor focus was moved to the offscreen div.
	 * @type Object
	 * @private
	 */
	_saveRange: null,

	/**
	 * One of the scroll tops to save (and restore) if the document scroll
	 * offset changes when the helper div is created and removed.
	 * @type int
	 * @private
	 */
	_scrollTop1: 0,

	/**
	 * One of the scroll tops to save (and restore) if the document scroll
	 * offset changes when the helper div is created and removed.
	 * @type int
	 * @private
	 */
	_scrollTop2: 0,

	/**
	 * Set of allowed tags an allowed attributes for tag filtering.
	 * @type Object
	 * @private
	 */
	_tagFilter: {
		"P": [],
		"H1": [],
		"H2": [],
		"H3": [],
		"H4": [],
		"H5": [],
		"H6": [],
		"BR": [],
		"HR": [],
		"IMG": ["src", "align", "title", "alt"],
		"A": ["href", "title"],
		"B": [],
		"U": [],
		"I": [],
		"SPAN": ["lang"],
		"STRONG": [],
		"EM": [],
		"SMALL": [],
		"BIG": [],
		"CODE": [],
		"PRE": [],
		"TABLE": ["summary", "cellspacing"],
		"TD": ["id", "headers", "axis"],
		"TH": ["id", "headers", "axis"],
		"TR": [],
		"THEAD": [],
		"TBODY": [],
		"TFOOT": [],
		"CAPTION": [],
		"LI": [],
		"UL": [],
		"OL": [],
		"DD": [],
		"DT": [],
		"DL": []
	},

	/**
	 * Set of tag that should be terminated with a line break when converted
	 * to plain text.
	 * @type Object
	 * @private
	 */
	_blockTags: {
		"P": 1,
		"H1": 1,
		"H2": 1,
		"H3": 1,
		"H4": 1,
		"H5": 1,
		"H6": 1,
		"BR": 1,
		"HR": 1,
		"PRE": 1,
		"TABLE": 1,
		"TR": 1,
		"CAPTION": 1,
		"LI": 1,
		"DD": 1,
		"DT": 1
	},
	
	/**
	 * Create an offscreen div as target for the paste actions so that we can
	 * access and process the data.
	 * @private
	 */
	_createPasteDiv: function() {
		// create an offscreen contenteditable div
		this._div = SUI.browser.createElement();
		SUI.style.removeClass(this._div, "no-select");
		SUI.style.setRect(this._div, 10, -110, 100, 100);
		this._div.style.overflow = "hidden";
		// If you do something, always do it with style ;)
		this._div.style.backgroundColor = "#FFFFBB";
		this._div.contentEditable = true;
		//this._editor._editDoc.body.appendChild(this._div);
		this._editor._editWin.frames.parent.document.body.appendChild(this._div);
	},

	/**
	 * Filter all unwanted tags and attributes from an HTML text string.
	 * @private
	 */
	_filter: function(txt, method) {

		// Setup the filter DIV's ...
		var a = document.createElement("DIV");
		var b = document.createElement("DIV");
		a.innerHTML = txt;
		// ... and filter the tags.
		this._filterTags(a, b);
		
		// If "filtered" was selected, return the filtered result.
		if (method == "filtered") {
			return b.innerHTML;
		}
		
		// Else create plain text from the filtered HTML.
		var res = {res: ""};
		this._toText(b, res, false);
		var makePs = true;
		// Replace line end (with trailing line ends and spaces) ...
		var txt = res.res.replace(/[\r\n][\s\r\n]*/g, makePs ? "<P>" : "");
		// ... clean spaces around P tags ...
		txt = txt.replace(/\s*<P>\s*/g, makePs ? "<P>" : "");
		// ... remove double spaces.
		return txt.replace(/\s+/g, " ");
	},

	/**
	 * When filtering (actually transfering) CSS class names keep the
	 * scrivo system styles.
	 * @param {HTMLElementNode} nd The target node.
	 * @param {String} cls The class name to strip the non scrivo class
	 *     selectors from.
	 * @private
	 */
	_filterClass: function(nd, cls) {
		if (!cls) {
			return;
		}
		var c = cls.split(" ");
		for (var i=0; i<c.length; i++) {
			if (c[i]) {
				if (c[i].substr(0, 4) == "sys_"
						|| c[i].substr(0, 6) == "scrivo") {
					SUI.style.addClass(nd, c[i]);
				}
			}
		}
	},

	/**
	 * Copy an HTML DOM sub tree from a HTML element to an other empty
	 * element node. And while doing so forget all unwanted tags and
	 * attributes, thus in effect filter the tree.
	 * @param {HTMLElementNode} src An source node containing a an HTML
	 *     fragment to filter.
	 * @param {HTMLElementNode} dest An empty target node.
	 * @private
	 */
	_filterTags: function(src, dest) {
		
		var ignore = {"SCRIPT":true, "HEAD":true, "LINK":true, "STYLE":true,
			"META": true, "TITLE": true};

		// first check if src is not an element node ...
		if(src.nodeType != 1 /*Element*/) {
			// ... and if it is an text node ...
			if (src.nodeType == 3 /*Text*/) {
				// ... append the text to the destiny node.
				dest.appendChild(document.createTextNode(src.nodeValue));
			}
			// ... else we're done (only possibility are comment nodes here)
			return;
		}

		// ... node type is element node: get the tag name ...
		var tg = this._tagFilter[src.tagName];
		// ... and check if is allowed and copy it to dest ...
		if(tg) {

			/* TODO need to fix this, not very important, but a bug however
			if (src.tagName == 'TABLE') {
				replaceTableIds(edtWin.document, src);
			}
			*/

			if (src.tagName == 'A' && src.name != "")  {
				// anchor is a special case in IE ...
				if (SUI.browser.isIE && SUI.browser.version <= 8) {
					var nwnd =
						document.createElement("<A name='"+src.name+"'></A>");
				} else {
					var nwnd = document.createElement(src.tagName);
					nwnd.name = src.name;
				}
				SUI.style.addClass(nwnd, "sys_anchor");
			} else {
				// ... else just create the tag.
				var nwnd = document.createElement(src.tagName);
			}

			// now copy the attributes from the source to the destiny node
			for (var i=0; i<tg.length; i++) {
				var attr = src.getAttribute(tg[i]);
				if (attr && (attr != "" || tg[i] == "alt")) {
					nwnd.setAttribute(tg[i], attr);
				}
			}

			// now copy scrivo system styles to the destiny node
			this._filterClass(nwnd, src.className);

			// append the newly created node to the destiny tree
			if (nwnd.tagName == "IMG") {
				// TODO what was the need for this clause
				//if (nwnd.src.indexOf("baseUrl") != -1) {
				dest.appendChild(nwnd);
				dest = nwnd;
				//}
			} else {
				dest.appendChild(nwnd);
				dest = nwnd;
			}
		}

		// recurse into the tree for all child nodes
		for(var i=0; i<src.childNodes.length; i++) {
			if (ignore[src.childNodes[i].tagName]) {
				continue;
			}
			this._filterTags(src.childNodes[i], dest);
		}
	},

	/**
	 * Convert HTML that was filtered using the _filterTags method to plain
	 * text.
	 * @param {HTMLElementNode} src An source node containing a an HTML
	 *     fragment to filter.
	 * @param {object} dest An object containing a 'res' data member field.
	 * @param {bool} prev Value to indicate that the line break should be 
	 *   appended before the new text will be added.
	 * @private
	 */
	_toText: function(src, dest, prev) {
		
		if (prev) {
			dest.res += "\n";
		}		

		// first check if src is not an element node ...
		if(src.nodeType != 1 /*Element*/) {
			// ... and if it is an text node ...
			if (src.nodeType == 3 /*Text*/) {
				// ... append the text to the destiny node.
				dest.res += src.nodeValue.replace(/\r?\n/g, " ");
			}
			return;
		}

		// Recurse into the tree for all child nodes
		for(var i=0; i<src.childNodes.length; i++) {
			this._toText(src.childNodes[i], dest, 
				this._blockTags[src.childNodes[i].tagName]);
		}
	},

	/**
	 * Finish a pending pasting action: filter out unwanted tags from the
	 * pasted data, remove the helper div and insert the cleaned data in
	 * the correct location of the document in the editor.
	 * @private
	 */
	_finishPaste: function() {

		var html = this._filter(this._div.innerHTML, this._pasteMethod);

		// ... remove the helper div ...
//      this._div.contentEditable = false;
//      this._editor._editDoc.body.removeChild(this._div);
		this._editor._editWin.frames.parent.document.body.removeChild(this._div);
		this._div = null;

		if (SUI.browser.isGecko) {
			// set the editor back to contenteditable ...
			this._editor._editDoc.body.contentEditable = false;
			this._editor._editDoc.body.contentEditable = true;
		// ... and restore the selection
		var s = this._editor._getSelection();
		s.removeAllRanges();
		s.addRange(this._saveRange);
		this._saveRange = null;
		}

		// restore document scroll offsets
//		this._editor._editDoc.body.scrollTop = this._scrollTop1;
//		this._editor._editDoc.body.parentNode.scrollTop = this._scrollTop2;

		// Now we can try to insert the cleaned html
		try {
			this._editor._execCommand("insertHtml", html);
		} catch (e) {
			alert(e.message);
		}

		this._editor._editDoc.body.focus();
	},

	/**
	 * Finish a pending pasting action: filter out unwanted tags from the
	 * pasted data, remove the helper div and insert the cleaned data in
	 * the correct location of the document in the editor. This method is
	 * of course specific to IE.
	 * @private
	 */
   _finishPasteIE: function() {

		// ... get the data from the helper div using the selected
		// cleaning method ...
		var html = this._filter(this._div.innerHTML, this._pasteMethod);

		// ... remove the helper div ...
		//this._editor._editDoc.body.removeChild(this._div);
		this._editor._editWin.frames.parent.document.body.removeChild(this._div);
		this._div = null;

		// ... and restore the selection
		var rng = this._editor._ieRestoreBookmark(this._saveRange);
		this._saveRange = null;

		if (rng) {
			rng.pasteHTML(html);
		}

	},

	/**
	 * In order to manipulate the paste buffer we need to intercept the paste
	 * action. Therefore we create an offscreen contenteditable div that gets
	 * the focus over the editor's contenteditable. On a later moment we use
	 * 'finishPaste' to manipulate the pasted data and insert it into the
	 * editor.
	 * @private
	 */
	_interceptPaste: function() {

		// save document scroll offsets
//		this._scrollTop1 = this._editor._editDoc.body.scrollTop;
//		this._scrollTop2 = this._editor._editDoc.body.parentNode.scrollTop;

		// save the current selection
		this._saveRange = this._editor._getCurrentRange();
		this._saveRange =
			this._editor._editWin.getSelection().getRangeAt(0);

		// create an offscreen contenteditable div
		this._createPasteDiv();

		// this works on FF and chrome linux. On chrome window's I find that
		// not the div but the document body receives the paste event. So
		// for chrome you'll find an onpaste handler also.
		var that = this;
		SUI.browser.addEventListener(
			this._div, "paste",
			function () {
				setTimeout(
					function() {
						that.finishPaste();
					}
				);
			}
		);

		// ... and set the focus to the offscreen div, so that the data
		// will be pasted into this div
		this._div.focus();
		var range = this._editor._editWin.frames.parent.document.createRange();
		range.selectNode(this._div);

/*
		// We'll raise an blur event on the paste event. This way we schedule
		// the finishPaste excecution after the content was inserted into the
		// hidden div.
			var that = this;
			SUI.browser.addEventListener(this._div, "blur",
			function(e) {
				setTimeout(
					function() {
						///that.finishPaste();
		}
				);
			}
		);
*/
	},

	/**
	 * In order to manipulate the paste buffer we need to intercept the paste
	 * action. Therefore we create an offscreen contenteditable div that gets
	 * the focus over the editor's contenteditable. On a later moment we use
	 * 'finishPaste' to manipulate the pasted data and insert it into the
	 * editor. This method is of course specific to IE.
	 * @private
	 */
	_interceptPasteIE: function() {

		this._saveRange = this._editor._ieSaveBookmark();

		// create an offscreen contenteditable div
		this._createPasteDiv();
		var that = this;
		SUI.browser.addEventListener(this._div, "blur",
			function(e) {
				that.finishPaste();
			}
		);

		var that = this;
		SUI.browser.addEventListener(this._div, "paste",
			function(e) {
				setTimeout(
					function() {
						that._editor._editDoc.body.focus();
					}
				);
			}
		);


		// ... and set the focus to the offscreen div, so that the data
		// will be pasted into this div
		this._div.focus();
		//var r3 = this._editor._editDoc.body.createTextRange();
		var r3 = this._editor._editWin.frames.parent.document.body.createTextRange();
		r3.moveToElementText(this._div);
		r3.select();

	}

});