/*******************************************************************************
 * Copyright (c) 2000, 2019 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.jface.text;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.NoSuchElementException;

import org.eclipse.swt.events.VerifyEvent;

import org.eclipse.core.runtime.Assert;


/**
 * Represents a text modification as a document replace command. The text
 * modification is given as a {@link org.eclipse.swt.events.VerifyEvent} and
 * translated into a document replace command relative to a given offset. A
 * document command can also be used to initialize a given
 * <code>VerifyEvent</code>.
 * <p>
 * A document command can also represent a list of related changes.</p>
 */
public class DocumentCommand {

	/**
	 * A command which is added to document commands.
	 * @since 2.1
	 */
	private static class Command implements Comparable<Command> {
		/** The offset of the range to be replaced */
		private final int fOffset;
		/** The length of the range to be replaced. */
		private final int fLength;
		/** The replacement text */
		private final String fText;
		/** The listener who owns this command */
		private final IDocumentListener fOwner;

		/**
		 * Creates a new command with the given specification.
		 *
		 * @param offset the offset of the replace command
		 * @param length the length of the replace command
		 * @param text the text to replace with, may be <code>null</code>
		 * @param owner the document command owner, may be <code>null</code>
		 * @since 3.0
		 */
		public Command(int offset, int length, String text, IDocumentListener owner) {
			if (offset < 0 || length < 0)
				throw new IllegalArgumentException();
			fOffset= offset;
			fLength= length;
			fText= text;
			fOwner= owner;
		}

		/**
		 * Executes the document command on the specified document.
		 *
		 * @param document the document on which to execute the command.
		 * @throws BadLocationException in case this commands cannot be executed
		 */
		public void execute(IDocument document) throws BadLocationException {

			if (fLength == 0 && fText == null)
				return;

			if (fOwner != null)
				document.removeDocumentListener(fOwner);

			document.replace(fOffset, fLength, fText);

			if (fOwner != null)
				document.addDocumentListener(fOwner);
		}

		@Override
		public int compareTo(Command object) {
			if (isEqual(object))
				return 0;

			Command command= object;

			// diff middle points if not intersecting
			if (fOffset + fLength <= command.fOffset || command.fOffset + command.fLength <= fOffset) {
				int value= (2 * fOffset + fLength) - (2 * command.fOffset + command.fLength);
				if (value != 0)
					return value;
			}
			// the answer
			return 42;
		}

		private boolean isEqual(Object object) {
			if (object == this)
				return true;
			if (!(object instanceof Command))
				return false;
			final Command command= (Command) object;
			return command.fOffset == fOffset && command.fLength == fLength;
		}
	}

	/**
	 * An iterator, which iterates in reverse over a list.
	 *
	 * @param <E> the type of elements returned by this iterator
	 */
	private static class ReverseListIterator<E> implements Iterator<E> {

		/** The list iterator. */
		private final ListIterator<E> fListIterator;

		/**
		 * Creates a reverse list iterator.
		 * @param listIterator the iterator that this reverse iterator is based upon
		 */
		public ReverseListIterator(ListIterator<E> listIterator) {
			if (listIterator == null)
				throw new IllegalArgumentException();
			fListIterator= listIterator;
		}

		@Override
		public boolean hasNext() {
			return fListIterator.hasPrevious();
		}

		@Override
		public E next() {
			return fListIterator.previous();
		}

		@Override
		public void remove() {
			throw new UnsupportedOperationException();
		}
	}

	/**
	 * A command iterator.
	 */
	private static class CommandIterator implements Iterator<Command> {

		/** The command iterator. */
		private final Iterator<Command> fIterator;

		/** The original command. */
		private Command fCommand;

		/** A flag indicating the direction of iteration. */
		private boolean fForward;

