/*******************************************************************************
 * Copyright (c) 2000, 2010 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.projection;

import org.eclipse.core.runtime.Assert;

import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.BadPositionCategoryException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IDocumentInformationMapping;
import org.eclipse.jface.text.IDocumentInformationMappingExtension;
import org.eclipse.jface.text.IDocumentInformationMappingExtension2;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.Position;
import org.eclipse.jface.text.Region;


/**
 * Internal class. Do not use. Only public for testing purposes.
 * <p>
 * Implementation of {@link org.eclipse.jface.text.IDocumentInformationMapping}
 * for the projection mapping between a master and a slave document.
 *
 * @since 3.0
 * @noinstantiate This class is not intended to be instantiated by clients.
 * @noextend This class is not intended to be subclassed by clients.
 */
public class ProjectionMapping implements IDocumentInformationMapping , IDocumentInformationMappingExtension, IDocumentInformationMappingExtension2, IMinimalMapping {

	private static final int LEFT=  -1;
	private static final int NONE=   0;
	private static final int RIGHT= +1;

	/** The master document */
	private IDocument fMasterDocument;
	/** The position category used to manage the projection fragments inside the master document */
	private String fFragmentsCategory;
	/** The projection document */
	private IDocument fSlaveDocument;
	/** The position category to manage the projection segments inside the slave document. */
	private String fSegmentsCategory;
	/** Cached segments */
	private Position[] fCachedSegments;
	/** Cached fragments */
	private Position[] fCachedFragments;

	/**
	 * Creates a new mapping between the given parent document and the given projection document.
	 *
	 * @param masterDocument the master document
	 * @param fragmentsCategory the position category of the parent document used to manage the projected regions
	 * @param slaveDocument the slave document
	 * @param segmentsCategory the position category of the projection document used to manage the fragments
	 */
	public ProjectionMapping(IDocument masterDocument, String fragmentsCategory, IDocument slaveDocument, String segmentsCategory) {
		fMasterDocument= masterDocument;
		fFragmentsCategory= fragmentsCategory;
		fSlaveDocument= slaveDocument;
		fSegmentsCategory= segmentsCategory;
	}

	/**
	 * Notifies this projection mapping that there was a projection change.
	 */
	public void projectionChanged() {
		fCachedSegments= null;
		fCachedFragments= null;
	}

	private Position[] getSegments() {
		if (fCachedSegments == null) {
			try {
				fCachedSegments= fSlaveDocument.getPositions(fSegmentsCategory);
			} catch (BadPositionCategoryException e) {
				return new Position[0];
			}
		}
		return fCachedSegments;
	}

	private Position[] getFragments() {
		if (fCachedFragments == null) {
			try {
				fCachedFragments= fMasterDocument.getPositions(fFragmentsCategory);
			} catch (BadPositionCategoryException e) {
				return new Position[0];
			}
		}
		return fCachedFragments;
	}

	private int findSegmentIndex(int offset) throws BadLocationException {
		Position[] segments= getSegments();
		if (segments.length == 0) {
			if (offset > 0)
				throw new BadLocationException();
			return -1;
		}

		try {
			int index= fSlaveDocument.computeIndexInCategory(fSegmentsCategory, offset);
			if (index == segments.length && offset > exclusiveEnd(segments[index-1]))
				throw new BadLocationException();

			if (index < segments.length && offset == segments[index].offset)
				return index;

			if (index > 0)
				index--;

			return index;

		} catch (BadPositionCategoryException e) {
			throw new IllegalStateException();
		}
	}

	private Segment findSegment(int offset) throws BadLocationException {

		checkImageOffset(offset);

		int index= findSegmentIndex(offset);
		if (index == -1) {

			Segment s= new Segment(0, 0);
			Fragment f= new Fragment(0, 0);
			s.fragment= f;
			f.segment= s;
			return s;
		}

		Position[] segments= getSegments();
		return (Segment) segments[index];
	}

