/**
 * Copyright 2019 Association for the promotion of open-source insurance software and for the establishment of open interface standards in the insurance industry (Verein zur Förderung quelloffener Versicherungssoftware und Etablierung offener Schnittstellenstandards in der Versicherungsbranche)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * 	http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
/**
 * 
 */
package org.aposin.mergeprocessor.model.svn;

import java.io.File;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.apache.commons.lang3.exception.ExceptionUtils;
import org.aposin.mergeprocessor.application.ApplicationUtil;
import org.aposin.mergeprocessor.configuration.Configuration;
import org.aposin.mergeprocessor.exception.SftpUtilException;
import org.aposin.mergeprocessor.exception.SvnUtilException;
import org.aposin.mergeprocessor.model.MergeUnitStatus;
import org.aposin.mergeprocessor.model.svn.ISvnClient.SvnClientException;
import org.aposin.mergeprocessor.utils.E4CompatibilityUtil;
import org.aposin.mergeprocessor.utils.LogUtil;
import org.aposin.mergeprocessor.utils.MergeProcessorUtil;
import org.aposin.mergeprocessor.utils.Messages;
import org.aposin.mergeprocessor.utils.SftpUtil;
import org.aposin.mergeprocessor.utils.SvnUtil;
import org.aposin.mergeprocessor.view.MessageDialogScrollable;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.e4.ui.di.UISynchronize;
import org.eclipse.jface.dialogs.ProgressMonitorDialog;
import org.eclipse.osgi.util.NLS;

/**
 * Utility class for merging in SVN.
 * 
 * @author Stefan Weiser
 *
 */
public class SVNMergeUtil {

	private static final Logger LOGGER = Logger.getLogger(SVNMergeUtil.class.getName());

	private SVNMergeUtil() {
		// Only static access
	}

	/**
	 * Lists all available branches for the repository of the given merge unit.
	 * 
	 * @param mergeUnit the merge unit
	 * @return a list of all available branches
	 */
	public static List<String> listBranches(SVNMergeUnit mergeUnit) {
		final String rootUrl = SvnUtil.getRepositoryRootOfUrl(mergeUnit.getUrlSource());
		try {
			final List<String> branches = SvnUtil.listDirectories(rootUrl + '/' + "branches");
			branches.add("trunk");
			Collections.reverse(branches);
			return branches;
		} catch (Exception e) {
			LogUtil.throwing(e);
			return Collections.emptyList();
		}
	}

	public static boolean merge(ProgressMonitorDialog pmd, IProgressMonitor monitor, SVNMergeUnit mergeUnit)
			throws SvnClientException {
		LogUtil.entering(pmd, monitor, mergeUnit);

		String taskName = String.format(Messages.MergeProcessorUtil_Process_TaskName, mergeUnit.getRepository(),
				mergeUnit.getRevisionInfo(), mergeUnit.getBranchSource(), mergeUnit.getBranchTarget());
		monitor.beginTask(taskName, 9);

		boolean cancel = false;
		final ISvnClient client = E4CompatibilityUtil.getApplicationContext().get(ISvnClient.class);

		if (!cancel) {
			monitor.subTask("copyRemoteToLocal"); //$NON-NLS-1$
			cancel = copyRemoteToLocal(mergeUnit);
			monitor.worked(1);
			if (monitor.isCanceled()) {
				LOGGER.fine("User cancelled after 'copyRemoteToLocal'."); //$NON-NLS-1$
				cancel = true;
			}
		}

		if (!cancel) {
			monitor.subTask("buildMinimalWorkingCopy"); //$NON-NLS-1$
			cancel = buildMinimalSVNWorkingCopy((SVNMergeUnit) mergeUnit, client);
			monitor.worked(1);
			if (monitor.isCanceled()) {
				LOGGER.fine("User cancelled after 'buildMinimalWorkingCopy'."); //$NON-NLS-1$
				cancel = true;
			}
		}

		if (!cancel) {
			monitor.subTask("mergeChangesIntoWorkingCopy"); //$NON-NLS-1$
			cancel = mergeChangesIntoWorkingCopy(mergeUnit, client);
			monitor.worked(1);
			if (monitor.isCanceled()) {
				LOGGER.fine("User cancelled after 'mergeChangesIntoWorkingCopy'."); //$NON-NLS-1$
				cancel = true;
			}
		}

		if (!cancel) {
			monitor.subTask("checkIsCommittable"); //$NON-NLS-1$
			cancel = checkIsCommittable(mergeUnit, client);
			monitor.worked(1);
			if (monitor.isCanceled()) {
				LOGGER.fine("User cancelled after 'checkIsCommittable'."); //$NON-NLS-1$
				cancel = true;
			}
		}

		if (!cancel) {
			monitor.subTask("commitChanges"); //$NON-NLS-1$

			// can't cancel after commit
			pmd.setCancelable(false);

			cancel = commitChanges(mergeUnit, client);
			monitor.worked(1);
		}

		if (!cancel) {
			monitor.subTask("copyLocalToDone"); //$NON-NLS-1$
			cancel = copyLocalToDone(mergeUnit);
			monitor.worked(1);
		}

		if (cancel) {
			mergeUnit.setStatus(MergeUnitStatus.CANCELLED);
		}

		// delete the local merge file, even if cancelled.
		monitor.subTask("deleteLocal"); //$NON-NLS-1$
		deleteLocal(mergeUnit);
		monitor.worked(1);
		return LogUtil.exiting(cancel);
	}

