/*******************************************************************************
 * Copyright (c) 2000, 2020 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.Iterator;
import java.util.NoSuchElementException;

import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.StyleRange;
import org.eclipse.swt.custom.StyledText;

import org.eclipse.core.runtime.Assert;


/**
 * Describes the presentation styles for a section of an indexed text such as a
 * document or string. A text presentation defines a default style for the whole
 * section and in addition style differences for individual subsections. Text
 * presentations can be narrowed down to a particular result window. All methods
 * are result window aware, i.e. ranges outside the result window are always
 * ignored.
 * <p>
 * All iterators provided by a text presentation assume that they enumerate non
 * overlapping, consecutive ranges inside the default range. Thus, all these
 * iterators do not include the default range. The default style range must be
 * explicitly asked for using <code>getDefaultStyleRange</code>.
 */
public class TextPresentation {

	/**
	 * Applies the given presentation to the given text widget. Helper method.
	 *
	 * @param presentation the style information
	 * @param text the widget to which to apply the style information
	 * @since 2.0
	 */
	public static void applyTextPresentation(TextPresentation presentation, StyledText text) {

		StyleRange[] ranges= new StyleRange[presentation.getDenumerableRanges()];

		int i= 0;
		Iterator<StyleRange> e= presentation.getAllStyleRangeIterator();
		while (e.hasNext())
			ranges[i++]= e.next();

		text.setStyleRanges(ranges);
	}




	/**
	 * Enumerates all the <code>StyleRange</code>s included in the presentation.
	 */
	class FilterIterator implements Iterator<StyleRange> {

		/** The index of the next style range to be enumerated */
		protected int fIndex;
		/** The upper bound of the indices of style ranges to be enumerated */
		protected int fLength;
		/** Indicates whether ranges similar to the default range should be enumerated */
		protected boolean fSkipDefaults;
		/** The result window */
		protected IRegion fWindow;

		/**
		 * <code>skipDefaults</code> tells the enumeration to skip all those style ranges
		 * which define the same style as the presentation's default style range.
		 *
		 * @param skipDefaults <code>false</code> if ranges similar to the default range should be enumerated
		 */
		protected FilterIterator(boolean skipDefaults) {

			fSkipDefaults= skipDefaults;

			fWindow= fResultWindow;
			fIndex= getFirstIndexInWindow(fWindow);
			fLength= getFirstIndexAfterWindow(fWindow);

			if (fSkipDefaults)
				computeIndex();
		}

		@Override
		public StyleRange next() {
			try {
				StyleRange r= fRanges.get(fIndex++);
				return createWindowRelativeRange(fWindow, r);
			} catch (IndexOutOfBoundsException x) {
				throw new NoSuchElementException();
			} finally {
				if (fSkipDefaults)
					computeIndex();
			}
		}

		@Override
		public boolean hasNext() {
			return fIndex < fLength;
		}

		/**
		 * Returns whether the given object should be skipped.
		 *
		 * @param o the object to be checked
		 * @return <code>true</code> if the object should be skipped by the iterator
		 */
		protected boolean skip(Object o) {
			StyleRange r= (StyleRange) o;
			return r.similarTo(fDefaultRange);
		}

		/**
		 * Computes the index of the styled range that is the next to be enumerated.
		 */
		protected void computeIndex() {
			while (fIndex < fLength && skip(fRanges.get(fIndex)))
				++fIndex;
		}
	}

	/** The style information for the range covered by the whole presentation */
	private StyleRange fDefaultRange;
	/** The member ranges of the presentation */
	private ArrayList<StyleRange> fRanges;
	/** A clipping region against which the presentation can be clipped when asked for results */
	private IRegion fResultWindow;
	/**
	 * The optional extent for this presentation.
	 * @since 3.0
	 */
	private IRegion fExtent;


	/**
	 * Creates a new empty text presentation.
	 */
	public TextPresentation() {
		fRanges= new ArrayList<>(50);
	}

