/*******************************************************************************
 * Copyright (c) 2000, 2014 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
 *     Sergey Prigogin (Google) - bug 441448
 *******************************************************************************/

package org.eclipse.jface.text.formatter;

import java.util.HashMap;
import java.util.Map;

import org.eclipse.core.runtime.Assert;

import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.DefaultPositionUpdater;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IDocumentPartitioner;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.ITypedRegion;
import org.eclipse.jface.text.TextUtilities;
import org.eclipse.jface.text.TypedPosition;

/**
 * Content formatter for edit-based formatting strategies.
 * <p>
 * Two kinds of formatting strategies can be registered with this formatter:
 * <ul>
 * <li>one master formatting strategy for the default content type</li>
 * <li>one formatting strategy for each non-default content type</li>
 * </ul>
 * The master formatting strategy always formats the whole region to be
 * formatted in the first pass. In a second pass, all partitions of the region
 * to be formatted that are not of master content type are formatted using the
 * slave formatting strategy registered for the underlying content type. All
 * formatting strategies must implement {@link IFormattingStrategyExtension}.
 * <p>
 * Regions to be formatted with the master formatting strategy always have
 * an offset aligned to the line start. Regions to be formatted with slave formatting
 * strategies are aligned on partition boundaries.
 *
 * @see IFormattingStrategyExtension
 * @since 3.0
 */
public class MultiPassContentFormatter implements IContentFormatter, IContentFormatterExtension {

	/**
	 * Position updater that shifts otherwise deleted positions to the next
	 * non-whitespace character. The length of the positions are truncated to
	 * one if the position was shifted.
	 */
	protected class NonDeletingPositionUpdater extends DefaultPositionUpdater {

		/**
		 * Creates a new non-deleting position updater.
		 *
		 * @param category The position category to update its positions
		 */
		public NonDeletingPositionUpdater(final String category) {
			super(category);
		}

		@Override
		protected final boolean notDeleted() {

			if (fOffset < fPosition.offset && (fPosition.offset + fPosition.length < fOffset + fLength)) {

				int offset= fOffset + fLength;
				if (offset < fDocument.getLength()) {

					try {

						boolean moved= false;
						char character= fDocument.getChar(offset);

						while (offset < fDocument.getLength() && Character.isWhitespace(character)) {

							moved= true;
							character= fDocument.getChar(offset++);
						}

						if (moved)
							offset--;

					} catch (BadLocationException exception) {
						// Can not happen
					}

					fPosition.offset= offset;
					fPosition.length= 0;
				}
			}
			return true;
		}
	}

	/** The master formatting strategy */
	private IFormattingStrategyExtension fMaster= null;
	/** The partitioning of this content formatter */
	private final String fPartitioning;
	/** The slave formatting strategies */
	private final Map<String, IFormattingStrategy> fSlaves= new HashMap<>();
	/** The default content type */
	private final String fType;

	/**
	 * Creates a new content formatter.
	 *
	 * @param partitioning the document partitioning for this formatter
	 * @param type the default content type
	 */
	public MultiPassContentFormatter(final String partitioning, final String type) {
		fPartitioning= partitioning;
		fType= type;
	}

	@Override
	public final void format(final IDocument medium, final IFormattingContext context) {

		context.setProperty(FormattingContextProperties.CONTEXT_MEDIUM, medium);

		final Boolean document= (Boolean)context.getProperty(FormattingContextProperties.CONTEXT_DOCUMENT);
		if (document == null || !document.booleanValue()) {

			final IRegion region= (IRegion)context.getProperty(FormattingContextProperties.CONTEXT_REGION);
			if (region != null) {
				try {
					formatMaster(context, medium, region.getOffset(), region.getLength());
				} finally {
					formatSlaves(context, medium, region.getOffset(), region.getLength());
				}
			}
		} else {
			try {
				formatMaster(context, medium, 0, medium.getLength());
			} finally {
				formatSlaves(context, medium, 0, medium.getLength());
			}
		}
	}

	@Override
	public final void format(final IDocument medium, final IRegion region) {

		final FormattingContext context= new FormattingContext();

		context.setProperty(FormattingContextProperties.CONTEXT_DOCUMENT, Boolean.FALSE);
		context.setProperty(FormattingContextProperties.CONTEXT_REGION, region);

		format(medium, context);
	}