	private static boolean copyRemoteToLocal(SVNMergeUnit mergeUnit) {
		LogUtil.entering(mergeUnit);

		boolean cancel = false;
		while (!cancel) {
			try {
				SftpUtil.getInstance().copyMergeUnitToWork(mergeUnit);
			} catch (SftpUtilException e) {
				LOGGER.log(Level.WARNING, "Caught exception while copying to work.", e); //$NON-NLS-1$

				String message = Messages.MergeProcessorUtil_CopyRemoteToLocal_Error_Message;
				String messageScrollable = String.format(
						Messages.MergeProcessorUtil_CopyRemoteToLocal_Error_MessageScrollable,
						Configuration.getPathLocalMergeFile(mergeUnit), e.getMessage());
				if (MergeProcessorUtil.bugUserToFixProblem(message, messageScrollable)) {
					// user wants to retry
					LOGGER.fine(String.format("User wants to retry copying the mergeUnit=%s to working directory.", //$NON-NLS-1$
							mergeUnit));
					continue;
				} else {
					// user didn't say 'retry' so we cancel the whole merge...
					LOGGER.fine(String.format(
							"User cancelled processing mergeUnit=%s when copying merge file to working directory.", //$NON-NLS-1$
							mergeUnit));
					cancel = true;
				}
			}
			break;
		}

		return LogUtil.exiting(cancel);
	}

	private static boolean buildMinimalSVNWorkingCopy(SVNMergeUnit mergeUnit, ISvnClient client) {
		LogUtil.entering(mergeUnit);

		boolean cancel = false;
		while (!cancel) {
			try {
				cancel = SvnUtil.buildMinimalWorkingCopy(mergeUnit, client);
				if (cancel) {
					LOGGER.fine(() -> String.format("User cancelled building minimal working copy for mergeUnit=%s.", //$NON-NLS-1$
							mergeUnit));
				}
			} catch (SvnUtilException e) {
				LOGGER.log(Level.WARNING, "Caught exception while building minimal working copy.", e); //$NON-NLS-1$
				String message = Messages.MergeProcessorUtil_BuildMinimalWorkingCopy_Error_Message;
				String messageScrollable = String.format(
						Messages.MergeProcessorUtil_BuildMinimalWorkingCopy_Error_MessageScrollable_Prefix,
						ExceptionUtils.getStackTrace(e));
				if (MergeProcessorUtil.bugUserToFixProblem(message, messageScrollable)) {
					// user wants to retry
					LOGGER.fine(String.format("User wants to retry building the minimal working copy for mergeUnit=%s.", //$NON-NLS-1$
							mergeUnit));
					continue;
				} else {
					// user didn't say 'retry' so we cancel the whole merge...
					LOGGER.fine(
							String.format("User cancelled building minimal working copy for mergeUnit=%s.", mergeUnit)); //$NON-NLS-1$
					cancel = true;
				}
			}
			break;
		}

		return LogUtil.exiting(cancel);
	}

	private static boolean mergeChangesIntoWorkingCopy(final SVNMergeUnit mergeUnit, final ISvnClient client) {
		LogUtil.entering(mergeUnit);

		boolean cancel = false;
		boolean success = false;
		while (!cancel && !success) {
			try {
				SvnUtil.mergeChanges(mergeUnit, client);
				success = true;
			} catch (SvnUtilException e) {
				LOGGER.log(Level.WARNING, "Caught exception while merging changes into working copy.", e); //$NON-NLS-1$
				String messageScrollable = NLS.bind(
						Messages.MergeProcessorUtil_MergeChangesIntoWorkingCopy_Error_Message_Prefix, e.getMessage());
				if (MergeProcessorUtil.bugUserToFixProblem(
						Messages.MergeProcessorUtil_MergeChangesIntoWorkingCopy_Error_Title, messageScrollable)) {
					// user wants to retry
					LOGGER.fine(String.format("User wants to retry merging changes into working copy for mergeUnit=%s.", //$NON-NLS-1$
							mergeUnit));
					continue;
				} else {
					// user didn't say 'retry' so we cancel the whole merge...
					LOGGER.fine(String.format("User cancelled merging changes into working copy for mergeUnit=%s.", //$NON-NLS-1$
							mergeUnit));
					cancel = true;
				}
			}
		}

		if (!success) {
			// if we haven't had success we cancel. we can't continue without success.
			cancel = true;
		}

		return LogUtil.exiting(cancel);
	}