	/**
	 * Computes the fragment index given an origin offset. Returns the index of
	 * the fragment that contains <code>offset</code>, or <code>-1</code>
	 * if no fragment contains <code>offset</code>.
	 * <p>
	 * If <code>extensionDirection</code> is set to <code>RIGHT</code> or
	 * <code>LEFT</code>, the next fragment in that direction is returned if
	 * there is no fragment containing <code>offset</code>. Note that if
	 * <code>offset</code> occurs before any fragment and
	 * <code>extensionDirection</code> is <code>LEFT</code>,
	 * <code>-1</code> is also returned. The same applies for an offset after
	 * the last fragment and <code>extensionDirection</code> set to
	 * <code>RIGHT</code>.
	 * </p>
	 *
	 * @param offset an origin offset
	 * @param extensionDirection the direction in which to extend the search, or
	 *        <code>NONE</code>
	 * @return the index of the fragment containing <code>offset</code>, or
	 *         <code>-1</code>
	 * @throws BadLocationException if the index is not valid on the master
	 *         document
	 */
	private int findFragmentIndex(int offset, int extensionDirection) throws BadLocationException {
		try {

			Position[] fragments= getFragments();
			if (fragments.length == 0)
				return -1;

			int index= fMasterDocument.computeIndexInCategory(fFragmentsCategory, offset);

			if (index < fragments.length && offset == fragments[index].offset)
				return index;

			if (0 < index && index <= fragments.length && fragments[index - 1].includes(offset))
				return index - 1;

			switch (extensionDirection) {
				case LEFT:
					return index - 1;
				case RIGHT:
					if (index < fragments.length)
						return index;
			}

			return -1;

		} catch (BadPositionCategoryException e) {
			throw new IllegalStateException();
		}
	}

	private Fragment findFragment(int offset) throws BadLocationException {
		checkOriginOffset(offset);

		int index= findFragmentIndex(offset, NONE);
		Position[] fragments= getFragments();
		if (index == -1) {
			if (fragments.length > 0) {
				Fragment last= (Fragment) fragments[fragments.length - 1];
				if (exclusiveEnd(last) == offset)
					return last;
			}
			return null;
		}
		return (Fragment) fragments[index];
	}

	/**
	 * Returns the image region for <code>originRegion</code>.
	 *
	 * @param originRegion the region to get the image for
	 * @param exact if <code>true</code>, the begin and end offsets of
	 *        <code>originRegion</code> must be projected, otherwise
	 *        <code>null</code> is returned. If <code>false</code>, the
	 *        begin and end range that is not visible is simply clipped.
	 * @param takeClosestImage if <code>false</code>, <code>null</code> is
	 *        returned if <code>originRegion</code> is completely invisible.
	 *        If <code>true</code>, the zero-length region is returned that
	 *        "covers" the hidden origin region
	 * @return the image region of <code>originRegion</code>
	 * @throws BadLocationException if the region is not a valid origin region
	 */
	private IRegion toImageRegion(IRegion originRegion, boolean exact, boolean takeClosestImage) throws BadLocationException {
		if (originRegion.getLength() == 0 && !takeClosestImage) {
			int imageOffset= toImageOffset(originRegion.getOffset());
			return imageOffset == -1 ? null : new Region(imageOffset, 0);
		}

		Fragment[] fragments= findFragments(originRegion, exact, takeClosestImage);
		if (fragments == null) {
			if (takeClosestImage) {
				// originRegion may before the first or after the last fragment
				Position[] allFragments= getFragments();
				if (allFragments.length > 0) {
					// before the first
					if (exclusiveEnd(originRegion) <= allFragments[0].getOffset())
						return new Region(0, 0);
					// after last
					Position last= allFragments[allFragments.length - 1];
					if (originRegion.getOffset() >= exclusiveEnd(last))
						return new Region(exclusiveEnd(((Fragment) last).segment), 0);
				}
				return new Region(0, 0);
			}
			return null;
		}

		int imageOffset, exclusiveImageEndOffset;

		// translate start offset
		int relative= originRegion.getOffset() - fragments[0].getOffset();
		if (relative < 0) {
			Assert.isTrue(!exact);
			relative= 0;
		}
		imageOffset= fragments[0].segment.getOffset() + relative;

		// translate end offset
		relative= exclusiveEnd(originRegion) - fragments[1].getOffset();
		if (relative > fragments[1].getLength()) {
			Assert.isTrue(!exact);
			relative= fragments[1].getLength();
		}
		exclusiveImageEndOffset= fragments[1].segment.getOffset() + relative;

		return new Region(imageOffset, exclusiveImageEndOffset - imageOffset);
	}

