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

Purpose- 
This file has the implementation for Dreamweaver - Live View selection syncronization utility functions.
*/

/*jslint vars: true, plusplus: true, devel: true, browser: true, nomen: true, maxerr: 50 */
/*global GenericLiveEditBridger,dw, DW_LIVEEDIT_CONSTANTS, DWfile, FeatureManager */

function LVSelectionSyncBridger(documentDOM) {
    'use strict';
    this.logDebugMsg("Creating LVSelectionSyncBridger");
	var browser = dw.getBrowser();
    if (browser) {
        var browserWindow = browser.getWindow();
		if (browserWindow) {
			browserWindow.IDBASEDLVSELECTIONSYNC = dw.featureManager.isEnabled(FeatureManager.IDBASEDLVSELECTIONSYNC);
		}
    }
    this.setDocumentDOM(documentDOM);

	this.whiteSpaceChars = [
		9, // Tab
		10, // New Line (LF)
		13, // Enter (CR)
		32 // Blank Space
	];
}

LVSelectionSyncBridger.prototype = new GenericLiveEditBridger();
LVSelectionSyncBridger.prototype.constructor = LVSelectionSyncBridger;
LVSelectionSyncBridger.prototype.baseClass = GenericLiveEditBridger.prototype.constructor;

/*
Function: logMsg
	log message

Arguments :
	msg - message to log.
*/
LVSelectionSyncBridger.prototype.logMsg = function (msg) {
	'use strict';
	this.logDebugMsg(msg);
};

/*
Function: getEntityAdjustment
	Entities occupies different length in Live View. This function will consider all 
	entities as single character and returns the length to adjust in offset.

Arguments :
	htmlStr - input string.
Returns :
	returns length to adjust for entities.
*/
LVSelectionSyncBridger.prototype.getEntityAdjustment =  function (htmlStr, beginIndex, endOffset) {
	'use strict';
	if (typeof htmlStr !== "string" || htmlStr.length === 0) {
		return 0;
	}

	this.logMsg("Find entities offset adjustment :");
	// Find entities offset adjustment
	var startIndex = beginIndex,
		entityOffetAdjustment = 0;
	while (true) {
		var entityStartIndex = htmlStr.indexOf("&", startIndex);
		if (entityStartIndex === -1 || entityStartIndex > (endOffset + entityOffetAdjustment)) {
			break;
		}

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

		var entityLen =  entityEndIndex - entityStartIndex;

		entityOffetAdjustment += entityLen;

		startIndex = entityEndIndex;
	}
	this.logMsg("Entities offset adjustment :" + entityOffetAdjustment);
	return entityOffetAdjustment;
};

/*
Function: getNewLineAdjustment
	'\r\n' is treated as single '\n' character in Live View. This function 
	is to calculate the difference to adjust with offset

Arguments :
	htmlStr - input string.
Returns :
	returns length to adjust for new line.
*/
LVSelectionSyncBridger.prototype.getNewLineAdjustment =  function (htmlStr, beginIndex, endOffset) {
	'use strict';
	if (typeof htmlStr !== "string" || htmlStr.length === 0) {
		return 0;
	}

	// Find newLine offset adjustment
	var newLineStartIndex,
		newLineEndIndex,
		startIndex = beginIndex,
		newLineOffetAdjustment = 0;
	while (true) {
		newLineStartIndex = htmlStr.indexOf("\r", startIndex);
		if (newLineStartIndex === -1 || newLineStartIndex > (endOffset + newLineOffetAdjustment)) {
			break;
		}

		startIndex = newLineStartIndex + 1;
		newLineEndIndex = htmlStr.indexOf("\n", newLineStartIndex);
		if (newLineStartIndex !== -1 && newLineEndIndex !== -1 && newLineStartIndex === (newLineEndIndex - 1)) {
			newLineOffetAdjustment += 1;
			startIndex = newLineEndIndex;
		}
	}

	return newLineOffetAdjustment;
};

/*
Function: getEntitiesAdjustedText
	Entities occupies different length in Live View. This function will replace all 
	entities '*' character so that it is comparable with live view string.

Arguments :
	htmlStr - input string.
Returns :
	returns string after replacing all entities with *.
*/
LVSelectionSyncBridger.prototype.getEntitiesAdjustedText = function (htmlStr) {
	'use strict';
	if (typeof htmlStr !== "string") {
		return null;
	}

	// 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;
};