	private static boolean commitChanges(SVNMergeUnit mergeUnit, final ISvnClient client) {
		LogUtil.entering(mergeUnit);
		boolean cancel = false;
		while (!cancel) {
			try {
				SvnUtil.commitChanges(mergeUnit, client);
				break;
			} catch (SvnUtilException e) {
				LOGGER.log(Level.FINE, "Caught exception while committing changes.", e); //$NON-NLS-1$

				String[] conflicts;
				try {
					conflicts = SvnUtil.conflictsOfWorkingCopy(client);
				} catch (SvnUtilException e2) {
					LOGGER.log(Level.FINE, "Caught exception checking working copy for conflicts.", e2); //$NON-NLS-1$
					String messageScrollable = NLS.bind(
							Messages.MergeProcessorUtil_MergeProcessorUtil_CommitChanges_Conflicts_Error_Message,
							ExceptionUtils.getStackTrace(e2), ExceptionUtils.getStackTrace(e));
					if (MergeProcessorUtil.bugUserToFixProblem(
							Messages.MergeProcessorUtil_CommitChanges_Conflicts_Error_Title, messageScrollable)) {
						// user wants to retry
						LOGGER.fine(String.format(
								"User wants to retry checking the working copy for conflicts. mergeUnit=%s.", //$NON-NLS-1$
								mergeUnit));
						continue;
					} else {
						// user didn't say 'retry' so we cancel the whole merge...
						LOGGER.fine(
								String.format("User cancelled checking the working copy for conflicts. mergeUnit=%s.", //$NON-NLS-1$
										mergeUnit));
						cancel = true;
						break;
					}
				}

				boolean isUpdateRequired = false;
				{
					String[] lines = ExceptionUtils.getStackTrace(e).split("\n"); //$NON-NLS-1$
					for (String line : lines) {
						line = line.trim();
						if (line.endsWith("' is out of date") || line.endsWith("' is out of date; try updating") //$NON-NLS-1$ //$NON-NLS-2$
								|| line.endsWith("resource out of date; try updating")) { //$NON-NLS-1$
							isUpdateRequired = true;
							break;
						}
					}
				}

				if (isUpdateRequired) {
					LOGGER.fine("Working copy requires an update to commit the changes."); //$NON-NLS-1$
					try {
						SvnUtil.update(client);
					} catch (SvnUtilException e1) {
						LOGGER.log(Level.FINE, "Caught exception while updating working copy.", e1); //$NON-NLS-1$

						String messageScrollable = NLS.bind(
								Messages.MergeProcessorUtil_MergeProcessorUtil_CommitChanges_Update_Error_Details,
								ExceptionUtils.getStackTrace(e), ExceptionUtils.getStackTrace(e1));
						if (MergeProcessorUtil.bugUserToFixProblem(
								Messages.MergeProcessorUtil_CommitChanges_Update_Error_Title, messageScrollable)) {
							// user wants to retry
							LOGGER.fine(String.format(
									"User wants to retry updating the working copy before committing. mergeUnit=%s.", //$NON-NLS-1$
									mergeUnit));
							continue;
						} else {
							// user didn't say 'retry' so we cancel the whole merge...
							LOGGER.fine(String.format(
									"User cancelled updating the working copy before committing. mergeUnit=%s.", //$NON-NLS-1$
									mergeUnit));
							cancel = true;
						}
					}
				} else if (conflicts.length > 0) {
					LOGGER.fine("Working copy has conflicts."); //$NON-NLS-1$
					cancel = checkIsCommittable(mergeUnit, client);
				} else {
					String messageScrollable = NLS.bind(Messages.MergeProcessorUtil_CommitChanges_Commit_Error_Message,
							e.getMessage(), ExceptionUtils.getStackTrace(e));
					if (MergeProcessorUtil.bugUserToFixProblem(
							Messages.MergeProcessorUtil_CommitChanges_Commit_Error_Title, messageScrollable)) {
						LOGGER.fine(String.format(
								"User wants to retry committing changes from the working copy. mergeUnit=%s.", //$NON-NLS-1$
								mergeUnit));
						continue;
					} else {
						LOGGER.fine(
								String.format("User cancelled committing changes from the working copy. mergeUnit=%s.", //$NON-NLS-1$
										mergeUnit));
						cancel = true;
					}
				}
			}
		}

		return LogUtil.exiting(cancel);
	}