	/**
	 * Returns the two fragments containing the begin and end offsets of
	 * <code>originRegion</code>.
	 *
	 * @param originRegion the region to get the fragments for
	 * @param exact if <code>true</code>, only the fragments that contain the
	 *        begin and end offsets are returned; if <code>false</code>, the
	 *        first fragment after the begin offset and the last fragment before
	 *        the end offset are returned if the offsets are not projected
	 * @param takeClosestImage if <code>true</code>, the method will return
	 *        fragments also if <code>originRegion</code> completely lies in
	 *        an unprojected region.
	 * @return the two fragments containing the begin and end offset of
	 *         <code>originRegion</code>, or <code>null</code> if these do
	 *         not exist
	 * @throws BadLocationException if the region is not a valid origin region
	 */
	private Fragment[] findFragments(IRegion originRegion, boolean exact, boolean takeClosestImage) throws BadLocationException {
		Position[] fragments= getFragments();
		if (fragments.length == 0)
			return null;

		checkOriginRegion(originRegion);

		int startFragmentIdx= findFragmentIndex(originRegion.getOffset(), exact ? NONE : RIGHT);
		if (startFragmentIdx == -1)
			return null;

		int endFragmentIdx= findFragmentIndex(inclusiveEnd(originRegion), exact ? NONE : LEFT);
		if (!takeClosestImage && startFragmentIdx > endFragmentIdx || endFragmentIdx == -1)
			return null;

		Fragment[] result= {(Fragment) fragments[startFragmentIdx], (Fragment) fragments[endFragmentIdx]};
		return result;
	}

	private IRegion createOriginStartRegion(Segment image, int offsetShift) {
		return new Region(image.fragment.getOffset() + offsetShift, image.fragment.getLength() - offsetShift);
	}

	private IRegion createOriginRegion(Segment image) {
		return new Region(image.fragment.getOffset(), image.fragment.getLength());
	}

	private IRegion createOriginEndRegion(Segment image, int lengthReduction) {
		return new Region(image.fragment.getOffset(), image.fragment.getLength() - lengthReduction);
	}

	private IRegion createImageStartRegion(Fragment origin, int offsetShift) {
		int shift= offsetShift > 0 ? offsetShift : 0;
		return new Region(origin.segment.getOffset() + shift, origin.segment.getLength() - shift);
	}

	private IRegion createImageRegion(Fragment origin) {
		return new Region(origin.segment.getOffset(), origin.segment.getLength());
	}

	private IRegion createImageEndRegion(Fragment origin, int lengthReduction) {
		int reduction= lengthReduction > 0 ? lengthReduction : 0;
		return new Region(origin.segment.getOffset(), origin.segment.getLength() - reduction);
	}

	private IRegion createOriginStartRegion(Fragment origin, int offsetShift) {
		int shift= offsetShift > 0 ? offsetShift : 0;
		return new Region(origin.getOffset() + shift, origin.getLength() - shift);
	}

	private IRegion createOriginRegion(Fragment origin) {
		return new Region(origin.getOffset(), origin.getLength());
	}