	/**
	 * Creates a new empty text presentation. <code>sizeHint</code> tells the expected size of this
	 * presentation.
	 *
	 * @param sizeHint the expected size of this presentation, must be positive
	 */
	public TextPresentation(int sizeHint) {
		Assert.isTrue(sizeHint > 0);
		fRanges= new ArrayList<>(sizeHint);
	}

	/**
	 * Creates a new empty text presentation with the given extent. <code>sizeHint</code> tells the
	 * expected size of this presentation.
	 *
	 * @param extent the extent of the created <code>TextPresentation</code>
	 * @param sizeHint the expected size of this presentation, must be positive
	 * @since 3.0
	 */
	public TextPresentation(IRegion extent, int sizeHint) {
		this(sizeHint);
		Assert.isNotNull(extent);
		fExtent= extent;
	}

	/**
	 * Sets the result window for this presentation. When dealing with
	 * this presentation all ranges which are outside the result window
	 * are ignored. For example, the size of the presentation is 0
	 * when there is no range inside the window even if there are ranges
	 * outside the window. All methods are aware of the result window.
	 *
	 * @param resultWindow the result window
	 */
	public void setResultWindow(IRegion resultWindow) {
		fResultWindow= resultWindow;
	}

	/**
	 * Set the default style range of this presentation.
	 * The default style range defines the overall area covered
	 * by this presentation and its style information.
	 *
	 * @param range the range describing the default region
	 */
	public void setDefaultStyleRange(StyleRange range) {
		fDefaultRange= range;
	}

	/**
	 * Returns this presentation's default style range. The returned <code>StyleRange</code>
	 * is relative to the start of the result window.
	 *
	 * @return this presentation's default style range
	 */
	public StyleRange getDefaultStyleRange() {
		StyleRange range= createWindowRelativeRange(fResultWindow, fDefaultRange);
		if (range == null)
			return null;
		return (StyleRange)range.clone();

	}

	/**
	 * Add the given range to the presentation. The range must be a
	 * subrange of the presentation's default range.
	 *
	 * @param range the range to be added
	 */
	public void addStyleRange(StyleRange range) {
		checkConsistency(range);
		fRanges.add(range);
	}

	/**
	 * Replaces the given range in this presentation. The range must be a
	 * subrange of the presentation's default range.
	 *
	 * @param range the range to be added
	 * @since 3.0
	 */
	public void replaceStyleRange(StyleRange range) {
		applyStyleRange(range, false);
	}

	/**
	 * Merges the given range into this presentation. The range must be a
	 * subrange of the presentation's default range.
	 *
	 * @param range the range to be added
	 * @since 3.0
	 */
	public void mergeStyleRange(StyleRange range) {
		applyStyleRange(range, true);
	}