	private static boolean checkIsCommittable(final SVNMergeUnit mergeUnit, final ISvnClient client) {
		LogUtil.entering(mergeUnit);

		boolean cancel = false;
		String[] conflicts = null;
		CONFLICTS: while (!cancel) {
			try {
				conflicts = SvnUtil.conflictsOfWorkingCopy(client);
			} catch (SvnUtilException e) {
				LOGGER.log(Level.WARNING, "Caught exception while checking if working copy is in a committable state.", //$NON-NLS-1$
						e);

				String messageScrollable = NLS.bind(Messages.MergeProcessorUtil_CheckIsCommittable_Error_Message_Prefix,
						ExceptionUtils.getStackTrace(e));
				if (MergeProcessorUtil.bugUserToFixProblem(Messages.MergeProcessorUtil_CheckIsCommittable_Error_Title,
						messageScrollable)) {
					// user wants to retry
					LOGGER.fine(
							String.format("User wants to retry checking the working copy for problems. mergeUnit=%s.", //$NON-NLS-1$
									mergeUnit));
					continue;
				} else {
					// user didn't say 'retry' so we cancel the whole merge...
					LOGGER.fine(String.format("User cancelled checking working copy for problems. mergeUnit=%s.", //$NON-NLS-1$
							mergeUnit));
					cancel = true;
				}
			}

			if (conflicts == null || conflicts.length == 0) {
				LOGGER.fine("Workspace has no conflicts."); //$NON-NLS-1$
				break;
			} else {
				int choice = 1;
				while (choice == 1) {
					final String stringConflicts;
					{
						StringBuilder sbConflicts = new StringBuilder(
								Messages.MergeProcessorUtil_CheckIsCommittable_Conflicts_Prefix);
						for (String conflict : conflicts) {
							sbConflicts.append('\t' + conflict + '\n');
						}
						stringConflicts = sbConflicts.toString();
					}

					final String[] choices = new String[] { Choices.CANCEL.name, Choices.RETRY.name,
							Choices.OPEN_WORKING_COPY.name, Choices.OPEN_TORTOISE_SVN.name };
					{
						final AtomicInteger retVal = new AtomicInteger();

						String dialogTitle = Messages.MergeProcessorUtil_CheckIsCommittable_Conflicts_Title;
						String dialogMessage = String.format(
								Messages.MergeProcessorUtil_CheckIsCommittable_Conflicts_DialogMessage,
								mergeUnit.getRevisionInfo(), mergeUnit.getBranchSource(), mergeUnit.getBranchTarget());

						E4CompatibilityUtil.getApplicationContext().get(UISynchronize.class).syncExec(() -> {
							MessageDialogScrollable dialog = new MessageDialogScrollable(
									ApplicationUtil.getApplicationShell(), dialogTitle, null, dialogMessage,
									stringConflicts, MessageDialogScrollable.INFORMATION, choices, 0);

							retVal.set(dialog.open());
						});
						choice = retVal.get();
					}

					switch (Choices.forName(choices[choice])) {
					case CANCEL:
						// cancel the whole merge
						LOGGER.fine("User cancelled checking working copy for problems."); //$NON-NLS-1$
						cancel = true;
						break;
					case OPEN_WORKING_COPY:
						LOGGER.fine(() -> String.format("User wants to open the working copy=%s.", //$NON-NLS-1$
								Configuration.getPathSvnWorkingCopy()));
						MergeProcessorUtil.openFile(Configuration.getPathSvnWorkingCopy());
						break;
					case OPEN_TORTOISE_SVN:
						// Open TortoiseSVN
						LOGGER.fine("User wants to open TortoiseSVN."); //$NON-NLS-1$
						try {
							SvnUtil.openTortoiseSVN(Configuration.getPathSvnWorkingCopy());
						} catch (final SvnUtilException e) {
							LOGGER.log(Level.SEVERE, "Caught exception while opening TortoiseSVN.", e); //$NON-NLS-1$
							E4CompatibilityUtil.getApplicationContext().get(UISynchronize.class).syncExec(() -> {
								String message = NLS.bind(
										Messages.MergeProcessorUtil_CheckIsCommittable_TortoiseSvn_Error_Message_Prefix,
										ExceptionUtils.getStackTrace(e));
								MessageDialogScrollable.openError(ApplicationUtil.getApplicationShell(),
										Messages.MergeProcessorUtil_CheckIsCommittable_TortoiseSvn_Error_Title,
										message);
							});
						}
						break;
					case RETRY:
						LOGGER.fine("User wants to repeat checking working copy for problems."); //$NON-NLS-1$
						// repeat check.
						break CONFLICTS;
					default:
						if (LOGGER.isLoggable(Level.FINE)) {
							LOGGER.fine(String.format("Unknown choice. Repeating. choices=%s", //$NON-NLS-1$
									choices[choice]));
						}
						// repeat check.
						break CONFLICTS;
					}
				}
			}
		}

		return LogUtil.exiting(cancel);
	}