/*
Function: getNewLineAdjustedText
	'\r\n' is treated as single '\n' character in Live View. This function 
	is to replace all \r\n to \n so that it is comparable with live view string.

Arguments :
	htmlStr - input string.
Returns :
	returns string after replacing \r\n with \n.
*/
LVSelectionSyncBridger.prototype.getNewLineAdjustedText = function (htmlStr) {
	'use strict';
	if (typeof htmlStr !== "string") {
		return null;
	}

	htmlStr = htmlStr.replace(/\r\n/g, '\n');
	return htmlStr;
};

/*
Function: isTextPatternMatches
	Compares two texts. It does pattern matching. If "*" is part of 
	one string, the other string can have any character in same index 

Arguments :
	text1 - input string 1.
	text2 - input string 2.
Returns :
	true if both strings are matching or returns false.
*/
LVSelectionSyncBridger.prototype.isTextPatternMatches = function (text1, text2) {
	'use strict';

	// argument must be string type
	if (typeof text1 !== "string" || typeof text2 !== "string") {
		return false;
	}

	// if both are empty string
	if (text1.length === 0 && text1 === text2) {
		return true;
	}

	// if length is not matching, return false
	if (text1.length !== text2.length) {
		return false;
	}

	// Find entities offset adjustment
	var i, ch1, ch2,
		retval = true,
		textLen = text1.length;
	for (i = 0; i < textLen; i++) {
		ch1 = text1.charAt(i);
		ch2 = text2.charAt(i);
		if (ch1 !== ch2 && ch1 !== "*" && ch2 !== "*") {
			retval = false;
			break;
		}
	}

	return retval;
};

/*
Function: TryWhiteSpaceIgnoredOffsets
	Some times White Characters (such CR, NewLine and Tab) at the begining of Texts/Element
	cause the difference in offsets. We will try to find and ignore those characters and adjust
	offset accordingly. 

Arguments :
	beginOffset in Source code to start with.
	endOffset in Source code which needs to be adjusted.
	expectedHTML - input string.
Returns :
	returns offset to adjust in source.
*/
LVSelectionSyncBridger.prototype.TryWhiteSpaceIgnoredOffsets = function (expectedHTML, beginOffset, endOffset, adjustmentDirection) {
	'use strict';

	// argument must be string type
	if (typeof expectedHTML !== "string" || typeof beginOffset !== "number" || typeof endOffset !== "number") {
		this.logMsg("TryWhiteSpaceIgnoredOffsets - args error");
		// return negative Value to indicate error
		return -1;
	}

	if (!adjustmentDirection) {
		// adjustment and comparison can happen either side by
		// moving end offset back and forth.
		adjustmentDirection = "<->";
	}

	var theDOM = this.getDocumentDOM();
	if (!theDOM) {
		this.logMsg("TryWhiteSpaceIgnoredOffsets - theDOM error");
		return -1;
	}

	var outerHTML = theDOM.getDocumentFragment(beginOffset, endOffset);
	if (typeof outerHTML !== "string") {
		this.logMsg("TryWhiteSpaceIgnoredOffsets - outerHTML error");
		return -1;
	}

	outerHTML = this.getEntitiesAdjustedText(outerHTML);
	outerHTML = this.getNewLineAdjustedText(outerHTML);

	// Find entities offset adjustment
	var index1 = 0,
		index2 = 0,
		ch1,
		ch2,
		retval = true,
		outerHTMLLen = outerHTML.length,
		expecteHTMLLen = expectedHTML.length;
	while (true) {
		// Check string boundry.
		if (index1 >= outerHTMLLen || index2 >= expecteHTMLLen) {
			break;
		}

		// Compare chars
		ch1 = outerHTML.charAt(index1);
		ch2 = expectedHTML.charAt(index2);
		if (ch1 !== ch2 && ch1 !== "*" && ch2 !== "*") {
			// Check if white space and adjust index
			var s1 = outerHTML.charCodeAt(index1),
				s2 = expectedHTML.charCodeAt(index2),
				whiteSpaceFound = false;
			if (this.whiteSpaceChars.indexOf(s1) >= 0) {
				whiteSpaceFound = true;
				index1++;
			}
			if (this.whiteSpaceChars.indexOf(s2) >= 0) {
				whiteSpaceFound = true;
				index2++;
			}

			if (!whiteSpaceFound) {
				// mismatch return error
				return -1;
			}
		} else {
			// increase index
			index1++;
			index2++;
		}
	}

	// Check we are done completely
	var retVal = -1;
	if (index1 === outerHTMLLen && index2 === expecteHTMLLen) {
		// all are equal return current end offset
		retVal = endOffset;
	} else if (index1 === outerHTMLLen && index2 < expecteHTMLLen && (adjustmentDirection === "->" || adjustmentDirection === "<->")) {
		// We haven't done yet. we need to increase end offset and compare
		retVal = this.TryWhiteSpaceIgnoredOffsets(expectedHTML, beginOffset, endOffset + 1, "->");
	} else if (index1 < outerHTMLLen && index2 === expecteHTMLLen && (adjustmentDirection === "<-" || adjustmentDirection === "<->")) {
		// We haven't done yet. we need to decrease end offset and compare
		retVal = this.TryWhiteSpaceIgnoredOffsets(expectedHTML, beginOffset, endOffset - 1, "<-");
	}

	// some error.
	return retVal;
};