	private IRegion createOriginEndRegion(Fragment origin, int lengthReduction) {
		int reduction= lengthReduction > 0 ? lengthReduction : 0;
		return new Region(origin.getOffset(), origin.getLength() - reduction);
	}

	private IRegion getIntersectingRegion(IRegion left, IRegion right) {
		int offset= Math.max(left.getOffset(), right.getOffset());
		int exclusiveEndOffset= Math.min(exclusiveEnd(left), exclusiveEnd(right));
		if (exclusiveEndOffset < offset)
			return null;
		return new Region(offset, exclusiveEndOffset - offset);
	}

	@Override
	public IRegion getCoverage() {
		Position[] fragments= getFragments();
		if (fragments != null && fragments.length > 0) {
			Position first=fragments[0];
			Position last= fragments[fragments.length -1];
			return  new Region(first.offset, exclusiveEnd(last) - first.offset);
		}
		return new Region(0, 0);
	}

	@Override
	public int toOriginOffset(int imageOffset) throws BadLocationException {
		Segment segment= findSegment(imageOffset);
		int relative= imageOffset - segment.offset;
		return segment.fragment.offset + relative;
	}

	@Override
	public IRegion toOriginRegion(IRegion imageRegion) throws BadLocationException {
		int imageOffset= imageRegion.getOffset();
		int imageLength= imageRegion.getLength();

		if (imageLength == 0) {
			if (imageOffset == 0) {
				Position[] fragments= getFragments();
				if (fragments.length == 0 || (fragments.length == 1 && fragments[0].getOffset() == 0 && fragments[0].getLength() == 0))
					return new Region(0, fMasterDocument.getLength());
			}
			return new Region(toOriginOffset(imageOffset), 0);
		}

		int originOffset= toOriginOffset(imageOffset);
		int inclusiveImageEndOffset= imageOffset + imageLength -1;
		int inclusiveOriginEndOffset= toOriginOffset(inclusiveImageEndOffset);

		return new Region(originOffset, (inclusiveOriginEndOffset + 1) - originOffset);
	}

	@Override
	public IRegion toOriginLines(int imageLine) throws BadLocationException {
		IRegion imageRegion= fSlaveDocument.getLineInformation(imageLine);
		IRegion originRegion= toOriginRegion(imageRegion);

		int originStartLine= fMasterDocument.getLineOfOffset(originRegion.getOffset());
		if (originRegion.getLength() == 0)
			return new Region(originStartLine, 1);

		int originEndLine= fMasterDocument.getLineOfOffset(inclusiveEnd(originRegion));
		return new Region(originStartLine, (originEndLine + 1) - originStartLine);
	}

	@Override
	public int toOriginLine(int imageLine) throws BadLocationException {
		IRegion lines= toOriginLines(imageLine);
		return (lines.getLength() > 1 ? -1 : lines.getOffset());
	}

	@Override
	public int toImageOffset(int originOffset) throws BadLocationException {
		Fragment fragment= findFragment(originOffset);
		if (fragment != null) {
			int relative= originOffset - fragment.offset;
			return fragment.segment.offset + relative;
		}
		return -1;
	}

	@Override
	public IRegion toExactImageRegion(IRegion originRegion) throws BadLocationException {
		return toImageRegion(originRegion, true, false);
	}

	@Override
	public IRegion toImageRegion(IRegion originRegion) throws BadLocationException {
		return toImageRegion(originRegion, false, false);
	}

	@Override
	public IRegion toClosestImageRegion(IRegion originRegion) throws BadLocationException {
		return toImageRegion(originRegion, false, true);
	}