	public static boolean copyLocalToDone(SVNMergeUnit mergeUnit) {
		LogUtil.entering(mergeUnit);

		boolean cancel = false;
		while (!cancel) {
			try {
				SftpUtil.getInstance().copyMergeUnitFromWorkToDoneAndDeleteInTodo(mergeUnit);
				mergeUnit.setStatus(MergeUnitStatus.DONE);
				break;
			} catch (SftpUtilException e) {
				String logMessage = String.format("Caught exception while copying from work to done. mergeUnit=[%s]", //$NON-NLS-1$
						mergeUnit);
				LOGGER.log(Level.WARNING, logMessage, e);

				String message = NLS.bind(Messages.MergeProcessorUtil_CopyLocalToDone_Error_Message, Choices.CANCEL,
						MergeUnitStatus.TODO);
				String messageScrollable = NLS.bind(Messages.MergeProcessorUtil_CopyLocalToDone_Error_Details,
						e.getMessage());
				if (MergeProcessorUtil.bugUserToFixProblem(message, messageScrollable)) {
					// user wants to retry
					LOGGER.fine(String.format("User wants to retry copying the merge file to the server. mergeUnit=%s.", //$NON-NLS-1$
							mergeUnit));
					continue;
				} else {
					// user didn't say 'retry' so we cancel the whole merge...
					LOGGER.fine(String.format("User cancelled committing changes from the working copy. mergeUnit=%s.", //$NON-NLS-1$
							mergeUnit));
					cancel = true;
				}
			}
		}

		return LogUtil.exiting(cancel);
	}

	private static void deleteLocal(SVNMergeUnit mergeUnit) {
		LogUtil.entering(mergeUnit);

		File file = new File(Configuration.getPathLocalMergeFile(mergeUnit));
		LOGGER.fine(() -> String.format("Deleting local merge file=%s.", file.getAbsolutePath())); //$NON-NLS-1$
		boolean success = !file.exists();
		while (!success) {
			success = file.delete();
			if (!success) {
				String messageScrollable = NLS.bind(Messages.MergeProcessorUtil_DeleteLocal_Error_Message,
						file.getAbsolutePath());
				if (MergeProcessorUtil.bugUserToFixProblem(Messages.MergeProcessorUtil_DeleteLocal_Error_Title,
						messageScrollable)) {
					LOGGER.fine(() -> String.format("User wants to retry deleting local merge file. mergeUnit=%s.", //$NON-NLS-1$
							mergeUnit));
				} else {
					LOGGER.fine(
							() -> String.format("User cancelled deleting local merge file. mergeUnit=%s.", mergeUnit)); //$NON-NLS-1$
					success = true;
				}
			}
		}
		LogUtil.exiting();
	}

	/**
	 * Enum indicating the user choice when doing merge.
	 */
	private enum Choices {
		CANCEL(Messages.MergeProcessorUtil_CheckIsCommittable_Conflicts_Cancel),
		RETRY(Messages.MergeProcessorUtil_CheckIsCommittable_Conflicts_Retry),
		OPEN_WORKING_COPY(Messages.MergeProcessorUtil_CheckIsCommittable_Conflicts_OpenWorkingCopy),
		OPEN_TORTOISE_SVN(Messages.MergeProcessorUtil_CheckIsCommittable_Conflicts_OpenTortoiseSvn);

		private final String name;

		private Choices(String name) {
			this.name = name;
		}

		private static Choices forName(String name) {
			return Arrays.stream(Choices.values()).filter((choice) -> choice.name.equals(name)).findFirst().orElseThrow(
					() -> new RuntimeException(NLS.bind(Messages.MergeProcessorUtil_Choices_Invalid, name)));
		}

		@Override
		public String toString() {
			return name;
		}
	}

}