	/**
	 * Applies the given range to this presentation. The range must be a
	 * subrange of the presentation's default range.
	 *
	 * @param range the range to be added
	 * @param merge <code>true</code> if the style should be merged instead of replaced
	 * @since 3.0
	 */
	private void applyStyleRange(StyleRange range, boolean merge) {
		if (range.length == 0)
			return;

		checkConsistency(range);

		int start= range.start;
		int length= range.length;
		int end= start + length;

		if (fRanges.isEmpty()) {
			StyleRange defaultRange= getDefaultStyleRange();
			if (defaultRange == null)
				defaultRange= range;

			defaultRange.start= start;
			defaultRange.length= length;
			applyStyle(range, defaultRange, merge);
			fRanges.add(defaultRange);
		} else {
			IRegion rangeRegion= new Region(start, length);
			int first= getFirstIndexInWindow(rangeRegion);

			if (first == fRanges.size()) {
				StyleRange defaultRange= getDefaultStyleRange();
				if (defaultRange == null)
					defaultRange= range;
				defaultRange.start= start;
				defaultRange.length= length;
				applyStyle(range, defaultRange, merge);
				fRanges.add(defaultRange);
				return;
			}

			int last= getFirstIndexAfterWindow(rangeRegion);
			for (int i= first; i < last && length > 0; i++) {

				StyleRange current= fRanges.get(i);
				int currentStart= current.start;
				int currentEnd= currentStart + current.length;

				if (end <= currentStart) {
					fRanges.add(i, range);
					return;
				}

				if (start >= currentEnd)
					continue;

				StyleRange currentCopy= null;
				if (end < currentEnd)
					currentCopy= (StyleRange)current.clone();

				if (start < currentStart) {
					// Apply style to new default range and add it
					StyleRange defaultRange= getDefaultStyleRange();
					if (defaultRange == null)
						defaultRange= new StyleRange();

					defaultRange.start= start;
					defaultRange.length= currentStart - start;
					applyStyle(range, defaultRange, merge);
					fRanges.add(i, defaultRange);
					i++; last++;


					// Apply style to first part of current range
					current.length= Math.min(end, currentEnd) - currentStart;
					applyStyle(range, current, merge);
				}

				if (start >= currentStart) {
					// Shorten the current range
					current.length= start - currentStart;

					// Apply the style to the rest of the current range and add it
					if (current.length > 0) {
						current= (StyleRange)current.clone();
						i++; last++;
						fRanges.add(i, current);
					}
					applyStyle(range, current, merge);
					current.start= start;
					current.length= Math.min(end, currentEnd) - start;
				}

				if (end < currentEnd) {
					// Add rest of current range
					currentCopy.start= end;
					currentCopy.length= currentEnd - end;
					i++; last++;
					fRanges.add(i,  currentCopy);
				}

				// Update range
				range.start=  currentEnd;
				range.length= Math.max(end - currentEnd, 0);
				start= range.start;
				length= range.length;
			}
			if (length > 0) {
				// Apply style to new default range and add it
				StyleRange defaultRange= getDefaultStyleRange();
				if (defaultRange == null)
					defaultRange= range;
				defaultRange.start= start;
				defaultRange.length= end - start;
				applyStyle(range, defaultRange, merge);
				fRanges.add(last, defaultRange);
			}
		}
	}

	/**
	 * Replaces the given ranges in this presentation. Each range must be a
	 * subrange of the presentation's default range. The ranges must be ordered
	 * by increasing offset and must not overlap (but may be adjacent).
	 *
	 * @param ranges the ranges to be added
	 * @since 3.0
	 */
	public void replaceStyleRanges(StyleRange[] ranges) {
		applyStyleRanges(ranges, false);
	}

	/**
	 * Merges the given ranges into this presentation. Each range must be a
	 * subrange of the presentation's default range. The ranges must be ordered
	 * by increasing offset and must not overlap (but may be adjacent).
	 *
	 * @param ranges the ranges to be added
	 * @since 3.0
	 */
	public void mergeStyleRanges(StyleRange[] ranges) {
		applyStyleRanges(ranges, true);
	}

	/**
	 * Applies the given ranges to this presentation. Each range must be a
	 * subrange of the presentation's default range. The ranges must be ordered
	 * by increasing offset and must not overlap (but may be adjacent).
	 *
	 * @param ranges the ranges to be added
	 * @param merge <code>true</code> if the style should be merged instead of replaced
	 * @since 3.0
	 */
	private void applyStyleRanges(StyleRange[] ranges, boolean merge) {
		int j= 0;
		ArrayList<StyleRange> oldRanges= fRanges;
		ArrayList<StyleRange> newRanges= new ArrayList<>(2*ranges.length + oldRanges.size());
		for (StyleRange range : ranges) {
			fRanges= oldRanges; // for getFirstIndexAfterWindow(...)
			for (int m= getFirstIndexAfterWindow(new Region(range.start, range.length)); j < m; j++)
				newRanges.add(oldRanges.get(j));
			fRanges= newRanges; // for mergeStyleRange(...)
			applyStyleRange(range, merge);
		}
		for (int m= oldRanges.size(); j < m; j++)
			newRanges.add(oldRanges.get(j));
		fRanges= newRanges;
	}