		/**
		 * Creates a command iterator.
		 *
		 * @param commands an ascending ordered list of commands
		 * @param command the original command
		 * @param forward the direction
		 */
		public CommandIterator(final List<Command> commands, final Command command, final boolean forward) {
			if (commands == null || command == null)
				throw new IllegalArgumentException();
			fIterator= forward ? commands.iterator() : new ReverseListIterator<>(commands.listIterator(commands.size()));
			fCommand= command;
			fForward= forward;
		}

		@Override
		public boolean hasNext() {
			return fCommand != null || fIterator.hasNext();
		}

		@Override
		public Command next() {

			if (!hasNext())
				throw new NoSuchElementException();

			if (fCommand == null)
				return fIterator.next();

			if (!fIterator.hasNext()) {
				final Command tempCommand= fCommand;
				fCommand= null;
				return tempCommand;
			}

			final Command command= fIterator.next();
			final int compareValue= command.compareTo(fCommand);

			if ((compareValue < 0) ^ !fForward) {
				return command;

			} else if ((compareValue > 0) ^ !fForward) {
				final Command tempCommand= fCommand;
				fCommand= command;
				return tempCommand;

			} else {
				throw new IllegalArgumentException();
			}
		}

		@Override
		public void remove() {
			throw new UnsupportedOperationException();
		}
	}

	/** Must the command be updated */
	public boolean doit= false;
	/** The offset of the command. */
	public int offset;
	/** The length of the command */
	public int length;
	/** The text to be inserted */
	public String text;
	/**
	 * The owner of the document command which will not be notified.
	 * @since 2.1
	 */
	public IDocumentListener owner;
	/**
	 * The caret offset with respect to the document before the document command is executed.
	 * @since 2.1
	 */
	public int caretOffset;
	/**
	 * Additional document commands.
	 * @since 2.1
	 */
	private final List<Command> fCommands= new ArrayList<>();
	/**
	 * Indicates whether the caret should be shifted by this command.
	 * @since 3.0
	 */
	public boolean shiftsCaret;

	ITextSelection fSelection;

	/**
	 * Creates a new document command.
	 */
	protected DocumentCommand() {
	}

	/**
	 * Translates a verify event into a document replace command using the given offset.
	 *
	 * @param event the event to be translated
	 * @param modelRange the event range as model range
	 */
	void setEvent(VerifyEvent event, IRegion modelRange) {

		doit= true;
		text= event.text;

		offset= modelRange.getOffset();
		length= modelRange.getLength();

		owner= null;
		caretOffset= -1;
		shiftsCaret= true;
		fCommands.clear();
	}

	/**
	 * Fills the given verify event with the replace text and the <code>doit</code>
	 * flag of this document command. Returns whether the document command
	 * covers the same range as the verify event considering the given offset.
	 *
	 * @param event the event to be changed
	 * @param modelRange to be considered for range comparison
	 * @return <code>true</code> if this command and the event cover the same range
	 */
	boolean fillEvent(VerifyEvent event, IRegion modelRange) {
		event.text= text;
		event.doit= (offset == modelRange.getOffset() && length == modelRange.getLength() && doit && caretOffset == -1);
		return event.doit;
	}

	/**
	 * Adds an additional replace command. The added replace command must not overlap
	 * with existing ones. If the document command owner is not <code>null</code>, it will not
	 * get document change notifications for the particular command.
	 *
	 * @param commandOffset the offset of the region to replace
	 * @param commandLength the length of the region to replace
	 * @param commandText the text to replace with, may be <code>null</code>
	 * @param commandOwner the command owner, may be <code>null</code>
	 * @throws BadLocationException if the added command intersects with an existing one
	 * @since 2.1
	 */
	public void addCommand(int commandOffset, int commandLength, String commandText, IDocumentListener commandOwner) throws BadLocationException {
		final Command command= new Command(commandOffset, commandLength, commandText, commandOwner);

		if (intersects(command))
			throw new BadLocationException();

		final int index= Collections.binarySearch(fCommands, command);

		// a command with exactly the same ranges exists already
		if (index >= 0)
			throw new BadLocationException();

		// binary search result is defined as (-(insertionIndex) - 1)
		final int insertionIndex= -(index + 1);

		// overlaps to the right?
		if (insertionIndex != fCommands.size() && intersects(fCommands.get(insertionIndex), command))
			throw new BadLocationException();

		// overlaps to the left?
		if (insertionIndex != 0 && intersects(fCommands.get(insertionIndex - 1), command))
			throw new BadLocationException();

		fCommands.add(insertionIndex, command);
	}

