/*******************************************************************************
 * Copyright (c) 2009, 2011 IBM Corporation and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.compare.patch;

import java.util.Arrays;
import java.util.Comparator;

import org.eclipse.compare.internal.core.patch.FilePatch2;
import org.eclipse.compare.internal.core.patch.Hunk;
import org.eclipse.core.runtime.IPath;

/**
 * Builder for creating IFilePatch2 and IHunk objects as well as building
 * relationship between them.
 *
 * @noextend This class is not intended to be subclassed by clients.
 * @noinstantiate This class is not intended to be instantiated by clients.
 *
 * @since org.eclipse.compare.core 3.5
 */
public class PatchBuilder {

	/**
	 * Line prefix used to mark context lines.
	 */
	public static final char CONTEXT_PREFIX = ' ';
	/**
	 * Line prefix used to mark an added lines.
	 */
	public static final char ADDITION_PREFIX = '+';
	/**
	 * Line prefix used to mark an removed lines.
	 */
	public static final char REMOVAL_PREFIX = '-';

	/**
	 * Creates an IHunk instance.
	 *
	 * @param start
	 *            the start position in the before file
	 * @param lines
	 *            content of the hunk. Each line starts with a control
	 *            character. Their meaning is as follows:
	 *            <ul>
	 *            <li>
	 *            '+': add the line
	 *            <li>
	 *            '-': delete the line
	 *            <li>
	 *            ' ': no change, context line
	 *            </ul>
	 * @return IHunk instance
	 */
	public static IHunk createHunk(int start, String[] lines) {
		int type = getHunkType(lines);
		int oldLength = getHunkLength(lines, true);
		int newLength = getHunkLength(lines, false);
		return new Hunk(null, type, start, oldLength, start, newLength, lines);
	}

	/**
	 * Creates an IFilePatch2 instance and performs recalculation of all hunks'
	 * after positions. Hunk's after position is position in the file state
	 * after applying a patch. It is affected by all the hunks that are to be
	 * applied before a given one. This recalculation is necessary to keep
	 * IFilePatch2's state coherent.
	 *
	 * @param oldPath
	 *            the path of the before state of the file
	 * @param oldDate
	 *            the timestamp of the before state of the file, see also
	 *            {@link IFilePatch2#DATE_UNKNOWN}
	 * @param newPath
	 *            the path of the after state of the file
	 * @param newDate
	 *            the timestamp of the after state of the file, see also
	 *            {@link IFilePatch2#DATE_UNKNOWN}
	 * @param hunks
	 *            a set of hunks to insert into IFilePatch2
	 * @return IFilePatch2 instance
	 */
	public static IFilePatch2 createFilePatch(IPath oldPath, long oldDate,
			IPath newPath, long newDate, IHunk[] hunks) {
		reorder(hunks);
		FilePatch2 fileDiff = new FilePatch2(oldPath, oldDate, newPath, newDate);
		for (IHunk hunk : hunks) {
			fileDiff.add((Hunk) hunk);
		}
		return fileDiff;
	}

	/**
	 * Adds IHunks to a given IFilePatch2 and performs recalculation of all
	 * hunks' after positions. Hunk's after position is position in the file
	 * state after applying a patch. It is affected by all the hunks that are to
	 * be applied before a given one. This recalculation is necessary to keep
	 * IFilePatch2's state coherent.
	 *
	 * @param filePatch
	 *            a file patch to add hunks to
	 * @param toAdd
	 *            a set of IHunks to add
	 * @return newly created file patch with added hunks
	 */
	public static IFilePatch2 addHunks(IFilePatch2 filePatch, IHunk[] toAdd) {
		IHunk[] result = addHunks(filePatch.getHunks(), toAdd);
		reorder(result);
		return createFilePatch(filePatch, result);
	}