	/**
	 * Applies the template's style to the target.
	 *
	 * @param template the style range to be used as template
	 * @param target the style range to which to apply the template
	 * @param merge <code>true</code> if the style should be merged instead of replaced
	 * @since 3.0
	 */
	private void applyStyle(StyleRange template, StyleRange target, boolean merge) {
		if (merge) {
			if (template.font != null)
				target.font= template.font;
			target.fontStyle|= template.fontStyle;

			if (template.metrics != null)
				target.metrics= template.metrics;

			if (template.foreground != null || template.underlineStyle == SWT.UNDERLINE_LINK)
				target.foreground= template.foreground;
			if (template.background != null)
				target.background= template.background;

			target.strikeout|= template.strikeout;
			if (template.strikeoutColor != null)
				target.strikeoutColor= template.strikeoutColor;

			target.underline|= template.underline;
			if (template.underlineStyle != SWT.NONE && target.underlineStyle != SWT.UNDERLINE_LINK)
				target.underlineStyle= template.underlineStyle;

			if (template.underlineColor != null)
				target.underlineColor= template.underlineColor;

			if (template.borderStyle != SWT.NONE)
				target.borderStyle= template.borderStyle;
			if (template.borderColor != null)
				target.borderColor= template.borderColor;

		} else {
			target.font= template.font;
			target.fontStyle= template.fontStyle;
			target.metrics= template.metrics;
			target.foreground= template.foreground;
			target.background= template.background;
			target.strikeout= template.strikeout;
			target.strikeoutColor= template.strikeoutColor;
			target.underline= template.underline;
			target.underlineStyle= template.underlineStyle;
			target.underlineColor= template.underlineColor;
			target.borderStyle= template.borderStyle;
			target.borderColor= template.borderColor;
		}
	}

	/**
	 * Checks whether the given range is a subrange of the presentation's
	 * default style range.
	 *
	 * @param range the range to be checked
	 * @exception IllegalArgumentException if range is not a subrange of the presentation's default range
	 */
	private void checkConsistency(StyleRange range) {

		if (range == null)
			throw new IllegalArgumentException();

		if (fDefaultRange != null) {

			if (range.start < fDefaultRange.start)
				range.start= fDefaultRange.start;

			int defaultEnd= fDefaultRange.start + fDefaultRange.length;
			int end= range.start + range.length;
			if (end > defaultEnd)
				range.length -= (end - defaultEnd);
		}
	}

	/**
	 * Returns the index of the first range which overlaps with the
	 * specified window.
	 *
	 * @param window the window to be used for searching
	 * @return the index of the first range overlapping with the window
	 */
	private int getFirstIndexInWindow(IRegion window) {
		if (window != null) {
			int start= window.getOffset();
			int i= -1, j= fRanges.size();
			while (j - i > 1) {
				int k= (i + j) >> 1;
				StyleRange r= fRanges.get(k);
				if (r.start + r.length > start)
					j= k;
				else
					i= k;
			}
			return j;
		}
		return 0;
	}

	/**
	 * Returns the index of the first range which comes after the specified window and does
	 * not overlap with this window.
	 *
	 * @param window the window to be used for searching
	 * @return the index of the first range behind the window and not overlapping with the window
	 */
	private int getFirstIndexAfterWindow(IRegion window) {
		if (window != null) {
			int end= window.getOffset() + window.getLength();
			int i= -1, j= fRanges.size();
			while (j - i > 1) {
				int k= (i + j) >> 1;
				StyleRange r= fRanges.get(k);
				if (r.start < end)
					i= k;
				else
					j= k;
			}
			return j;
		}
		return fRanges.size();
	}

	/**
	 * Returns a style range which is relative to the specified window and
	 * appropriately clipped if necessary. The original style range is not
	 * modified.
	 *
	 * @param window the reference window
	 * @param range the absolute range
	 * @return the window relative range based on the absolute range
	 */
	private StyleRange createWindowRelativeRange(IRegion window, StyleRange range) {
		if (window == null || range == null)
			return range;

		int start= range.start - window.getOffset();
		if (start < 0)
			start= 0;

		int rangeEnd= range.start + range.length;
		int windowEnd= window.getOffset() + window.getLength();
		int end= (rangeEnd > windowEnd ? windowEnd : rangeEnd);
		end -= window.getOffset();

		StyleRange newRange= (StyleRange) range.clone();
		newRange.start= start;
		newRange.length= end - start;
		return newRange;
	}