	@Override
	public int toImageLine(int originLine) throws BadLocationException {
		IRegion originRegion= fMasterDocument.getLineInformation(originLine);
		IRegion imageRegion= toImageRegion(originRegion);
		if (imageRegion == null) {
			int imageOffset= toImageOffset(originRegion.getOffset());
			if (imageOffset > -1)
				imageRegion= new Region(imageOffset, 0);
			else
				return -1;
		}

		int startLine= fSlaveDocument.getLineOfOffset(imageRegion.getOffset());
		if (imageRegion.getLength() == 0)
			return startLine;

		int endLine= fSlaveDocument.getLineOfOffset(imageRegion.getOffset() + imageRegion.getLength());
		if (endLine != startLine)
			throw new IllegalStateException("startLine (" + startLine + ") does not match endLine (" + endLine + ")"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$

		return startLine;
	}

	@Override
	public int toClosestImageLine(int originLine) throws BadLocationException {
		try {

			int imageLine= toImageLine(originLine);
			if (imageLine > -1)
				return imageLine;

			Position[] fragments= getFragments();
			if (fragments.length == 0)
				return -1;

			IRegion originLineRegion= fMasterDocument.getLineInformation(originLine);
			int index= fMasterDocument.computeIndexInCategory(fFragmentsCategory, originLineRegion.getOffset());

			if (0 < index && index < fragments.length) {
				Fragment left= (Fragment) fragments[index - 1];
				int leftDistance= originLineRegion.getOffset() - (exclusiveEnd(left));
				Fragment right= (Fragment) fragments[index];
				int rightDistance= right.getOffset() - (exclusiveEnd(originLineRegion));

				if (leftDistance <= rightDistance)
					originLine= fMasterDocument.getLineOfOffset(left.getOffset() + Math.max(left.getLength() - 1, 0));
				else
					originLine= fMasterDocument.getLineOfOffset(right.getOffset());

			} else if (index == 0) {
				Fragment right= (Fragment) fragments[index];
				originLine= fMasterDocument.getLineOfOffset(right.getOffset());
			} else if (index == fragments.length) {
				Fragment left= (Fragment) fragments[index - 1];
				originLine= fMasterDocument.getLineOfOffset(exclusiveEnd(left));
			}

			return toImageLine(originLine);

		} catch (BadPositionCategoryException x) {
		}

		return -1;
	}

	@Override
	public IRegion[] toExactOriginRegions(IRegion imageRegion) throws BadLocationException {

		if (imageRegion.getLength() == 0)
			return new IRegion[] { new Region(toOriginOffset(imageRegion.getOffset()), 0) };

		int endOffset= exclusiveEnd(imageRegion);
		Position[] segments= getSegments();
		int firstIndex= findSegmentIndex(imageRegion.getOffset());
		int lastIndex= findSegmentIndex(endOffset - 1);

		int resultLength= lastIndex - firstIndex + 1;
		IRegion[] result= new IRegion[resultLength];

		// first
		result[0]= createOriginStartRegion((Segment) segments[firstIndex], imageRegion.getOffset() - segments[firstIndex].getOffset());
		// middles
		for (int i= 1; i < resultLength - 1; i++)
			result[i]= createOriginRegion((Segment) segments[firstIndex + i]);
		// last
		Segment last= (Segment) segments[lastIndex];
		int segmentEndOffset= exclusiveEnd(last);
		IRegion lastRegion= createOriginEndRegion(last, segmentEndOffset - endOffset);
		if (resultLength > 1) {
			// first != last
			result[resultLength - 1]= lastRegion;
		} else {
			// merge first and last
			IRegion intersection= getIntersectingRegion(result[0], lastRegion);
			if (intersection == null)
				result= new IRegion[0];
			else
				result[0]= intersection;
		}

		return result;
	}

	@Override
	public int getImageLength() {
		Position[] segments= getSegments();
		int length= 0;
		for (Position segment : segments) {
			length += segment.length;
		}
		return length;
	}

	@Override
	public IRegion[] toExactImageRegions(IRegion originRegion) throws BadLocationException {

		int offset= originRegion.getOffset();
		if (originRegion.getLength() == 0) {
			int imageOffset= toImageOffset(offset);
			return imageOffset > -1 ? new IRegion[] { new Region(imageOffset, 0) } : null;
		}

		int endOffset= exclusiveEnd(originRegion);
		Position[] fragments= getFragments();
		int firstIndex= findFragmentIndex(offset, RIGHT);
		int lastIndex= findFragmentIndex(endOffset - 1, LEFT);

		if (firstIndex == -1 || firstIndex > lastIndex)
			return null;

		int resultLength= lastIndex - firstIndex + 1;
		IRegion[] result= new IRegion[resultLength];

		// first
		result[0]= createImageStartRegion((Fragment) fragments[firstIndex], offset - fragments[firstIndex].getOffset());
		// middles
		for (int i= 1; i < resultLength - 1; i++)
			result[i]= createImageRegion((Fragment) fragments[firstIndex + i]);
		// last
		Fragment last= (Fragment) fragments[lastIndex];
		int fragmentEndOffset= exclusiveEnd(last);
		IRegion lastRegion= createImageEndRegion(last, fragmentEndOffset - endOffset);
		if (resultLength > 1) {
			// first != last
			result[resultLength - 1]= lastRegion;
		} else {
			// merge first and last
			IRegion intersection= getIntersectingRegion(result[0], lastRegion);
			if (intersection == null)
				return null;
			result[0]= intersection;
		}

		return result;
	}

	@Override
	public IRegion[] getExactCoverage(IRegion originRegion) throws BadLocationException {

		int originOffset= originRegion.getOffset();
		int originLength= originRegion.getLength();

		if (originLength == 0) {
			int imageOffset= toImageOffset(originOffset);
			return imageOffset > -1 ? new IRegion[] { new Region(originOffset, 0) } : null;
		}

		int endOffset= originOffset + originLength;
		Position[] fragments= getFragments();
		int firstIndex= findFragmentIndex(originOffset, RIGHT);
		int lastIndex= findFragmentIndex(endOffset - 1, LEFT);

		if (firstIndex == -1 || firstIndex > lastIndex)
			return null;

		int resultLength= lastIndex - firstIndex + 1;
		IRegion[] result= new IRegion[resultLength];

		// first
		result[0]= createOriginStartRegion((Fragment) fragments[firstIndex], originOffset - fragments[firstIndex].getOffset());
		// middles
		for (int i= 1; i < resultLength - 1; i++)
			result[i]= createOriginRegion((Fragment) fragments[firstIndex + i]);
		// last
		Fragment last= (Fragment) fragments[lastIndex];
		int fragmentEndOffset= exclusiveEnd(last);
		IRegion lastRegion= createOriginEndRegion(last, fragmentEndOffset - endOffset);
		if (resultLength > 1) {
			// first != last
			result[resultLength - 1]= lastRegion;
		} else {
			// merge first and last
			IRegion intersection= getIntersectingRegion(result[0], lastRegion);
			if (intersection == null)
				return null;
			result[0]= intersection;
		}

		return result;
	}

	private final void checkOriginRegion(IRegion originRegion) throws BadLocationException {
		int offset= originRegion.getOffset();
		int endOffset= inclusiveEnd(originRegion);
		int max= fMasterDocument.getLength();
		if (offset < 0 || offset > max || endOffset < 0 || endOffset > max)
			throw new BadLocationException();
	}

	private final void checkOriginOffset(int originOffset) throws BadLocationException {
		if (originOffset < 0 || originOffset > fMasterDocument.getLength())
			throw new BadLocationException();
	}

	private final void checkImageOffset(int imageOffset) throws BadLocationException {
		if (imageOffset < 0 || imageOffset > getImageLength())
			throw new BadLocationException();
	}

	private final int exclusiveEnd(Position position) {
		return position.offset + position.length;
	}

	private final int exclusiveEnd(IRegion region) {
		return region.getOffset() + region.getLength();
	}

	private final int inclusiveEnd(IRegion region) {
		int length= region.getLength();
		if (length == 0)
			return region.getOffset();
		return region.getOffset() + length - 1;
	}


}
