/*
 * ADOBE CONFIDENTIAL
 *
 * Copyright (c) 2015 Adobe Systems Incorporated. All rights reserved.
 *
 * NOTICE:  All information contained herein is, and remains
 * the property of Adobe Systems Incorporated and its suppliers,
 * if any.  The intellectual and technical concepts contained
 * herein are proprietary to Adobe Systems Incorporated and its
 * suppliers and are protected by trade secret or copyright law.
 * Dissemination of this information or reproduction of this material
 * is strictly forbidden unless prior written permission is obtained
 * from Adobe Systems Incorporated.
 */

/*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4,
maxerr: 50, node: true */
/*global dw, DWfile, UT_SOFTUPDATE, UT_UpdateSuccessful, UT_UpdateFailed, UT_InAppUpdates_Reverted*/

"use strict";

var csXmlOpenNode = "<updates>\r\n";
var csXmlUpdateRecord = "\t<update id=\"@@id@@\"/>\r\n";
var csXmlCloseNode = "</updates>";
var csIdReplacePattern = /@@id@@/g;

// Files/Directories
var csConfigDir = "Configuration";
var csUpdatesDir = "Updates";
var csInstallDir = "Install";
var csUninstallDir = "Uninstall";
var csLogDir = "Log";
var csInstallXml = "install.xml";
var csUninstallXml = "uninstall.xml";
var csUpdateXml = "update.xml";
var csLogTxt = "log.txt";
var csIamDir = "Iam";
var csStringsDir = "Strings";
var csActionsProperties = "updatesAction.properties";
var csPathSeparator = "/";

// Tags
var csUpdatesTag = "updates";
var csUpdateTag = "update";
var csOperationsTag = "operations";
var csOperationTag = "operation";
var csPostInstallTag = "post-install";
var csPostUninstallTag = "post-uninstall";

// Attributes
var csIdAttr = "id";
var csNameAttr = "name";
var csParamAttr = "param";
var csModeAttr = "mode";

// Attribute values
var csAddCommand = "add";
var csDeleteCommand = "delete";
var csFailNeverMode = "fail-never";
var csEvalCommand = "eval";

var csInstallFailure = "install";
var csRevertFailure = "revert";