	/**
	 * Removes IHunks from a given IFilePatch2 and performs recalculation of all
	 * hunks' after positions. Hunk's after position is position in the file
	 * state after applying a patch. It is affected by all the hunks that are to
	 * be applied before a given one. This recalculation is necessary to keep
	 * IFilePatch2's state coherent.
	 *
	 * @param filePatch
	 *            a file patch to add hunks to
	 * @param toRemove
	 *            a set of IHunks to add
	 * @return newly created file patch with removed hunks
	 */
	public static IFilePatch2 removeHunks(IFilePatch2 filePatch,
			IHunk[] toRemove) {
		IHunk[] result = removeHunks(filePatch.getHunks(), toRemove);
		reorder(result);
		return createFilePatch(filePatch, result);
	}

	private static IFilePatch2 createFilePatch(IFilePatch2 filePatch,
			IHunk[] hunks) {
		PatchConfiguration config = new PatchConfiguration();
		IPath beforePath = filePatch.getTargetPath(config);
		config.setReversed(true);
		IPath afterPath = filePatch.getTargetPath(config);
		return createFilePatch(beforePath, filePatch.getBeforeDate(),
				afterPath, filePatch.getAfterDate(), hunks);
	}

	private static int getHunkType(String[] lines) {
		boolean hasContextLines = checkForPrefix(CONTEXT_PREFIX, lines);
		if (!hasContextLines) {
			boolean hasLineAdditions = checkForPrefix(ADDITION_PREFIX, lines);
			boolean hasLineDeletions = checkForPrefix(REMOVAL_PREFIX, lines);
			if (hasLineAdditions && !hasLineDeletions) {
				return FilePatch2.ADDITION;
			} else if (!hasLineAdditions && hasLineDeletions) {
				return FilePatch2.DELETION;
			}
		}
		return FilePatch2.CHANGE;
	}

	private static int getHunkLength(String[] lines, boolean old) {
		int length = 0;
		for (String line : lines) {
			if (line.length() > 0) {
				switch (line.charAt(0)) {
				case ' ':
					length++;
					break;
				case '+':
					if (!old) {
						length++;
					}
					break;
				case '-':
					if (old) {
						length++;
					}
					break;
				default:
					throw new IllegalArgumentException(""); //$NON-NLS-1$
				}
			}
		}
		return length;
	}

	private static boolean checkForPrefix(char prefix, String[] lines) {
		for (String line : lines) {
			if (line.length() > 0) {
				if (line.charAt(0) == prefix) {
					return true;
				}
			}
		}
		return false;
	}

	private static IHunk[] addHunks(IHunk[] hunks, IHunk[] toAdd) {
		IHunk[] ret = new IHunk[hunks.length + toAdd.length];
		System.arraycopy(hunks, 0, ret, 0, hunks.length);
		System.arraycopy(toAdd, 0, ret, hunks.length, toAdd.length);
		return ret;
	}

	private static IHunk[] removeHunks(IHunk[] hunks, IHunk[] toRemove) {
		int removed = 0;
		for (int i = 0; i < hunks.length; i++) {
			for (IHunk r : toRemove) {
				if (r == hunks[i]) {
					hunks[i] = null;
					removed++;
				}
			}
		}
		IHunk[] ret = new IHunk[hunks.length - removed];
		for (int i = 0, j = 0; i < hunks.length; i++) {
			if (hunks[i] != null) {
				ret[j++] = hunks[i];
			}
		}
		return ret;
	}

	private static void reorder(IHunk[] hunks) {
		Arrays.sort(hunks, new HunkComparator());
		int shift = 0;
		for (IHunk h : hunks) {
			Hunk hunk = (Hunk) h;
			int start = hunk.getStart(false) + shift;
			hunk.setStart(start, true);
			shift += hunk.getLength(true) - hunk.getLength(false);
		}
	}

	static class HunkComparator implements Comparator<IHunk> {
		@Override
		public int compare(IHunk arg0, IHunk arg1) {
			if ((arg0 != null && arg0 instanceof Hunk)
					&& (arg1 != null && arg1 instanceof Hunk)) {
				Hunk hunk0 = (Hunk) arg0;
				Hunk hunk1 = (Hunk) arg1;
				int shift = hunk0.getStart(true) - hunk1.getStart(true);
				return shift;
			}
			return 0;
		}
	}

}