Ext.namespace('Zarafa.plugins.jodit');

/**
 * @class Zarafa.plugins.jodit
 * @extends String
 *
 * The copyright string holding the copyright notice.
 */
Zarafa.plugins.jodit.ABOUT = ""
+ "<h2>htmleditor-jodit plugin</h2>"
+ "<p>Copyright (C) 2020 Kopano and its licensors</p>"

+ "<p>This program is free software: you can redistribute it and/or modify "
+ "it under the terms of the GNU Affero General Public License as "
+ "published by the Free Software Foundation, either version 3 of the "
+ "License, or (at your option) any later version.</p>"

+ "<p>This program is distributed in the hope that it will be useful, "
+ "but WITHOUT ANY WARRANTY; without even the implied warranty of "
+ "MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the "
+ "GNU Affero General Public License for more details.</p>"

+ "<p>You should have received a copy of the GNU Affero General Public License "
+ "along with this program.  If not, see <a href=\"http://www.gnu.org/licenses/\" target=\"_blank\">http://www.gnu.org/licenses/</a>.</p>"

+ "<p>htmleditor-jodit plugin contains the following third-party components:</p>"

+ "<h2>Jodit - Licensed under the MIT License</h2>"

+ "Copyright (c) 2013-2020 http://xdsoft.net "
+ "<p>Permission is hereby granted, free of charge, to any person obtaining a copy "
+ "of this software and associated documentation files (the \"Software\"), to deal "
+ "in the Software without restriction, including without limitation the rights "
+ "to use, copy, modify, merge, publish, distribute, sublicense, and/or sell "
+ "copies of the Software, and to permit persons to whom the Software is "
+ "furnished to do so, subject to the following conditions:</p>"
 
+ "<p>The above copyright notice and this permission notice shall be included in "
+ "all copies or substantial portions of the Software.</p>"

+ "<p>THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR "
+ "IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, "
+ "FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE "
+ "AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER "
+ "LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, "
+ "OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN "
+ "THE SOFTWARE.</p>"
+ "<p>© 2020 GitHub, Inc.</p>"
;

Ext.namespace('Zarafa.plugins.jodit');

/**
 * @class Zarafa.plugins.jodit.JoditPlugin
 * @extends Zarafa.core.HtmlEditorPlugin
 *
 * Plugin that makes it possible to use an html editor plugin
 */
var editorPlugin = {};
if (Ext.isDefined(Zarafa.core.HtmlEditorPlugin)) {
	editorPlugin = Zarafa.core.HtmlEditorPlugin;
} else {
	// following code is for backword compatibility with older version of webapp.
	// in future we can remove this code
	if (Zarafa.plugins.htmleditor && Zarafa.plugins.htmleditor.Plugin) {
		editorPlugin = Zarafa.plugins.htmleditor.Plugin;
	}
	console.warn("Please upgrade Kopano WebApp to use the Jodit plugin");
}

Zarafa.plugins.jodit.JoditPlugin = Ext.extend(editorPlugin, {
	/**
	 * Editor plugins should overwrite this value with the xtype of their editor.
	 * @property
	 */
	editorXType: 'plugins.joditeditor',
});

// make the editorPlugin object to undefined
// after use of it.
editorPlugin = undefined;

// Don't load the plugin if browser is IE
if (!Ext.isIE) {
	Zarafa.onReady(function () {
		container.registerPlugin(new Zarafa.core.PluginMetaData({
			name: 'jodit',
			about: Zarafa.plugins.jodit.ABOUT,
			allowUserDisable: false,
			allowUserVisible: false,
			displayName: _('Jodit Editor'),
			pluginConstructor: Zarafa.plugins.jodit.JoditPlugin
		}));
	});
}
Ext.namespace('Zarafa.plugins.jodit.ui');

var joditIdGen = 0;

