/*
Copyright 2015 Adobe Systems Incorporated.  All rights reserved. 
*/

/*jslint vars: true, plusplus: true, devel: true, browser: true, nomen: true, maxerr: 50 */
/*global window, DW_LIVEEDIT_CONSTANTS, DW_EXTENSION_EVENT, DW_INSPECT_CONST, Node, dwExtensionController,
liveViewExtensionsObject, dwNotifySelectionChange, dwNotifySourceChange, MutationObserver, dwReloadStylesCompleted,
dwSelectRun, DW_VISUAL_DOM_POSITIONS*/

function CEFDWSelectionSyncUtil() {
    'use strict';
    
    // Log message
	this.Log = function (message) {
		if (window.writeDWLog) {
			window.writeDWLog(message);
		} else {
			// Can't write log, replace Log with dummy function
			this.Log = function (message) { };
		}
	};

	/* retuns the HTML string corresponding to current selection */
    this.getSelectionHtml =  function () {
		var html = "",
			sel = window.getSelection();

		if (sel && sel.rangeCount) {
			var i, len = sel.rangeCount,
				container = document.createElement("div");
			for (i = 0; i < len; ++i) {
				container.appendChild(sel.getRangeAt(i).cloneContents());
			}
			html = container.innerHTML;
		}
		return html;
	};

	/*Entities may occupy different length in DW. This function will consider all 
	entities as single character and returns the length to adjust in offset.*/
	this.getEntitiesAdjustedOffset = function (htmlStr) {
		// Find entities offset adjustment
		var startIndex = 0,
			entityOffetAdjustment = 0,
			offset = htmlStr.length;
		while (true) {
			var entityStartIndex = htmlStr.indexOf("&", startIndex);
			if (entityStartIndex === -1) {
				break;
			}

			var entityEndIndex = htmlStr.indexOf(";", entityStartIndex);
			if (entityEndIndex === -1) {
				break;
			}

			var entityLen =  entityEndIndex - entityStartIndex;

			entityOffetAdjustment += entityLen;

			startIndex = entityEndIndex;
		}
		return (offset - entityOffetAdjustment);
	};

	/*Entities may occupy different length in DW. This function will consider all 
	entities as single character "*" and returns the new string*/
	this.getEntitiesAdjustedText = function (htmlStr) {
		// Find entities offset adjustment
		var text = "",
			entityStartIndex,
			entityEndIndex,
			startIndex = 0;

		while (true) {
			entityStartIndex = htmlStr.indexOf("&", startIndex);
			if (entityStartIndex === -1) {
				text += htmlStr.substring(startIndex, htmlStr.length);
				break;
			}

			entityEndIndex = htmlStr.indexOf(";", entityStartIndex);
			if (entityEndIndex === -1) {
				// We should not be here.
				// In error html case, if there is & without ?
				// we returning original string as it is.
				text = htmlStr;
				break;
			}

			text += htmlStr.substring(startIndex, entityStartIndex);
			text += "*";

			startIndex = entityEndIndex + 1;
		}

		return text;
	};

	/* Builds identifier for start caret of marquee selection and returns*/
	this.getStartIdentifier = function (range) {
		if (!range || !range.startContainer) {
			return null;
		}

		// Get start container element
		var element = range.startContainer;
		if (element.nodeType !== Node.ELEMENT_NODE) {
			element = element.parentNode;
		}

		// Find selection inside start container 
		var preCaretRange = range.cloneRange();
		preCaretRange.selectNodeContents(element);
		preCaretRange.setEnd(range.startContainer, range.startOffset);

		// Get selection end point info
		return this.getRangeEndProperties(preCaretRange, element);
	};

	/* Builds identifier for end caret of marquee selection and returns*/
	this.getEndIdentifier = function (range) {
		if (!range || !range.endContainer) {
			return null;
		}

		// Get end container element
		var element = range.endContainer;
		if (element.nodeType !== Node.ELEMENT_NODE) {
			element = element.parentNode;
		}

		// Find selection inside end container 
		var preCaretRange = range.cloneRange();
		preCaretRange.selectNodeContents(element);
		preCaretRange.setEnd(range.endContainer, range.endOffset);

		// Get selection end point info
		return this.getRangeEndProperties(preCaretRange, element);
	};

	/* Returns the properties of range. Such as DWUniqueId, offset inside/after
	element */
	this.getRangeEndProperties = function (preCaretRange, element) {
		if (!preCaretRange || !element) {
			return null;
		}

		// Clone content to find adjusted offset.
		var container = document.createElement("div");
		container.appendChild(preCaretRange.cloneContents());

		// Find last element inside container
		var endIdObj = {},
			foundElement = false,
			lastChildID = null,
			childNode = container.firstChild;
		while (childNode) {
			if (childNode.nodeType === Node.ELEMENT_NODE) {
				foundElement = true;
				lastChildID = childNode.getAttribute(DW_LIVEEDIT_CONSTANTS.DWUniqueId);
			}
			childNode = childNode.nextSibling;
		}

		if (foundElement) {
			// if we found and no unique id, it can not be identified in DW.
			if (!lastChildID) {
				return null;
			}

			// remove all nodes till last element.
			var isLastNode = false,
				nextNode;

			childNode = container.firstChild;
			while (childNode) {
				nextNode = childNode.nextSibling;

				if (childNode.nodeType === Node.ELEMENT_NODE
						&& lastChildID === childNode.getAttribute(DW_LIVEEDIT_CONSTANTS.DWUniqueId)) {
					isLastNode = true;
				}

				container.removeChild(childNode);

				if (isLastNode) {
					break;
				}

				childNode = nextNode;
			}

			endIdObj.dwId = lastChildID;
			endIdObj.position = "after";
		} else {
			endIdObj.dwId = element.getAttribute(DW_LIVEEDIT_CONSTANTS.DWUniqueId);
			endIdObj.position = "inside";
		}

		endIdObj.offset = this.getEntitiesAdjustedOffset(container.innerHTML);
		endIdObj.html = this.getEntitiesAdjustedText(container.innerHTML);

		return endIdObj;
	};

	/* Gets start and end identifier for marquee selection and returns*/
	this.getDWSelectionIdentifier = function () {
		var sel = window.getSelection();
		if (sel.rangeCount > 1 || sel.rangeCount === 0) {
			return null;
		}

		var range = sel.getRangeAt(0),
			startIDObj = this.getStartIdentifier(range),
			endIDObj = this.getEndIdentifier(range);

		if (!startIDObj || !endIDObj) {
			return null;
		}

		var selectionInfo = {};
		selectionInfo.startId = startIDObj;
		selectionInfo.endId = endIDObj;

		return selectionInfo;
	};

	/* Reset DW Marquee selection state on selection identification failure. */
	this.resetDWMarqueeSelectionState = function (force) {
		if (this.selectionTimer) {
			window.clearTimeout(this.selectionTimer);
			this.selectionTimer = null;
		}
		window.DWLECallJsBridgingFunction(DW_LIVEEDIT_CONSTANTS.SelectionSyncBridge, "ResetSelectionState", force, false);
	};

	/* Set DW Marquee selection for live view selection range. */
	this.setDWMarqueeSelectionForRange = function (selectionInfo, force) {
		if (!selectionInfo) {
			return;
		}

		var thisObj = this;
		this.selCtx = selectionInfo;

		// If selection is already in progress, wait for timout.
		if (this.selectionIsInProgress && !force) {
			if (!this.selectionTimer) {
				this.selectionTimer = window.setTimeout(function () {
					thisObj.setDWMarqueeSelectionForRange(thisObj.selCtx, true);
					thisObj.selectionTimer = null;
				}, 100);
			}
			return;
		}

		// Set progress flag.
		this.selectionIsInProgress = true;

		// Callback to reset progress flag.
		selectionInfo.callback = function (success) {
			thisObj.selectionIsInProgress = false;

			// we need to tell other extension about the new selection
			var messageDetails = {};
			messageDetails.type = DW_EXTENSION_EVENT.SELECTION_CHANGE;
			liveViewExtensionsObject.messageToExtensions(messageDetails);
		};

		window.DWLECallJsBridgingFunction(DW_LIVEEDIT_CONSTANTS.SelectionSyncBridge, "DWSMSetSelectionOnRange", selectionInfo, true);
	};

	/* Global mouse up handler to detect and handle marquee selection*/
	this.myMouseUp = function (event) {
		// Don't do anything when in inspect mode
	    if (this.inspectMode !== DW_INSPECT_CONST.InspectModeOff) {
			return;
		}
		//clicking on img, audio, video does not change the selection in browser,
		//so we need to clear the previous selection in these cases
		if (window.IDBASEDLVSELECTIONSYNC === true && window.getSelection() &&
				(event.which === 1 /*left button*/ && event.target && this.mouseDownTarget === event.target &&
					(event.target.tagName === "IMG" || event.target.tagName === "AUDIO" || event.target.tagName === "VIDEO"))) {
			window.getSelection().removeAllRanges();
			this.resetDWMarqueeSelectionState(true);
			return;
		}

		this.handleDWSelectionUsingIdentifier();
	};

	/* Global mouse down handler to detect and handle marquee selection*/
	this.myMouseDown = function (event) {
		// To detect click on img, audio & video elements.
		this.mouseDownTarget = event.target;
	};

	/* Inspect mode change handler */
	this.handleDWInspectModeChange = function (inspectMode) {
		this.inspectMode = inspectMode;
        
        if (inspectMode > DW_INSPECT_CONST.InspectModeOff && inspectMode < DW_INSPECT_CONST.InspectModeLast) {
            this.oldUseCefSelection = this.useCefSelection;
            this.useCefSelection = true;
        }
        
        this.Log("Inspect Mode Changed to " + this.inspectMode);
        
        if (inspectMode > DW_INSPECT_CONST.InspectModeOff && inspectMode < DW_INSPECT_CONST.InspectModeLast) {
            this.oldUseCefSelection = this.useCefSelection;
            this.useCefSelection = true;
        }
        
        if (dwExtensionController) {
            var messageDetails = {};
            messageDetails.type = DW_EXTENSION_EVENT.INSPECT_MODE_CHANGE;
            messageDetails.mode = inspectMode;
            dwExtensionController.sendMessage(messageDetails);
        }
	};

	/* Marquee selection handler */
	this.handleDWSelectionUsingIdentifier = function () {
	    if (window.IDBASEDLVSELECTIONSYNC !== true || this.inspectMode !== DW_INSPECT_CONST.InspectModeOff) {
			return false;
		}

		try {
			var selInfo = this.getDWSelectionIdentifier();
			if (selInfo && (selInfo.startId.dwId === selInfo.endId.dwId
					&& selInfo.startId.position === selInfo.endId.position
					&& selInfo.startId.offset === selInfo.endId.offset) === false) {
				this.setDWMarqueeSelectionForRange(selInfo, false);
				return true;
			}
			this.resetDWMarqueeSelectionState(true);
			return false;
		} catch (e) {
			console.log(e.message);
		}
		this.resetDWMarqueeSelectionState(true);
		return false;
	};

	/* DW Message handler */
	this.messageHandler = function (e) {
		if (e.data && e.data.type) {
			if (e.data.type === DW_EXTENSION_EVENT.SELECTION_CHANGE) {
				this.resetDWMarqueeSelectionState(false);
			}
		}
	};

	this.selStart = function (e) {
		this.useCefSelection = false;
        if (e && e.target) {
            if (e.target.nodeType ===  Node.TEXT_NODE) {
                this.useCefSelection = true;
            }
        }
        
        if (this.oldUseCefSelection !== this.useCefSelection) {
            dwNotifySelectionChange(this.useCefSelection);
        }
	};

	this.selChange = function (e) {
		dwNotifySelectionChange(this.useCefSelection);
	};

	this.sourceChange = function (mutations) {
		dwNotifySourceChange();
	};
    
    this.reloadStylesCompleted = function () {
        dwReloadStylesCompleted();
    };
    
    this.docMouseDown = function (e) {
        if (this.inspectMode === DW_INSPECT_CONST.InspectModeOff) {
			this.oldUseCefSelection = this.useCefSelection;
			this.useCefSelection = false;
		}
    };
    
    this.docMouseClick = function (e) {
        if (this.useCefSelection) {
            return;
        }
        
        if (e && e.target) {
            var dwUniqueID = e.target.getAttribute(DW_LIVEEDIT_CONSTANTS.DWUniqueId);
            if (dwUniqueID) {
                dwSelectRun(dwUniqueID);
            }
        }
    };
    
    this.docMouseMove = function (e) {
        if (this.inspectMode === DW_INSPECT_CONST.InspectModeOff) {
			return;
		}
        
        this.Log("Current Inspect Mode " + this.inspectMode);
        
        if (e && e.target) {
            window.dwDrawNodeHighlight(e.target,  e.clientX, e.clientY, DW_VISUAL_DOM_POSITIONS.NoInput, this.inspectMode);
        }
    };
	
	this.docResize = function () {
        if (this.inspectMode === DW_INSPECT_CONST.InspectModeOff) {
			return;
		}
		
		//refresh inspect highlight
		if (window.dwRefreshNodeHighlight) {
			window.dwRefreshNodeHighlight();
		}
    };

	/* Initializer */
	this.initialize = function () {
	    this.inspectMode = DW_INSPECT_CONST.InspectModeOff;
        this.useCefSelection = true;
        this.oldUseCefSelection = true;
        
        // Selection changes
		document.addEventListener("selectstart", this.selStart.bind(this), true);
		document.addEventListener("selectionchange", this.selChange.bind(this), false);
        document.addEventListener("reloadStylesCompleted", this.reloadStylesCompleted.bind(this), false);
        document.addEventListener("mousedown", this.docMouseDown.bind(this), true);
        document.addEventListener("click", this.docMouseClick.bind(this), true);
        document.addEventListener("mousemove", this.docMouseMove.bind(this), true);

		window.addEventListener("mouseup", this.myMouseUp.bind(this), true);
		window.addEventListener("mousedown", this.myMouseDown.bind(this), true);
		window.handleDWSelectionUsingIdentifier = this.handleDWSelectionUsingIdentifier.bind(this);
		window.handleDWInspectModeChange = this.handleDWInspectModeChange.bind(this);
		window.addEventListener("message", this.messageHandler.bind(this), false);

		//resize handler
		window.addEventListener("resize", this.docResize.bind(this), true);

		// Observe document changes
		var observer = new MutationObserver(this.sourceChange.bind(this));
		observer.observe(document, { childList: true, attributes: true, characterData: true, subtree: true });
	};
}

var selCEFDWSyncUtil = new CEFDWSelectionSyncUtil();
selCEFDWSyncUtil.initialize();