	/**
	 * Returns the region which is relative to the specified window and
	 * appropriately clipped if necessary.
	 *
	 * @param coverage the absolute coverage
	 * @return the window relative region based on the absolute coverage
	 * @since 3.0
	 */
	private IRegion createWindowRelativeRegion(IRegion coverage) {
		if (fResultWindow == null || coverage == null)
			return coverage;

		int start= coverage.getOffset() - fResultWindow.getOffset();
		if (start < 0)
			start= 0;

		int rangeEnd= coverage.getOffset() + coverage.getLength();
		int windowEnd= fResultWindow.getOffset() + fResultWindow.getLength();
		int end= (rangeEnd > windowEnd ? windowEnd : rangeEnd);
		end -= fResultWindow.getOffset();

		return new Region(start, end - start);
	}

	/**
	 * Returns an iterator which enumerates all style ranged which define a style
	 * different from the presentation's default style range. The default style range
	 * is not enumerated.
	 *
	 * @return a style range iterator
	 */
	public Iterator<StyleRange> getNonDefaultStyleRangeIterator() {
		return new FilterIterator(fDefaultRange != null);
	}

	/**
	 * Returns an iterator which enumerates all style ranges of this presentation
	 * except the default style range. The returned <code>StyleRange</code>s
	 * are relative to the start of the presentation's result window.
	 *
	 * @return a style range iterator
	 */
	public Iterator<StyleRange> getAllStyleRangeIterator() {
		return new FilterIterator(false);
	}

	/**
	 * Returns whether this collection contains any style range including
	 * the default style range.
	 *
	 * @return <code>true</code> if there is no style range in this presentation
	 */
	public boolean isEmpty() {
		return (fDefaultRange == null && getDenumerableRanges() == 0);
	}

	/**
	 * Returns the number of style ranges in the presentation not counting the default
	 * style range.
	 *
	 * @return the number of style ranges in the presentation excluding the default style range
	 */
	public int getDenumerableRanges() {
		int size= getFirstIndexAfterWindow(fResultWindow) - getFirstIndexInWindow(fResultWindow);
		return (size < 0 ? 0 : size);
	}

	/**
	 * Returns the style range with the smallest offset ignoring the default style range or null
	 * if the presentation is empty.
	 *
	 * @return the style range with the smallest offset different from the default style range
	 */
	public StyleRange getFirstStyleRange() {
		try {
			StyleRange range= fRanges.get(getFirstIndexInWindow(fResultWindow));
			return createWindowRelativeRange(fResultWindow, range);
		} catch (NoSuchElementException | IndexOutOfBoundsException x) {
			return null;
		}
	}

	/**
	 * Returns the style range with the highest offset ignoring the default style range.
	 *
	 * @return the style range with the highest offset different from the default style range
	 */
	public StyleRange getLastStyleRange() {
		try {
			StyleRange range= fRanges.get(getFirstIndexAfterWindow(fResultWindow) - 1);
			return createWindowRelativeRange(fResultWindow, range);
		} catch (NoSuchElementException | IndexOutOfBoundsException x) {
			return null;
		}
	}

	/**
	 * Returns the coverage of this presentation as clipped by the presentation's
	 * result window.
	 *
	 * @return the coverage of this presentation
	 */
	public IRegion getCoverage() {

		if (fDefaultRange != null) {
			StyleRange range= getDefaultStyleRange();
			return new Region(range.start, range.length);
		}

		StyleRange first= getFirstStyleRange();
		StyleRange last= getLastStyleRange();

		if (first == null || last == null)
			return null;

		return new Region(first.start, last.start - first. start + last.length);
	}

	/**
	 * Returns the extent of this presentation clipped by the
	 * presentation's result window.
	 *
	 * @return the clipped extent
	 * @since 3.0
	 */
	public IRegion getExtent() {
		if (fExtent != null)
			return createWindowRelativeRegion(fExtent);
		return getCoverage();
	}

	/**
	 * Clears this presentation by resetting all applied changes.
	 * @since 2.0
	 */
	public void clear() {
		fDefaultRange= null;
		fResultWindow= null;
		fRanges.clear();
	}


}