	/**
	 * Formats the document specified in the formatting context with the master
	 * formatting strategy.
	 * <p>
	 * The master formatting strategy covers all regions of the document. The
	 * offset of the region to be formatted is aligned on line start boundaries,
	 * whereas the end index of the region remains the same. For this formatting
	 * type the document partitioning is not taken into account.
	 *
	 * @param context The formatting context to use
	 * @param document The document to operate on
	 * @param offset The offset of the region to format
	 * @param length The length of the region to format
	 */
	protected void formatMaster(final IFormattingContext context, final IDocument document, int offset, int length) {

		try {

			if (length != 0) {
				// Extend the selection to the beginning of line if it is not empty.
				// An empty selection must remain empty since it may be treated in
				// a special way by the formatter.
				final int delta= offset - document.getLineInformationOfOffset(offset).getOffset();
				offset -= delta;
				length += delta;
			}

		} catch (BadLocationException exception) {
			// Do nothing
		}

		if (fMaster != null) {

			context.setProperty(FormattingContextProperties.CONTEXT_PARTITION, new TypedPosition(offset, length, fType));

			fMaster.formatterStarts(context);
			fMaster.format();
			fMaster.formatterStops();
		}
	}

	/**
	 * Formats the document specified in the formatting context with the
	 * formatting strategy registered for the content type.
	 * <p>
	 * For this formatting type only slave strategies are used. The region to be
	 * formatted is aligned on partition boundaries of the underlying content
	 * type. The exact formatting strategy is determined by the underlying
	 * content type of the document partitioning.
	 *
	 * @param context The formatting context to use
	 * @param document The document to operate on
	 * @param offset The offset of the region to format
	 * @param length The length of the region to format
	 * @param type The content type of the region to format
	 */
	protected void formatSlave(final IFormattingContext context, final IDocument document, final int offset, final int length, final String type) {

		final IFormattingStrategyExtension strategy= (IFormattingStrategyExtension)fSlaves.get(type);
		if (strategy != null) {

			context.setProperty(FormattingContextProperties.CONTEXT_PARTITION, new TypedPosition(offset, length, type));

			strategy.formatterStarts(context);
			strategy.format();
			strategy.formatterStops();
		}
	}

	/**
	 * Formats the document specified in the formatting context with the slave
	 * formatting strategies.
	 * <p>
	 * For each content type of the region to be formatted in the document
	 * partitioning, the registered slave formatting strategy is used to format
	 * that particular region. The region to be formatted is aligned on
	 * partition boundaries of the underlying content type. If the content type
	 * is the document's default content type, nothing happens.
	 *
	 * @param context The formatting context to use
	 * @param document The document to operate on
	 * @param offset The offset of the region to format
	 * @param length The length of the region to format
	 */
	protected void formatSlaves(final IFormattingContext context, final IDocument document, final int offset, final int length) {

		Map<String, IDocumentPartitioner> partitioners= new HashMap<>(0);
		try {

			final ITypedRegion[] partitions= TextUtilities.computePartitioning(document, fPartitioning, offset, length, false);

			if (!fType.equals(partitions[0].getType()))
				partitions[0]= TextUtilities.getPartition(document, fPartitioning, partitions[0].getOffset(), false);

			if (partitions.length > 1) {

				if (!fType.equals(partitions[partitions.length - 1].getType()))
					partitions[partitions.length - 1]= TextUtilities.getPartition(document, fPartitioning, partitions[partitions.length - 1].getOffset(), false);
			}

			String type= null;
			ITypedRegion partition= null;

			partitioners= TextUtilities.removeDocumentPartitioners(document);

			for (int index= partitions.length - 1; index >= 0; index--) {

				partition= partitions[index];
				type= partition.getType();

				if (!fType.equals(type))
					formatSlave(context, document, partition.getOffset(), partition.getLength(), type);
			}

		} catch (BadLocationException exception) {
			// Should not happen
		} finally {
			TextUtilities.addDocumentPartitioners(document, partitioners);
		}
	}

	@Override
	public final IFormattingStrategy getFormattingStrategy(final String type) {
		return null;
	}

	/**
	 * Registers a master formatting strategy.
	 * <p>
	 * The strategy may already be registered with a certain content type as
	 * slave strategy. The master strategy is registered for the default content
	 * type of documents. If a master strategy has already been registered, it
	 * is overridden by the new one.
	 *
	 * @param strategy The master formatting strategy, must implement
	 * 	{@link IFormattingStrategyExtension}
	 */
	public final void setMasterStrategy(final IFormattingStrategy strategy) {
		Assert.isTrue(strategy instanceof IFormattingStrategyExtension);
		fMaster= (IFormattingStrategyExtension) strategy;
	}

	/**
	 * Registers a slave formatting strategy for a certain content type.
	 * <p>
	 * The strategy may already be registered as master strategy. An
	 * already registered slave strategy for the specified content type
	 * will be replaced. However, the same strategy may be registered with
	 * several content types. Slave strategies cannot be registered for the
	 * default content type of documents.
	 *
	 * @param strategy The slave formatting strategy
	 * @param type The content type to register this strategy with,
	 * 	must implement {@link IFormattingStrategyExtension}
	 */
	public final void setSlaveStrategy(final IFormattingStrategy strategy, final String type) {
		Assert.isTrue(strategy instanceof IFormattingStrategyExtension);
		if (!fType.equals(type))
			fSlaves.put(type, strategy);
	}
}