/*
Function: GetAdjustedOffsetInsideElement
Utility function to do required offset adjustment. Following are known cases (Keep this updated).

	1. Entities - entity like &euro; is considered as one character in Live View.

Arguments : 
	element - element which is the base/starting point for offset input.
	offsetElement - input offset position inside element which needs to be adjusted.
*/
LVSelectionSyncBridger.prototype.GetAdjustedOffsetInsideElement = function (element, offsetElement, validationHTML) {
    'use strict';
    if (!element) {
        this.logDebugMsg("GetAdjustedOffsetInsideElement args are not present");
		return -1;
    }

	var theDOM = this.getDocumentDOM();
	if (!theDOM) {
		return -1;
	}

	this.logMsg("GetAdjustedOffsetInsideElement for :" + offsetElement);

	var outerHTML = element.outerHTML,
		startTagEnd = outerHTML.indexOf(">"),
		startTagOffsetArrayInDOM = theDOM.nodeToOffsets(element),
		startTagOffsetInDOM = (startTagOffsetArrayInDOM ? startTagOffsetArrayInDOM[0] : -1),
		searchEndOffset = startTagEnd + offsetElement;

	if (startTagEnd === -1 || startTagOffsetInDOM === -1) {
		return -1;
	}

	// Find entities offset adjustment
	var entityOffetAdjustment = this.getEntityAdjustment(outerHTML, startTagEnd, searchEndOffset);
	var newLineOffsetAdjustment = this.getNewLineAdjustment(outerHTML, startTagEnd, searchEndOffset + entityOffetAdjustment);
	var adjustedOffset = (startTagOffsetInDOM + searchEndOffset + entityOffetAdjustment + newLineOffsetAdjustment + 1);

	outerHTML = theDOM.getDocumentFragment(startTagOffsetInDOM + startTagEnd, adjustedOffset);

	if (typeof outerHTML !== "string") {
		return -1;
	}

	outerHTML = this.getEntitiesAdjustedText(outerHTML);
	outerHTML = this.getNewLineAdjustedText(outerHTML);

	if (this.isTextPatternMatches(outerHTML, validationHTML)) {
		return adjustedOffset;
	}

	var newOffset = this.TryWhiteSpaceIgnoredOffsets(validationHTML, startTagOffsetInDOM + startTagEnd, adjustedOffset + 1);
	if (newOffset === -1) {
		this.logMsg("HTML VALIDATION IS FAILED");
		return -1;
	}

	return newOffset;
};