	/**
	 * Returns an iterator over the commands in ascending position order.
	 * The iterator includes the original document command.
	 * Commands cannot be removed.
	 *
	 * @return returns the command iterator
	 */
	public Iterator<Command> getCommandIterator() {
		Command command= new Command(offset, length, text, owner);
		return new CommandIterator(fCommands, command, true);
	}

	/**
	 * Returns the number of commands including the original document command.
	 *
	 * @return returns the number of commands
	 * @since 2.1
	 */
	public int getCommandCount() {
		return 1 + fCommands.size();
	}

	/**
	 * Returns whether the two given commands intersect.
	 *
	 * @param command0 the first command
	 * @param command1 the second command
	 * @return <code>true</code> if the commands intersect
	 * @since 2.1
	 */
	private boolean intersects(Command command0, Command command1) {
		// diff middle points if not intersecting
		if (command0.fOffset + command0.fLength <= command1.fOffset || command1.fOffset + command1.fLength <= command0.fOffset)
			return (2 * command0.fOffset + command0.fLength) - (2 * command1.fOffset + command1.fLength) == 0;
		return true;
	}

	/**
	 * Returns whether the given command intersects with this command.
	 *
	 * @param command the command
	 * @return <code>true</code> if the command intersects with this command
	 * @since 2.1
	 */
	private boolean intersects(Command command) {
		// diff middle points if not intersecting
		if (offset + length <= command.fOffset || command.fOffset + command.fLength <= offset)
			return (2 * offset + length) - (2 * command.fOffset + command.fLength) == 0;
		return true;
	}

	/**
	 * Executes the document commands on a document.
	 *
	 * @param document the document on which to execute the commands
	 * @throws BadLocationException in case access to the given document fails
	 * @since 2.1
	 */
	void execute(IDocument document) throws BadLocationException {

		if (length == 0 && text == null && fCommands.isEmpty())
			return;

		DefaultPositionUpdater updater= new DefaultPositionUpdater(getCategory());
		Position caretPosition= null;
		try {
			if (updateCaret()) {
				document.addPositionCategory(getCategory());
				document.addPositionUpdater(updater);
				caretPosition= new Position(caretOffset);
				document.addPosition(getCategory(), caretPosition);
			}

			final Command originalCommand= new Command(offset, length, text, owner);
			for (final Iterator<Command> iterator= new CommandIterator(fCommands, originalCommand, false); iterator.hasNext(); )
				iterator.next().execute(document);

		} catch (BadLocationException e) {
			// ignore
		} catch (BadPositionCategoryException e) {
			// ignore
		} finally {
			if (updateCaret()) {
				document.removePositionUpdater(updater);
				try {
					document.removePositionCategory(getCategory());
				} catch (BadPositionCategoryException e) {
					Assert.isTrue(false);
				}
				caretOffset= caretPosition.getOffset();
			}
		}
	}

	/**
	 * Returns <code>true</code> if the caret offset should be updated, <code>false</code> otherwise.
	 *
	 * @return <code>true</code> if the caret offset should be updated, <code>false</code> otherwise
	 * @since 3.0
	 */
	private boolean updateCaret() {
		return shiftsCaret && caretOffset != -1;
	}

	/**
	 * Returns the position category for the caret offset position.
	 *
	 * @return the position category for the caret offset position
	 * @since 3.0
	 */
	private String getCategory() {
		return toString();
	}

}