Zarafa.plugins.jodit.ui.JoditEditor = Ext.extend(Ext.Container, {
    /**
	 * The editor property which contains the instance of the Jodit editor.
	 * @property
	 * @type Object
	 */
	editor: undefined,

	/**
	 * It holds the last cursor position before switching the focus from editor.
	 *
	 * @property
	 * @type Array
	 */
	lastCursorPosition: undefined,

	/**
	 * @constructor
	 * @param {Object} config configuration object.
	 */
	constructor: function(config) {
		this.id = 'k-joditeditor-' + joditIdGen++;

		config = Ext.applyIf(config || {}, {
			xtype: 'plugins.joditeditor',
			cls: 'k-jodittextarea',
			enableSystemContextMenu: true
		});

		Zarafa.plugins.jodit.ui.JoditEditor.superclass.constructor.call(this, config);

		this.initEvents();
    },
    
    /**
	 * Function called after instance of {@link Ext.Container Container} has been created.
	 * @private
	 */
	initEvents: function() {
		this.on('activate', this.onActivateEditor, this);
		this.on('deactivate', this.onDeactivateEditor, this);
	},

	/**
	 * bind an {@link Zarafa.core.data.IPMRecord} to this component
	 * record is necessary to properly convert img tags, for which store and entryid are needed
	 * @param {Zarafa.core.data.IPMRecord} record
	 */
	bindRecord: function(record) {
		this.record = record;
	},

	/**
	 * Event handler which is fired when the {@link Ext.Container} has been rendered
	 * @private
	 */
	afterRender: function() {
		Zarafa.plugins.jodit.ui.JoditEditor.superclass.afterRender.call(this, arguments);

		var useHtml = container.getSettingsModel().get('zarafa/v1/contexts/mail/dialogs/mailcreate/use_html_editor');
		if (Ext.isDefined(this.dialog) && this.dialog.isXType('zarafa.mailcreatecontentpanel') === false) {
			return;
		} else if (useHtml === false) {
			return;
		}

		this.createEditorInstance();

		// If the instance of the editor is signature-editor then disable it.
		if (this.editor.container.classList.contains('k-signature-editor')) {
			this.disable();
		}
	},

	/**
	 * Event handler triggered when deactivate event is fired by the editor instance.
	 * Function is used to hide the editor.
	 */
	onDeactivateEditor: function() {
		var editorContainer = this.editor.container;
		var toolbarContainer = this.editor.toolbar.container;
		if (editorContainer.classList.contains('x-hide-display') === false) {
			editorContainer.setAttribute('class', editorContainer.getAttribute('class') + ' x-hide-display');
		}

		if (toolbarContainer.classList.contains('x-hide-display') === false) {
			toolbarContainer.setAttribute('class', toolbarContainer.getAttribute('class') + ' x-hide-display');
		}
	},

	/**
	 * Event handler triggered when activate event is fired by the editor instance.
	 * Function is used to show the editor else create new instance of Jodit editor.
	 */
	onActivateEditor: function() {
		if (this.editor) {
			var editorContainer = this.editor.container;
			var toolbarContainer = this.editor.toolbar.container;
			editorContainer.classList.remove('x-hide-display');
			toolbarContainer.classList.remove('x-hide-display');
			return;
		}

		this.createEditorInstance();
    },
    
    /**
	 * Create a new instance of Jodit editor.
	 */
	createEditorInstance: function() {
		var fontFamilies = this.getFontFamilies().map(function(font) {
			return font.name;
		});

		var formatting = {
			fontFamilies: fontFamilies,
			fontSize: Zarafa.common.ui.htmleditor.Fonts.getFontSizeString().split(' ')
		};
		
		var activeWindow = this.getActiveWindow();
		this.activeWindow = activeWindow;
		
		var editorContainer = this.id;
		if (Zarafa.core.BrowserWindowMgr.isMainWindowActive() === false) {
			editorContainer = activeWindow.document.getElementById(editorContainer);
		} else {
			editorContainer = '#' + editorContainer;
		}

		this.editor = new activeWindow.Jodit(editorContainer, {
			toolbarButtonSize: "medium",
			buttons: ['bold', 'italic', 'underline', 'ul', 'ol', 'image', 'table', 'link'],
			enableDragAndDropFileToEditor: true,
			tabIndex: 0,
			theme: "summer",
			style: {
				fontFamily : this.getDefaultFontFormatting().fontFamily,
				fontSize : this.getDefaultFontFormatting().fontSize
			},
			uploader: {
				// FIXME : Find a proper url for the connector
				url : 'https://xdsoft.net/jodit/connector/index.php?action=fileUpload',
				insertImageAsBase64URI: true
			},
			showTooltip: true,
			showPlaceholder: false,
			askBeforePasteHTML: false,
			askBeforePasteFromWord: false,
			defaultActionOnPaste: "insert_only_html",
			addNewLineOnDBLClick: false,
			link: {
				noFollowCheckbox : false,
				openInNewTabCheckbox : false
			},
			"disablePlugins": "cleanhtml"
		});

		this.editor.statusbar.hide();
		if (!this.editor.container.classList.contains('zarafa-contextmenu-enabled')) {
			this.editor.container.classList.add('zarafa-contextmenu-enabled');
		}

		this.editor.registerCommand('tabindent',{
			hotkeys:'TAB',
			exec: this.execTabIndent.bind(this)
		});

		this.editor.events.on('beforeEnter', function(event){
			if (event.ctrlKey && container.getSettingsModel().get('zarafa/v1/main/keycontrols') !== "disabled") {
				this.dialog.sendRecord();
				return false;
			}
			return;
		}.bind(this));

		this.editor.events.on('beforePasteInsert', function(html) {
			return " "+ html
		});

		// Add an event handler triggered when editor get the focus
		// and it will set the cursor to previous position.
		this.editor.editor.addEventListener('focus', function () {
			if (this.lastCursorPosition){
				this.editor.selection.restore(this.lastCursorPosition);
			}
		}.bind(this));

		this.editor.events.on('change', this.onTextChange.bind(this));
		this.editor.editor.addEventListener('focusout', this.onFocusOutHandler.bind(this));
	},

	/**
	 * Helper function used to find the closest elements.
	 * 
	 * @param {String} tags 
	 * @param {HTMLElement} el 
	 * @param {Object} editor 
	 * @returns 
	 */
	closest(tags, el, editor) {
		const condition = (node) => new RegExp(`^(${tags})$`, 'i').test(node.nodeName);
		let closest = el;
		do {
			if (condition(closest)) {
			return closest;
			}
			closest = closest.parentElement;
		} while (closest && closest !== editor.editor);
		return null;
	},

	/**
	 * Event handler used to add tag spacing.
	 */
	execTabIndent: function()
	{
		function addTabIndent (){
			const snapshot = editor.selection.save();
			var pEl = editor.createInside.element("p");
			pEl.insertHTML = "&nbsp;&nbsp;&nbsp;&nbsp;";
			editor.editor.appendChild(pEl);
			editor.selection.restore(snapshot);
			editor.selection.setCursorIn(pEl);
		}
		var editor = this.editor;
		var current = editor.selection.current(false);
		if (!current || !Ext.isDefined(current.closest)){
			if (editor.selection.sel.getRangeAt(0).startContainer === editor.editor) {
				addTabIndent();
			} else {
				this.insertAtCursor("&nbsp;&nbsp;&nbsp;&nbsp;");
			}
			return false;
		}

		var currentListItemElement = this.closest("li", current, editor);
		if (!currentListItemElement) {
			if (editor.selection.sel.getRangeAt(0).startContainer === editor.editor) {
				addTabIndent();
			} else {
				this.insertAtCursor("&nbsp;&nbsp;&nbsp;&nbsp;");
			}
			return false;
		}

		var currentListElement = this.closest("ul|ol", currentListItemElement, editor);
		if (!currentListElement ) {
			// No current list element
			return;
		}

		// Get previous list item to append item to sub list of that item.
		const previousListItemElement = currentListItemElement.previousElementSibling;
		if (!previousListItemElement) {
			// 'No previous sibling'
			return;
		}

		// Store snapshot to restore after having moved element
		const snapshot = editor.selection.save();
		// Check if previous list item already contains a list
		let childListElement = previousListItemElement.querySelector('ol,ul');
		// Create new list if previous item does not include any list
		childListElement = childListElement || editor.createInside.element(currentListElement.nodeName);
		childListElement.appendChild(currentListItemElement);
		previousListItemElement.appendChild(childListElement);
		editor.selection.restore(snapshot);
	},

	/**
	 * Event handler triggered when editor content has been changed by the user
	 * It will fire the 'keypress' event which used by the auto save functionality.
	 * 
	 * @param {String} newValue The new value entered in the editor.
	 * @param {String} oldValue The previous unchanged value.
	 */
	onTextChange : function(newValue, oldValue)
	{
		this.fireEvent('keypress', this, Ext.EventObject);
	},

	/**
	 * Event handler which is triggered when the level focus out of the editor container.
	 * It saves the last position of the cursor.
	 * 
	 * @param {Event} e The mouse event object
	 */
	onFocusOutHandler: function(e)
	{
		var focusEl = this.editor.s.sel.focusNode;
		if (focusEl.nodeName === '#text') {
			focusEl = focusEl.parentNode;
		}

		var marker = focusEl.querySelectorAll('span');
		if (marker.length === 0) {
			this.lastCursorPosition = this.editor.selection.save();
		}
	},
	
	/**
	 * Function will set value in the editor.
	 * 
	 * @param {String} value The value which was being set on the editor
	 */
	setValue : function(value)
	{
		value = this.removeInternalStyleTags(value);
		this.editor.setEditorValue(value);
		this.checkValueCorrection(this, value);
	},

	/**
	 * Remove any internal <style> tags from the value which is to be
	 * set in the editor.
	 * 
	 * @param {String} value The value which was being set on the editor
	 * @returns 
	 */
	removeInternalStyleTags : function(value)
	{
		if (value) {
			return value.replace(/<style>[\d\w\s"-{}!]*<\/style>/ig, "");
		}
	},

	/**
	 * This is called after {@link #setValue} and determines if the value has been adjusted by the jodit editor.
	 * Normally the value is applied to the jodit editor container element where it is being rendered, jodit editor will
	 * however also apply extra formatting, and some attributes. Because we only want to detect when the user made changes
	 * rather then the browser, we will check here what {@link #getValue} returns and check if that is different then
	 * the value which was being set. If a difference was found, the {@link #valuecorrection} event will be fired to inform the
	 * component user that he might need to implement some workaround.
	 *
	 * @param {Zarafa.common.ui.htmleditor.HtmlEditor} editor The HTML editor
	 * @param {String} value The value which was being set on the jodit editor.
	 * @private
	 */
	checkValueCorrection: function(editor, value) {
		var correctedValue = editor.getValue();

		if (value !== correctedValue) {
			editor.fireEvent('valuecorrection', editor, correctedValue, value);
		}
	},

	/**
	 * Function used to get the default formatting used by the user.
	 *
	 * @return {Object} Object The object which contains the default font size and type.
	 */
	getDefaultFontFormatting: function() {
		var fontType = container.getSettingsModel().get('zarafa/v1/main/default_font').split(',')[0];
		switch (fontType) {
			case 'courier new':
				fontType = 'Courier New';
				break;
			case 'times new roman':
				fontType = 'Times New Roman';
				break;
			case 'tahoma':
				fontType = 'Tahoma';
				break;
			case 'verdana':
				fontType = 'Verdana';
				break;
			default:
				fontType = 'Arial';
		}

		return {
			fontSize: Zarafa.common.ui.htmleditor.Fonts.getDefaultFontSize(),
			fontFamily: fontType
		};
	},

	/**
	 * Function used to list down the currently supported font families by the webapp.
	 *
	 * @return {Array} returns the font families which are supported.
	 */
	getFontFamilies: function() {
		var fontTypes = Zarafa.common.ui.htmleditor.Fonts.getFontFamilies();
		var fontFamilies = fontTypes.split(';').map(function(font) {
			return {
				name: font.split('=')[0],
				value: font.split('=')[1].toLowerCase()
			};
		});
		return fontFamilies;
	},

	/**
	 * Function returns the currently active window object.
	 * @return {Object} object Current active Window object
	 */
	getActiveWindow : function()
	{
		return Zarafa.core.BrowserWindowMgr.getActive();
	},

	/**
	 * Function will disable the editor.
	 */
	disable: function() {
		if (!this.editor) {
			this.createEditorInstance();
		}
		this.editor.setReadOnly(true);
	},

	/**
	 * Function will enable the editor.
	 */
	enable: function() {
		this.editor.setReadOnly(false);
	},

	/**
	 * Sets the focus on the editor.
	 */
	focus: function() {
		this.editor.selection.focus();
	},

	/**
	 * Returns the normalized data value (undefined or emptyText will be returned as '').
	 * To return the raw value see {@link #getRawValue}.
	 * @return {Mixed} value The field value
	 */
	getValue: function() {
		return this.editor.getEditorValue();
	},

	/**
	 * Function inserts HTML text into the editor field where cursor is positioned.
	 * @param {String} value The text which must be inserted at the cursor position
	 */
	insertAtCursor : function(value)
	{
		this.editor.selection.insertHTML(value);
	},

	/**
	 * Function sets the cursor position to the start of the text
	 */
	setCursorLocation: Ext.emptyFn,
});

Ext.reg('plugins.joditeditor', Zarafa.plugins.jodit.ui.JoditEditor);