/*
Function: GetAdjustedOffsetAfterElement
Utility function to do required offset adjustment. Following are known cases (Keep this updated).

	1. Entities - entity like &euro; is considered as one character in Live View.

Arguments : 
	element - element which is the base/starting point for offset input.
	offsetElement - input offset position after end of element which needs to be adjusted.
*/
LVSelectionSyncBridger.prototype.GetAdjustedOffsetAfterElement = function (element, offsetElement, validationHTML) {
    'use strict';
    if (!element) {
        this.logDebugMsg("GetAdjustedOffsetAfterElement args are not present");
		return -1;
    }

	var theDOM = this.getDocumentDOM();
	if (!theDOM) {
		return -1;
	}

	var parent = element.parentNode;
	if (!parent) {
		return -1;
	}

	this.logMsg("GetAdjustedOffsetAfterElement for :" + offsetElement);

	// Get document fragment html string.
	var parentTagOffsetInDOM = theDOM.nodeToOffsets(parent),
		elementOffsetInDOM = theDOM.nodeToOffsets(element);
	if (!parentTagOffsetInDOM || !elementOffsetInDOM) {
		return -1;
	}

	var outerHTML = theDOM.getDocumentFragment(elementOffsetInDOM[0], parentTagOffsetInDOM[1]);
	if (typeof outerHTML !== "string") {
		return -1;
	}

	var startTagEnd = outerHTML.indexOf(">", (elementOffsetInDOM[1] - elementOffsetInDOM[0] - 1)),
		startTagOffsetInDOM = elementOffsetInDOM[0],
		searchEndOffset = startTagEnd + offsetElement;

	if (startTagEnd === -1) {
		return -1;
	}

	// Find entities offset adjustment
	var entityOffetAdjustment = this.getEntityAdjustment(outerHTML, startTagEnd, searchEndOffset);
	var newLineOffsetAdjustment = this.getNewLineAdjustment(outerHTML, startTagEnd, searchEndOffset + entityOffetAdjustment);
	var adjustedOffset = startTagOffsetInDOM + searchEndOffset + entityOffetAdjustment + newLineOffsetAdjustment + 1;

	outerHTML = theDOM.getDocumentFragment(elementOffsetInDOM[1], adjustedOffset);
	if (typeof outerHTML !== "string") {
		return -1;
	}

	outerHTML = this.getEntitiesAdjustedText(outerHTML);
	outerHTML = this.getNewLineAdjustedText(outerHTML);

	if (this.isTextPatternMatches(outerHTML, validationHTML)) {
		return adjustedOffset;
	}

	var newOffset = this.TryWhiteSpaceIgnoredOffsets(validationHTML, elementOffsetInDOM[1], adjustedOffset + 1);
	if (newOffset === -1) {
		this.logMsg("HTML VALIDATION IS FAILED");
		return -1;
	}

	return newOffset;
};

/*
Function: GetAdjustedSourceOffset
Utility function to do required offset adjustment. Following are known cases (Keep this updated).

	1. Entities - entity like &euro; is considered as one character in Live View.

Arguments : 
	identifier - Identifier object.
	
	Identifier consists of following info.
	
		dwId = Unique ID in DW;
		position = postion of offset - "after" or "inside" element;
		offset = offset value
		html = html string;
*/
LVSelectionSyncBridger.prototype.GetAdjustedSourceOffset = function (identifier) {
    'use strict';
    if (!identifier) {
        this.logDebugMsg("GetAdjustedSourceOffset args are not present");
		return -1;
    }

	var theDOM = this.getDocumentDOM();
	if (!theDOM) {
		this.logDebugMsg("DOM is not found");
		return -1;
	}

	var element = this.getElementByDWId(identifier.dwId);
	if (!element) {
		this.logDebugMsg("Invalid identifier.dwId");
		return -1;
	}

	var adjustedOffset = -1;
	if (identifier.position === "inside") {
		adjustedOffset = this.GetAdjustedOffsetInsideElement(element, identifier.offset, identifier.html);
	} else if (identifier.position === "after") {
		adjustedOffset = this.GetAdjustedOffsetAfterElement(element, identifier.offset, identifier.html);
	}

	return adjustedOffset;
};

/*
Function: MakeSelectionByOffsets
	Make marquee selection by given start and end offset. 

Arguments :
	start - Number start offset in sorce code.
	end - Number end offset in sorce code.
Returns :
	true if selection is success or returns false.
*/
LVSelectionSyncBridger.prototype.MakeSelectionByOffsets = function (start, end) {
	'use strict';
	var theDOM = this.getDocumentDOM();
	theDOM.setSelection(start, end);

	// Validate Selection
	var curSel = theDOM.getSelection();

	if (start !== curSel[0] || end !== curSel[1]) {
		this.logMsg("##########################################");
		this.logMsg("DW SELECTION FAILED");
		this.logMsg("Given Start Offset " + start);
		this.logMsg("Given End Offset " + end);
		this.logMsg("Curr Start Offset " + curSel[0]);
		this.logMsg("Curr End Offset " + curSel[1]);
		this.logMsg("##########################################");
		return false;
	}

	return true;
};