// Object to handle installation of all updates
function Updater() {

	// In this file, directory URLs end with '/'
	var configDirURL = dw.getUserConfigurationPath();
	var updatesDirURL = configDirURL + csUpdatesDir + csPathSeparator;
	var installDirURL = updatesDirURL + csInstallDir + csPathSeparator;
	var installXmlURL = installDirURL + csInstallXml;
	var uninstallDirURL = updatesDirURL + csUninstallDir + csPathSeparator;
	var uninstallXmlURL = uninstallDirURL + csUninstallXml;
	var logURL = updatesDirURL + csLogDir + csPathSeparator + csLogTxt;
	var updatesActionFileURL = updatesDirURL + csIamDir + csPathSeparator + csStringsDir + csPathSeparator + csActionsProperties;

	function _log(msg) {
		try {
			var ts = new Date();
			var tsStr = ts.toDateString() + " " + ts.toLocaleTimeString('en-US', { hour12: false });
			DWfile.write(logURL, tsStr + ": " + msg + "\r\n", "append");
		} catch (e) {}
	}

	// Command to move a file/directory. Command can be executed and undone.
    // 'backupDirUrl' is optional. If provided, backup of destination file/directory
    // will be taken before actual move operation.
	function MoveCommand(path, fromDirUrl, toDirUrl, backupDirUrl) {
		var fromUrl;
		var toUrl;
        var backupUrl;
        var backup = false;
		var invalidPath = false;

        // Path
		path = path.replace(/\\/g, "/");

		// DWfile.move() fails for Urls that end with a path separator
		// so remove it, if exists.
		if (path.charAt(path.length - 1) === csPathSeparator) {
			path = path.slice(0, -1);
		}
		// Remove path separator in the beginning
		if (path.charAt(0) === csPathSeparator) {
			path = path.slice(1);
		}
		// Validate path
		if (path === "" || path === csPathSeparator) {
			invalidPath = true;
		}

        // From path
        fromDirUrl = fromDirUrl.replace(/\\/g, "/");
        fromUrl = fromDirUrl + path;

        // To path
		toDirUrl = toDirUrl.replace(/\\/g, "/");
		toUrl = toDirUrl + path;

        // Backup path
        if (backupDirUrl) {
            backupDirUrl = backupDirUrl.replace(/\\/g, "/");
            backupUrl = backupDirUrl + path;
            backup = true;
        }

        function _execute() {
            var success = true;
			if (invalidPath) {
				success = false;
			}

            // If an update get installed partially during a session of Dreamweaver,
            // it will be installed again on next launch and same command may get 
            // executed more than once (once in previous session + once in current session).
            // So, move a file only if source path exists. If not, it means this command
            // is being executed second time, so return success.
            if (success && DWfile.exists(fromUrl, false)) {
                // Take a backup of destination file, if asked.
                if (backup && DWfile.exists(toUrl, false)) {
                    // Ignore errors
                    DWfile.move(toUrl, backupUrl, false);
                }
                success = DWfile.move(fromUrl, toUrl, false);
            }

            if (!success) {
                _log("Failed to move '" + fromUrl + "' to '" + toUrl + "' during execute.");
            }

            return success;
        }

        function _undo() {
            var success = true;
			if (invalidPath) {
				success = false;
			}

            // Perform undo only if this command was executed before i.e. source file was moved.
            if (success && !DWfile.exists(fromUrl, false)) {
                if (DWfile.exists(toUrl, false)) {
                    success = DWfile.move(toUrl, fromUrl, false);
                }
            }

            // Move backup file to destination location
            if (success && backup && !DWfile.exists(toUrl, false)) {
                if (DWfile.exists(backupUrl, false)) {
					// Ignore errors
					DWfile.move(backupUrl, toUrl, false);
				}
            }

            if (!success) {
                _log("Failed to move '" + fromUrl + "' to '" + toUrl + "' during undo.");
            }

            return success;
        }

		return {
			execute: _execute,
			undo: _undo
		};
	}

	// Object representing an update
	function Update(updateId) {
		var id = updateId;
		// URL to the direcotry where update's files are downloaded
		var updateDirURL = updatesDirURL + csInstallDir + csPathSeparator + id + csPathSeparator;
		// URL to the directory where back-up of original files 
		// which are modified during installation will be kept
		var backupDirURL = updatesDirURL + csUninstallDir + csPathSeparator + id + csPathSeparator;
		var configInUpdate = updateDirURL + csConfigDir + csPathSeparator;
		var configInBackup = backupDirURL + csConfigDir + csPathSeparator;
		var xmlInUpdate = updateDirURL + csUpdateXml;
		var xmlInBackup = backupDirURL + csUpdateXml;
		var failure = null;

		// Populate commands from "operation" nodes
		function _populateCommands(nodes, commands) {
			if (!nodes || !commands) {
				return;
			}

			nodes.forEach(
				function (node, index, array) {
					// Name of the command. Values are add, delete, replace
					var name = node.getAttribute(csNameAttr);
					// Param is path relative to Configuration directory
					var param = node.getAttribute(csParamAttr);

					if (name && (typeof name === "string") && name !== "" &&
							param && (typeof param === "string") && param !== "") {

						name = name.toLowerCase();

						if (name === csAddCommand) {
							// add --> move from update directory to config directory.
							commands.push(new MoveCommand(param, configInUpdate, configDirURL, configInBackup));

						} else if (name === csDeleteCommand) {
							// delete --> move from config directory to backup directory.
							commands.push(new MoveCommand(param, configDirURL, configInBackup, null));

						}
					}
				}
			);
		}
        
        // Eval scripts in param path in post-install or uninstall operations
        /* copyFile = true in the case when in post-installation step, 
           the uninstall script (if it exists) has to be copied to backup folder in User Config to execute on revert of that soft update. */
		function _evalCommands(nodes, isPostInstallStep, copyFile) {
			if (!nodes) {
				return;
			}

			nodes.forEach(
				function (node, index, array) {
					// Name of the command. Values are add, delete, eval
					var name = node.getAttribute(csNameAttr);
					// Param is path relative to Configuration directory
					var param = node.getAttribute(csParamAttr);

					if (name && (typeof name === "string") && name !== "" &&
							param && (typeof param === "string") && param !== "") {

						name = name.toLowerCase();
                        
                        if (name === csEvalCommand) {
                            var filePath;
                            if (isPostInstallStep) {
                                filePath = configInUpdate + param;
                            } else {
                                filePath = configInBackup + param;
                            }
                            if (DWfile.exists(filePath)) {
                                if (copyFile === undefined) {
                                    var text = DWfile.read(filePath);
                                    eval(text);
                                } else if (copyFile) {
                                    var destPath = configInBackup + param;
                                    DWfile.move(filePath, destPath, false);
                                }
                            }
                            
                        }
					}
				}
			);
		}

		// Install partially reverted update
		function _partialInstall() {
			var updateDom;
			var opsNodes;
			var opNodes;
			var cmds = [];
			var i;
			var success = true;
			var start = 0;

			// If previous revert operation failed, then only perform partial install
			if (!failure || failure.operation !== csRevertFailure) {
				return false;
			}

			start = failure.index + 1;
			failure = null;

			if (!DWfile.exists(xmlInBackup, false)) {
				return true;
			}

			_log("Installing partially reverted update with id " + id);

			updateDom = dw.getDocumentDOM(xmlInBackup);
			if (!updateDom) {
				_log("Error: Invalid update.xml");
				return false;
			}

			// 3. Populate and execute commands in "operations -> operation" tag
			opsNodes = updateDom.getElementsByTagName(csOperationsTag);
			if (opsNodes && opsNodes.length > 0) {

				opNodes = opsNodes[0].getElementsByTagName(csOperationTag);
				_populateCommands(opNodes, cmds);

				for (i = start; i < cmds.length; i++) {

					success = cmds[i].execute();
					if (!success) {
						break;
					}
				}
			}

			if (success) {
				// Delete update directory
				DWfile.remove(updateDirURL);
			}

			return success;
		}

		// Install the update
		function _install() {
			var updateDom;
			var opsNodes;
			var updateNodes;
			var mode;
			var postInstallNodes;
			var postUninstallNodes;
			var opNodes;
			var cmds = [];
			var postInstallCmds = [];
			var i, j;
			var success = true;
			var failNever = false;

			// If previous revert operation failed, then perform partial install
			if (failure && failure.operation === csRevertFailure) {
				return _partialInstall();
			}
			failure = null;

			// 1. If update.xml does not exist, it means update is already installed, return true.
			if (!DWfile.exists(xmlInUpdate, false)) {
				return true;
			}

			_log("Installing update with id " + id);

			updateDom = dw.getDocumentDOM(xmlInUpdate);
			if (!updateDom) {
				_log("Error: Invalid update.xml");
				return false;
			}

			// 2. Read update mode
			updateNodes = updateDom.getElementsByTagName(csUpdateTag);
			if (updateNodes && updateNodes.length > 0) {
				mode = updateNodes[0].getAttribute(csModeAttr);
				if (mode && mode === csFailNeverMode) {
					failNever = true;
				}
			}

			// 3. Populate and execute commands in "operations -> operation" tag
			opsNodes = updateDom.getElementsByTagName(csOperationsTag);
			if (opsNodes && opsNodes.length > 0) {

				opNodes = opsNodes[0].getElementsByTagName(csOperationTag);
				_populateCommands(opNodes, cmds);

				for (i = 0; i < cmds.length; i++) {

					success = (cmds[i].execute() || failNever);
					if (!success) {
						failure = {};
						failure.index = i;
						failure.operation = csInstallFailure;
						break;
					}
				}
			}

			if (success) {

				// 4. Perform post-install operations.
				// Populate and execute commands in "post-install -> operation" tag
				postInstallNodes = updateDom.getElementsByTagName(csPostInstallTag);

				if (postInstallNodes && postInstallNodes.length > 0) {

					opNodes = postInstallNodes[0].getElementsByTagName(csOperationTag);
					_populateCommands(opNodes, postInstallCmds); // only for add and delete operation names

					for (i = 0; i < postInstallCmds.length; i++) {
						postInstallCmds[i].execute();
					}
                    _evalCommands(opNodes, true); // for Eval operation name
				}
                
                // For performing post-uninstall operations, if any.
				// Copy the script to backup if present as param in "post-uninstall -> operation" tag 
				postUninstallNodes = updateDom.getElementsByTagName(csPostUninstallTag);

				if (postUninstallNodes && postUninstallNodes.length > 0) {

					opNodes = postUninstallNodes[0].getElementsByTagName(csOperationTag);
					_evalCommands(opNodes, true, true); // for Eval operation name
                    
				}

				// 5. Move the xml to backup directory
				DWfile.move(xmlInUpdate, xmlInBackup, false);

				// 6. Delete update directory
				DWfile.remove(updateDirURL);
			}

			return success;
		}

		// Revert partially installed update
		function _partialRevert() {
			var updateDom;
			var opsNodes;
			var opNodes;
			var cmds = [];
			var i;
			var success = true;
			var start = 0;

			// If previous install operation failed, then perform partial revert
			if (!failure || failure.operation !== csInstallFailure) {
				return false;
			}

			start = failure.index - 1;
			failure = null;

			if (!DWfile.exists(xmlInUpdate, false)) {
				return true;
			}

			_log("Reverting partially installed update with id " + id);

			updateDom = dw.getDocumentDOM(xmlInUpdate);
			if (!updateDom) {
				_log("Error: Invalid update.xml");
				return false;
			}

			// 3. Populate and execute commands in "operations -> operation" tag
			opsNodes = updateDom.getElementsByTagName(csOperationsTag);
			if (opsNodes && opsNodes.length > 0) {

				opNodes = opsNodes[0].getElementsByTagName(csOperationTag);
				_populateCommands(opNodes, cmds);

				// Undo in reverse order
				for (i = start; i >= 0; i--) {

					success = cmds[i].undo();
					if (!success) {
						break;
					}
				}
			}

			if (success) {

				// 6. Delete backup directory
				DWfile.remove(backupDirURL);
			}

			return success;
		}

		// Revert the update
		function _revert() {
			var updateDom;
			var opsNodes;
			var updateNodes;
			var mode;
			var postUninstallNodes;
			var opNodes;
			var cmds = [];
			var postUninstallCmds = [];
			var i, j;
			var success = true;
			var failNever = false;

			// If previous install operation failed, then perform partial revert
			if (failure && failure.operation === csInstallFailure) {
				return _partialRevert();
			}
			failure = null;

			// 1. If update.xml does not exist, it means update is already reverted, return true.
			if (!DWfile.exists(xmlInBackup, false)) {
				return true;
			}

			_log("Reverting update with id " + id);

			updateDom = dw.getDocumentDOM(xmlInBackup);
			if (!updateDom) {
				_log("Error: Invalid update.xml");
				return false;
			}

			// 2. Read update mode
			updateNodes = updateDom.getElementsByTagName(csUpdateTag);
			if (updateNodes && updateNodes.length > 0) {
				mode = updateNodes[0].getAttribute(csModeAttr);
				if (mode && mode === csFailNeverMode) {
					failNever = true;
				}
			}

			// 3. Populate and execute commands in "operations -> operation" tag
			opsNodes = updateDom.getElementsByTagName(csOperationsTag);
			if (opsNodes && opsNodes.length > 0) {

				opNodes = opsNodes[0].getElementsByTagName(csOperationTag);
				_populateCommands(opNodes, cmds);

				// Undo in reverse order
				for (i = cmds.length - 1; i >= 0; i--) {

					success = (cmds[i].undo() || failNever);
					if (!success) {
						failure = {};
						failure.index = i;
						failure.operation = csRevertFailure;
						break;
					}
				}
			}

			if (success) {

				// 4. Perform post-uninstall operations.
				// Populate and execute commands in "post-uninstall -> operation" tag
				postUninstallNodes = updateDom.getElementsByTagName(csPostUninstallTag);

				if (postUninstallNodes && postUninstallNodes.length > 0) {

					opNodes = postUninstallNodes[0].getElementsByTagName(csOperationTag);
					_populateCommands(opNodes, postUninstallCmds);

					// Execute in original order
					for (i = 0; i < postUninstallCmds.length; i++) {
						postUninstallCmds[i].execute();
					}
                    
                    _evalCommands(opNodes, false);
				}

				// 5. Move the xml to update directory
				DWfile.move(xmlInBackup, xmlInUpdate, false);

				// 6. Delete backup directory
				DWfile.remove(backupDirURL);
			}

			return success;
		}

		return {
			getId: function () {
				return id;
			},
			install: _install,
			revert: _revert
		};
	}

	function _cleanupPreInstall() {
		DWfile.remove(updatesActionFileURL);
	}

	// Populate updates
	function _populateUpdates(fileUrl, updates) {
		var updateNodes;
		var updatesDom;

		if (!updates) {
			return;
		}

		updatesDom = dw.getDocumentDOM(fileUrl);
		if (!updatesDom) {
			return;
		}

		updateNodes = updatesDom.getElementsByTagName(csUpdateTag);
		if (updateNodes) {
			updateNodes.forEach(
				function (updateNode, index, array) {
					var updateId = updateNode.getAttribute(csIdAttr);
					if (updateId) {
						updates.push(new Update(updateId));
					}
				}
			);
		}
	}

	// Modify provided xml by adding mentioned ids in specified order
	// If reverseOrder is false, idsToAdd are added in given order after existing ids
	// else idsToAdd are added in reverse order before existing ids
	function _modifyXml(xmlUrl, idsToAdd, reverseOrder) {
		var fileContents;
		var updatesDom;
		var updateNodes;
		var uRec;
		var id;
		var newIds = [];
		var i;

		// Return, if there is nothing to add
		if (!idsToAdd || idsToAdd.length === 0) {
			return;
		}

		// If reverseOrder is true, add ids in reverse order
		if (reverseOrder) {
			for (i = idsToAdd.length - 1; i >= 0; i--) {
				if (newIds.indexOf(idsToAdd[i]) === -1) {
					newIds.push(idsToAdd[i]);
				}
			}
		}

		// Get existing ids
		if (DWfile.exists(xmlUrl, false)) {
			updatesDom = dw.getDocumentDOM(xmlUrl);
			updateNodes = updatesDom.getElementsByTagName(csUpdateTag);

			if (updateNodes) {
				for (i = 0; i < updateNodes.length; i++) {
					id = updateNodes[i].getAttribute(csIdAttr);

					// Don't add id if it is added already
					if (id && (newIds.indexOf(id) === -1)) {
						newIds.push(id);
					}
				}
			}
		}

		// ids to be added
		if (!reverseOrder) {
			for (i = 0; i < idsToAdd.length; i++) {
				if (newIds.indexOf(idsToAdd[i]) === -1) {
					newIds.push(idsToAdd[i]);
				}
			}
		}

		// Start document
		fileContents = csXmlOpenNode;

		// Updates
		for (i = 0; i < newIds.length; i++) {
			uRec = csXmlUpdateRecord;
			uRec = uRec.replace(csIdReplacePattern, newIds[i]);
			fileContents = fileContents + uRec;
		}

		// End document
		fileContents = fileContents + csXmlCloseNode;

		// Write xml file to disk
		return DWfile.write(xmlUrl, fileContents);
	}

	// Apply all available updates
	function _applyUpdates() {
		var updates = [];
		var successfulIds = [];
		var success = true, fault = false;
		var i, j;

		// 1. Check if updates should be installed. 
		// If install.xml does not exist, it means no new updates are downloaded.
		if (!DWfile.exists(installXmlURL, false)) {
			return true;
		}

		_log("Installation process started.");

		// Clean before installation starts
		_cleanupPreInstall();

		// 2. Populate updates
		_populateUpdates(installXmlURL, updates);

		// 3. Install updates
		for (i = 0; i < updates.length; i++) {
			success = updates[i].install();
			if (!success) {
				// On failure, revert all currently applied updates starting with the update which failed.
				for (j = i; j >= 0; j--) {
					if (!updates[j].revert()) {
						// If not able to revert all updates, its a fault i.e. user config is in inconsistent state.
						fault = true;
						break;
					}
				}
				break;
			}
			successfulIds.push(updates[i].getId());
		}

		if (success) {

			// 4. Modify uninstall.xml with successful ids
			_modifyXml(uninstallXmlURL, successfulIds, false);

			// 5. If all updates are applied successfully, then delete install.xml.
			DWfile.remove(installXmlURL);

			// Showing notification at this time would increase Dreamweaver launch time.
			// So store it in preferences and the notification would be shown on idle.
			dw.setPreferenceString("Updates", "Status", "InstallSuccess");

			_log("Install completed successfully.");
		} else {

			// Remove Install directory on failure, otherwise on the next launch, Dreamweaver would try to install same updates.
			DWfile.remove(installDirURL);

			if (fault) {
				dw.setPreferenceString("Updates", "Status", "InstallFault");
				_log("=========================== ERROR ===========================");
				_log("Could not install updates successfully - could not restore original state. Troubleshooting the issue is highly recommended.");
				_log("=============================================================");

			} else {
				dw.setPreferenceString("Updates", "Status", "InstallFailure");
				_log("Could not install updates successfully - original state restored.");
			}
		}

		return true;
	}

	// Rollback all applied updates
	function _rollbackUpdates() {
		var updates = [];
		var successfulIds = [];
		var success = true, fault = false;
		var i, j;

		// 1. Check if updates should be reverted. 
		// If uninstall.xml does not exist, it means no updates were installed.
		if (!DWfile.exists(uninstallXmlURL, false)) {
			return true;
		}

		_log("Revert process started.");

		// 2. Populate updates
		_populateUpdates(uninstallXmlURL, updates);

		// 3. Revert updates in reverse order
		for (i = updates.length - 1; i >= 0; i--) {
			success = updates[i].revert();
			if (!success) {
				// On failure, restore all currently reverted updates starting with the update which failed.
				for (j = i; j < updates.length; j++) {
					if (!updates[j].install()) {
						// If not able to restore all updates, its a fault i.e. user config is in inconsistent state.
						fault = true;
						break;
					}
				}
				break;
			}
			successfulIds.push(updates[i].getId());
		}

		if (success) {

			// Following step is not required because install directory will be removed.
			// 4. Modify install.xml with successful ids.
			//_modifyXml(installXmlURL, successfulIds, true);

			// 5. If all updates are reverted successfully, then delete install and uninstall directory,
			// else on the next launch, now reverted updates would get installed again.
			DWfile.remove(installDirURL);
			DWfile.remove(uninstallDirURL);

			dw.setPreferenceString("Updates", "Status", "RevertSuccess");

            _log("Revert completed successfully.");
		} else {

			if (fault) {
				dw.setPreferenceString("Updates", "Status", "RevertFault");
				_log("=========================== ERROR ===========================");
				_log("Could not revert updates successfully - could not restore original state. Troubleshooting the issue is highly recommended.");
				_log("=============================================================");

			} else {
				dw.setPreferenceString("Updates", "Status", "RevertFailure");
				_log("Could not revert updates successfully - original state restored.");
			}
		}

		return true;
	}

	return {
		applyUpdates: _applyUpdates,
		rollbackUpdates: _rollbackUpdates
	};
}

function install() {
	var updater = new Updater();
	updater.applyUpdates();
}

function revert() {
	var updater = new Updater();
	updater.rollbackUpdates();
}

// Get id of the latest successfully installed update.
function getLatestUpdateId() {
	var uninstallXmlURL = dw.getUserConfigurationPath() + csUpdatesDir + csPathSeparator + csUninstallDir + csPathSeparator + csUninstallXml;
	var uninstallDOM;
	var updateNodes;
	var latestUpdate;
	var id;

	if (!DWfile.exists(uninstallXmlURL, false)) {
		return "0";
	}

	uninstallDOM = dw.getDocumentDOM(uninstallXmlURL);
	if (!uninstallDOM) {
		return "0";
	}

	updateNodes = uninstallDOM.getElementsByTagName("update");

	if (updateNodes && updateNodes.length > 0) {
		id = updateNodes[updateNodes.length - 1].getAttribute("id");
		if (id) {
			return id;
		}
	}
	return "0";
}