/*
Function: DWSMSetSelectionOnRange
Utility function to set selection on offset range

Arguments : range info {
	obj.startId - Identifier of start offset,
	obj.endId - Identifier of end offset
	
	Identifier consists of following info.
	
		dwId = Unique ID in DW;
		position = postion of offset - "after" or "inside" element;
		offset = offset value
		html = html string;
}
*/
LVSelectionSyncBridger.prototype.DWSMSetSelectionOnRange = function (argObj) {
    'use strict';
    if (!argObj || !argObj.startId || !argObj.endId) {
        this.logDebugMsg("DWSMSetSelectionOnRange argObj is not present");
		return;
    }

	this.logMsg("************* DWSMSetSelectionOnRange ***************");
	try {
		var selectionSuccess = false,
			startIdentifier = argObj.startId,
			endIdentifier = argObj.endId;

		var theDOM = this.getDocumentDOM();
		var startoffsets = this.GetAdjustedSourceOffset(startIdentifier);
		var endoffsets = this.GetAdjustedSourceOffset(endIdentifier);

		this.logMsg("Start Node adjusted Offset " + startoffsets + "\n");
		this.logMsg("End Node  adjusted Offset " + endoffsets + "\n");

		if (startoffsets >= 0 && endoffsets >= 0) {
			//DW selection bug. try reverse selection
			if (this.MakeSelectionByOffsets(startoffsets, endoffsets)) {
				selectionSuccess = true;
			} else if (this.MakeSelectionByOffsets(endoffsets, endoffsets) && this.MakeSelectionByOffsets(startoffsets, endoffsets)) {
				selectionSuccess = true;
			} else {
				// Marquee selection failure case. Just select full element.
				var startelement = this.getElementByDWId(startIdentifier.dwId),
					endelement = this.getElementByDWId(endIdentifier.dwId);
				if (startelement && endelement) {
					startoffsets = theDOM.nodeToOffsets(startelement)[0];
					endoffsets = theDOM.nodeToOffsets(endelement)[1];
					if (startoffsets >= 0 && endoffsets >= 0) {
						this.MakeSelectionByOffsets(startoffsets, endoffsets);
					}
				}
			}
		}

		if (selectionSuccess && theDOM.getLiveCodeEnabled()) {
			dw.syncCurrentLiveDocFromUserDocSelection();
		}

		// Set validity of live View selection.
		theDOM.setLVMarqueeSelectionValid(selectionSuccess);
		this.selectionStart = startoffsets;
		this.selectionEnd = endoffsets;

		// Issue callback to LV.
		argObj.callback(selectionSuccess);
	} catch (e) {
		this.logMsg(e.message + "\n");
	}
};

/*
Function: DWSMSelectElement
Utility function to set selection on element identified by DW unique ID.

Arguments :
	dwUniqueID - DW ID of element
*/
LVSelectionSyncBridger.prototype.DWSMSelectElement = function (dwUniqueID) {
    'use strict';
    if (!dwUniqueID) {
        this.logDebugMsg("DWSMSelectElement dwUniqueID is not present");
        return;
    }

    try {
        var theDOM = this.getDocumentDOM();

        var element = this.getElementByDWId(dwUniqueID);
        if (element) {
            var offsets = theDOM.nodeToOffsets(element);
            if (this.MakeSelectionByOffsets(offsets[0], offsets[1])) {
                dw.syncCurrentLiveDocFromUserDocSelection();
            }
        }
    } catch (e) {
        this.logMsg(e.message);
    }
};

/*
Function: ResetSelectionState
	Reset marquee selection flag.

Arguments :
	force - if true, reset the validity of marquee selection flag,
		else reset it only if current selection differ from last successful selection.
*/
LVSelectionSyncBridger.prototype.ResetSelectionState = function (force) {
	'use strict';
	this.logMsg("ResetSelectionState ..... force = " + force);
	var theDOM = this.getDocumentDOM();
	if (force && theDOM) {
		theDOM.setLVMarqueeSelectionValid(false);
	} else if (theDOM && theDOM.getLVMarqueeSelectionValid()) {
		var curSel = theDOM.getSelection();
		if (this.selectionStart !== curSel[0] || this.selectionEnd !== curSel[1]) {
			theDOM.setLVMarqueeSelectionValid(false);
		}
	}
};

