/*******************************************************************************
 * Copyright (c) 2000, 2016 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.jdt.internal.ui.javaeditor;

import java.text.Bidi;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;

import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.StyleRange;
import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.RGB;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;

import org.eclipse.core.runtime.Assert;

import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jface.preference.PreferenceConverter;
import org.eclipse.jface.util.IPropertyChangeListener;
import org.eclipse.jface.util.PropertyChangeEvent;

import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.ITextPresentationListener;
import org.eclipse.jface.text.ITypedRegion;
import org.eclipse.jface.text.Region;
import org.eclipse.jface.text.TextUtilities;
import org.eclipse.jface.text.formatter.FormattingContextProperties;
import org.eclipse.jface.text.formatter.IFormattingContext;
import org.eclipse.jface.text.information.IInformationPresenter;
import org.eclipse.jface.text.reconciler.IReconciler;
import org.eclipse.jface.text.source.IOverviewRuler;
import org.eclipse.jface.text.source.IVerticalRuler;
import org.eclipse.jface.text.source.SourceViewerConfiguration;
import org.eclipse.jface.text.source.projection.ProjectionViewer;

import org.eclipse.ui.texteditor.AbstractDecoratedTextEditorPreferenceConstants;
import org.eclipse.ui.texteditor.AbstractTextEditor;

import org.eclipse.jdt.core.JavaCore;

import org.eclipse.jdt.ui.JavaUI;
import org.eclipse.jdt.ui.PreferenceConstants;
import org.eclipse.jdt.ui.text.IJavaColorConstants;
import org.eclipse.jdt.ui.text.IJavaPartitions;
import org.eclipse.jdt.ui.text.JavaSourceViewerConfiguration;

import org.eclipse.jdt.internal.ui.text.SmartBackspaceManager;
import org.eclipse.jdt.internal.ui.text.java.CompletionProposalComputerRegistry;
import org.eclipse.jdt.internal.ui.text.java.JavaFormattingContext;



public class JavaSourceViewer extends ProjectionViewer implements IPropertyChangeListener {

	/**
	 * Text operation code for requesting the outline for the current input.
	 */
	public static final int SHOW_OUTLINE= 51;

	/**
	 * Text operation code for requesting the outline for the element at the current position.
	 */
	public static final int OPEN_STRUCTURE= 52;

	/**
	 * Text operation code for requesting the hierarchy for the current input.
	 */
	public static final int SHOW_HIERARCHY= 53;

	/**
	 * Operation code for the annotate class file action.
	 */
	public static final int ANNOTATE_CLASS_FILE= 54;

	private IInformationPresenter fOutlinePresenter;
	private IInformationPresenter fStructurePresenter;
	private IInformationPresenter fHierarchyPresenter;

	/**
	 * The preference store.
	 *
	 * @since 3.0
	 */
	private IPreferenceStore fPreferenceStore;
	/**
	 * Is this source viewer configured?
	 *
	 * @since 3.0
	 */
	private boolean fIsConfigured;
	/**
	 * The backspace manager of this viewer.
	 *
	 * @since 3.0
	 */
	private SmartBackspaceManager fBackspaceManager;

	/**
	 * Whether to delay setting the visual document until the projection has been computed.
	 * <p>
	 * Added for performance optimization.
	 * </p>
	 * @see #prepareDelayedProjection()
	 * @since 3.1
	 */
	private boolean fIsSetVisibleDocumentDelayed= false;

	/**
	 * BIDI delimtiers.
	 *
	 * @since 3.4
	 */
	private static final String BIDI_DELIMITERS= "[ \\p{Punct}&&[^_]]"; //$NON-NLS-1$


	public JavaSourceViewer(Composite parent, IVerticalRuler verticalRuler, IOverviewRuler overviewRuler, boolean showAnnotationsOverview, int styles, IPreferenceStore store) {
		super(parent, verticalRuler, overviewRuler, showAnnotationsOverview, styles);
		setPreferenceStore(store);
	}

	/*
	 * @see org.eclipse.jface.text.source.SourceViewer#createFormattingContext()
	 * @since 3.0
	 */
	@Override
	public IFormattingContext createFormattingContext() {

		// it's ok to use instance preferences here as subclasses replace
		// with project dependent versions (see CompilationUnitEditor.AdaptedSourceViewer)
		IFormattingContext context= new JavaFormattingContext();
		Map<String, String> map= new HashMap<>(JavaCore.getOptions());
		context.setProperty(FormattingContextProperties.CONTEXT_PREFERENCES, map);

		return context;
	}

	/*
	 * @see ITextOperationTarget#doOperation(int)
	 */
	@Override
	public void doOperation(int operation) {
		if (getTextWidget() == null)
			return;

		switch (operation) {
			case SHOW_OUTLINE:
				if (fOutlinePresenter != null)
					fOutlinePresenter.showInformation();
				return;
			case OPEN_STRUCTURE:
				if (fStructurePresenter != null)
					fStructurePresenter.showInformation();
				return;
			case SHOW_HIERARCHY:
				if (fHierarchyPresenter != null)
					fHierarchyPresenter.showInformation();
				return;
		}

		super.doOperation(operation);
	}

	/*
	 * @see ITextOperationTarget#canDoOperation(int)
	 */
	@Override
	public boolean canDoOperation(int operation) {
		switch (operation) {
			case SHOW_OUTLINE:
				return fOutlinePresenter != null;
			case OPEN_STRUCTURE:
				return fStructurePresenter != null;
			case SHOW_HIERARCHY:
				return fHierarchyPresenter != null;
			default:
				return super.canDoOperation(operation);
		}
	}

	/*
	 * @see ISourceViewer#configure(SourceViewerConfiguration)
	 */
	@Override
	public void configure(SourceViewerConfiguration configuration) {

		/*
		 * Prevent access to colors disposed in unconfigure(), see:
		 *   https://bugs.eclipse.org/bugs/show_bug.cgi?id=53641
		 *   https://bugs.eclipse.org/bugs/show_bug.cgi?id=86177
		 */
		StyledText textWidget= getTextWidget();
		if (textWidget != null && !textWidget.isDisposed()) {
			Color foregroundColor= textWidget.getForeground();
			if (foregroundColor != null && foregroundColor.isDisposed())
				textWidget.setForeground(null);
			Color backgroundColor= textWidget.getBackground();
			if (backgroundColor != null && backgroundColor.isDisposed())
				textWidget.setBackground(null);
		}

		super.configure(configuration);
		if (configuration instanceof JavaSourceViewerConfiguration) {
			JavaSourceViewerConfiguration javaSVCconfiguration= (JavaSourceViewerConfiguration)configuration;
			fOutlinePresenter= javaSVCconfiguration.getOutlinePresenter(this, false);
			if (fOutlinePresenter != null)
				fOutlinePresenter.install(this);

			fStructurePresenter= javaSVCconfiguration.getOutlinePresenter(this, true);
			if (fStructurePresenter != null)
				fStructurePresenter.install(this);

			fHierarchyPresenter= javaSVCconfiguration.getHierarchyPresenter(this, true);
			if (fHierarchyPresenter != null)
				fHierarchyPresenter.install(this);

		}

		if (fPreferenceStore != null) {
			fPreferenceStore.addPropertyChangeListener(this);
			initializeViewerColors();
		}

		fIsConfigured= true;
	}


	protected void initializeViewerColors() {
		if (fPreferenceStore != null) {

			StyledText styledText= getTextWidget();

			// ----------- foreground color --------------------
			Color color= fPreferenceStore.getBoolean(AbstractTextEditor.PREFERENCE_COLOR_FOREGROUND_SYSTEM_DEFAULT)
			? null
			: createColor(fPreferenceStore, AbstractTextEditor.PREFERENCE_COLOR_FOREGROUND, styledText.getDisplay());
			styledText.setForeground(color);

			// ---------- background color ----------------------
			color= fPreferenceStore.getBoolean(AbstractTextEditor.PREFERENCE_COLOR_BACKGROUND_SYSTEM_DEFAULT)
			? null
			: createColor(fPreferenceStore, AbstractTextEditor.PREFERENCE_COLOR_BACKGROUND, styledText.getDisplay());
			styledText.setBackground(color);

			// ----------- selection foreground color --------------------
			color= fPreferenceStore.getBoolean(AbstractDecoratedTextEditorPreferenceConstants.EDITOR_SELECTION_FOREGROUND_DEFAULT_COLOR)
				? null
				: createColor(fPreferenceStore, AbstractDecoratedTextEditorPreferenceConstants.EDITOR_SELECTION_FOREGROUND_COLOR, styledText.getDisplay());
			styledText.setSelectionForeground(color);

			// ---------- selection background color ----------------------
			color= fPreferenceStore.getBoolean(AbstractDecoratedTextEditorPreferenceConstants.EDITOR_SELECTION_BACKGROUND_DEFAULT_COLOR)
				? null
				: createColor(fPreferenceStore, AbstractDecoratedTextEditorPreferenceConstants.EDITOR_SELECTION_BACKGROUND_COLOR, styledText.getDisplay());
			styledText.setSelectionBackground(color);

		}
    }

    /**
     * Creates a color from the information stored in the given preference store.
     * Returns <code>null</code> if there is no such information available.
     *
     * @param store the store to read from
     * @param key the key used for the lookup in the preference store
     * @param display the display used create the color
     * @return the created color according to the specification in the preference store
     * @since 3.0
     */
    private Color createColor(IPreferenceStore store, String key, Display display) {

        RGB rgb= null;

        if (store.contains(key)) {

            if (store.isDefault(key))
                rgb= PreferenceConverter.getDefaultColor(store, key);
            else
                rgb= PreferenceConverter.getColor(store, key);

            if (rgb != null)
                return new Color(display, rgb);
        }

        return null;
    }

	/**
	 * Sets the viewer's background color to the given control's background color.
	 * The background color is <em>only</em> set if it's visibly distinct from the
	 * default Java source text color.
	 *
	 * @param control the control with the default background color
	 * @since 3.7
	 */
	public void adaptBackgroundColor(Control control) {
		// workaround for dark editor background color, see https://bugs.eclipse.org/330680

		Color defaultColor= control.getBackground();
		float[] defaultBgHSB= defaultColor.getRGB().getHSB();

		Color javaDefaultColor= JavaUI.getColorManager().getColor(IJavaColorConstants.JAVA_DEFAULT);
		RGB javaDefaultRGB= javaDefaultColor != null ? javaDefaultColor.getRGB() : new RGB(255, 255, 255);
		float[] javaDefaultHSB= javaDefaultRGB.getHSB();

		if (Math.abs(defaultBgHSB[2] - javaDefaultHSB[2]) >= 0.5f) {
			getTextWidget().setBackground(defaultColor);
		}
	}


	/*
	 * @see org.eclipse.jface.text.source.ISourceViewerExtension2#unconfigure()
	 * @since 3.0
	 */
	@Override
	public void unconfigure() {
		if (fOutlinePresenter != null) {
			fOutlinePresenter.uninstall();
			fOutlinePresenter= null;
		}
		if (fStructurePresenter != null) {
			fStructurePresenter.uninstall();
			fStructurePresenter= null;
		}
		if (fHierarchyPresenter != null) {
			fHierarchyPresenter.uninstall();
			fHierarchyPresenter= null;
		}

		if (fPreferenceStore != null)
			fPreferenceStore.removePropertyChangeListener(this);

		super.unconfigure();

		fIsConfigured= false;
	}

	/*
	 * @see org.eclipse.jface.text.source.SourceViewer#rememberSelection()
	 */
	@Override
	public Point rememberSelection() {
		return super.rememberSelection();
	}

	/*
	 * @see org.eclipse.jface.text.source.SourceViewer#restoreSelection()
	 */
	@Override
	public void restoreSelection() {
		super.restoreSelection();
	}

	/*
	 * @see org.eclipse.jface.util.IPropertyChangeListener#propertyChange(org.eclipse.jface.util.PropertyChangeEvent)
	 */
	@Override
	public void propertyChange(PropertyChangeEvent event) {
		String property = event.getProperty();
		if (AbstractTextEditor.PREFERENCE_COLOR_FOREGROUND.equals(property)
				|| AbstractTextEditor.PREFERENCE_COLOR_FOREGROUND_SYSTEM_DEFAULT.equals(property)
				|| AbstractTextEditor.PREFERENCE_COLOR_BACKGROUND.equals(property)
				|| AbstractTextEditor.PREFERENCE_COLOR_BACKGROUND_SYSTEM_DEFAULT.equals(property)
				|| AbstractDecoratedTextEditorPreferenceConstants.EDITOR_SELECTION_FOREGROUND_COLOR.equals(property)
				|| AbstractDecoratedTextEditorPreferenceConstants.EDITOR_SELECTION_FOREGROUND_DEFAULT_COLOR.equals(property)
				|| AbstractDecoratedTextEditorPreferenceConstants.EDITOR_SELECTION_BACKGROUND_COLOR.equals(property)
				|| AbstractDecoratedTextEditorPreferenceConstants.EDITOR_SELECTION_BACKGROUND_DEFAULT_COLOR.equals(property))
		{
			initializeViewerColors();
		}
	}

	/**
	 * Sets the preference store on this viewer.
	 *
	 * @param store the preference store
	 *
	 * @since 3.0
	 */
	public void setPreferenceStore(IPreferenceStore store) {
		if (fIsConfigured && fPreferenceStore != null)
			fPreferenceStore.removePropertyChangeListener(this);

		fPreferenceStore= store;

		if (fIsConfigured && fPreferenceStore != null) {
			fPreferenceStore.addPropertyChangeListener(this);
			initializeViewerColors();
		}
	}

	/*
	 * @see org.eclipse.jface.text.source.SourceViewer#createControl(org.eclipse.swt.widgets.Composite, int)
	 */
	@Override
	protected void createControl(Composite parent, int styles) {

		// Use LEFT_TO_RIGHT unless otherwise specified.
		if ((styles & SWT.RIGHT_TO_LEFT) == 0 && (styles & SWT.LEFT_TO_RIGHT) == 0)
			styles |= SWT.LEFT_TO_RIGHT;

		final int baseLevel= (styles & SWT.RIGHT_TO_LEFT) != 0 ? Bidi.DIRECTION_RIGHT_TO_LEFT : Bidi.DIRECTION_LEFT_TO_RIGHT;

		super.createControl(parent, styles);

		fBackspaceManager= new SmartBackspaceManager();
		fBackspaceManager.install(this);

		StyledText text= getTextWidget();
		text.addBidiSegmentListener(event -> {
			if (redraws()) {
				try {
					event.segments= getBidiLineSegments(getDocument(), baseLevel, widgetOffset2ModelOffset(event.lineOffset), event.lineText);
				} catch (BadLocationException e) {
					// don't touch the segments
				}
			}
		});
	}

	/**
	 * Returns the backspace manager for this viewer.
	 *
	 * @return the backspace manager for this viewer, or <code>null</code> if
	 *         there is none
	 * @since 3.0
	 */
	public SmartBackspaceManager getBackspaceManager() {
		return fBackspaceManager;
	}

	/*
	 * @see org.eclipse.jface.text.source.SourceViewer#handleDispose()
	 */
	@Override
	protected void handleDispose() {
		if (fBackspaceManager != null) {
			fBackspaceManager.uninstall();
			fBackspaceManager= null;
		}
		super.handleDispose();
	}

	/**
	 * Prepends the text presentation listener at the beginning of the viewer's
	 * list of text presentation listeners.  If the listener is already registered
	 * with the viewer this call moves the listener to the beginning of
	 * the list.
	 *
	 * @param listener the text presentation listener
	 * @since 3.0
	 */
	public void prependTextPresentationListener(ITextPresentationListener listener) {

		Assert.isNotNull(listener);

		if (fTextPresentationListeners == null)
			fTextPresentationListeners= new ArrayList<>();

		fTextPresentationListeners.remove(listener);
		fTextPresentationListeners.add(0, listener);
	}

	/**
	 * Sets the given reconciler.
	 *
	 * @param reconciler the reconciler
	 * @since 3.0
	 */
	void setReconciler(IReconciler reconciler) {
		fReconciler= reconciler;
	}

	/**
	 * Returns the reconciler.
	 *
	 * @return the reconciler or <code>null</code> if not set
	 * @since 3.0
	 */
	IReconciler getReconciler() {
		return fReconciler;
	}

	/**
	 * Returns a segmentation of the line of the given document appropriate for
	 * Bidi rendering.
	 *
	 * @param document the document
	 * @param baseLevel the base level of the line
	 * @param lineStart the offset of the line
	 * @param lineText Text of the line to retrieve Bidi segments for
	 * @return the line's Bidi segmentation
	 * @throws BadLocationException in case lineOffset is not valid in document
	 */
	protected static int[] getBidiLineSegments(IDocument document, int baseLevel, int lineStart, String lineText) throws BadLocationException {

		if (lineText == null || document == null)
			return null;

		int lineLength= lineText.length();
		if (lineLength <= 2)
			return null;

		// Have ICU compute embedding levels. Consume these levels to reduce
		// the Bidi impact, by creating selective segments (preceding
		// character runs with a level mismatching the base level).
		// XXX: Alternatively, we could apply TextLayout. Pros would be full
		// synchronization with the underlying StyledText's (i.e. native) Bidi
		// implementation. Cons are performance penalty because of
		// unavailability of such methods as isLeftToRight and getLevels.

		Bidi bidi= new Bidi(lineText, baseLevel);
		if (bidi.isLeftToRight())
			// Bail out if this is not Bidi text.
			return null;

		IRegion line= document.getLineInformationOfOffset(lineStart);
		ITypedRegion[] linePartitioning= TextUtilities.computePartitioning(document, IJavaPartitions.JAVA_PARTITIONING, lineStart, line.getLength(), false);
		if (linePartitioning == null || linePartitioning.length < 1)
			return null;

		int segmentIndex= 1;
		int[] segments= new int[lineLength + 1];
		int nPartitions= linePartitioning.length;
		for (int partitionIndex= 0; partitionIndex < nPartitions; partitionIndex++) {

			ITypedRegion partition = linePartitioning[partitionIndex];
			int lineOffset= partition.getOffset() - lineStart;
			//Assert.isTrue(lineOffset >= 0 && lineOffset < lineLength);

			if (lineOffset > 0
					&& isMismatchingLevel(bidi.getLevelAt(lineOffset), baseLevel)
					&& isMismatchingLevel(bidi.getLevelAt(lineOffset - 1), baseLevel)) {
				// Indicate a Bidi segment at the partition start - provided
				// levels of both character at the current offset and its
				// preceding character mismatch the base paragraph level.
				// Partition end will be covered either by the start of the next
				// partition, a delimiter inside a next partition, or end of line.
				segments[segmentIndex++]= lineOffset;
			}
			if (IDocument.DEFAULT_CONTENT_TYPE.equals(partition.getType())) {
				int partitionEnd= Math.min(lineLength, lineOffset + partition.getLength());
				while (++lineOffset < partitionEnd) {
					if (isMismatchingLevel(bidi.getLevelAt(lineOffset), baseLevel)
							&& String.valueOf(lineText.charAt(lineOffset)).matches(BIDI_DELIMITERS)) {
						// For default content types, indicate a segment before
						// a delimiting character with a mismatching embedding
						// level.
						segments[segmentIndex++]= lineOffset;
					}
				}
			}
		}
		if (segmentIndex <= 1)
			return null;

		segments[0]= 0;
		if (segments[segmentIndex - 1] != lineLength)
			segments[segmentIndex++]= lineLength;

		if (segmentIndex == segments.length)
			return segments;

		int[] newSegments= new int[segmentIndex];
		System.arraycopy(segments, 0, newSegments, 0, segmentIndex);
		return newSegments;
	}

	/**
	 * Checks if the given embedding level is consistent with the base level.
	 *
	 * @param level Character embedding level to check.
	 * @param baseLevel Base level (direction) of the text.
	 * @return <code>true</code> if the character level is odd and the base
	 *         level is even OR the character level is even and the base level
	 *         is odd, and return <code>false</code> otherwise.
	 *
	 * @since 3.4
	 */
	private static boolean isMismatchingLevel(int level, int baseLevel) {
		return ((level ^ baseLevel) & 1) != 0;
	}

	/**
	 * Delays setting the visual document until after the projection has been computed.
	 * This method must only be called before the document is set on the viewer.
	 * <p>
	 * This is a performance optimization to reduce the computation of
	 * the text presentation triggered by <code>setVisibleDocument(IDocument)</code>.
	 * </p>
	 *
	 * @see #setVisibleDocument(IDocument)
	 * @since 3.1
	 */
	void prepareDelayedProjection() {
		// This assertion sometimes fails, but we don't know why. Commented out, since it hurts users.
		// See bug 400374: opening an editor when the maximum allowed is reached will result in an exception (and editor will not be opened)
//		Assert.isTrue(!fIsSetVisibleDocumentDelayed);
		fIsSetVisibleDocumentDelayed= true;
	}

	/**
	 * {@inheritDoc}
	 * <p>
	 * This is a performance optimization to reduce the computation of
	 * the text presentation triggered by {@link #setVisibleDocument(IDocument)}
	 * </p>
	 * @see #prepareDelayedProjection()
	 * @since 3.1
	 */
	@Override
	protected void setVisibleDocument(IDocument document) {
		if (fIsSetVisibleDocumentDelayed) {
			fIsSetVisibleDocumentDelayed= false;
			IDocument previous= getVisibleDocument();
			enableProjection(); // will set the visible document if anything is folded
			IDocument current= getVisibleDocument();
			// if the visible document was not replaced, continue as usual
			if (current != null && current != previous)
				return;
		}

		super.setVisibleDocument(document);
	}

	/**
	 * {@inheritDoc}
	 * <p>
	 * Performance optimization: since we know at this place
	 * that none of the clients expects the given range to be
	 * untouched we reuse the given range as return value.
	 * </p>
	 */
	@Override
	protected StyleRange modelStyleRange2WidgetStyleRange(StyleRange range) {
		IRegion region= modelRange2WidgetRange(new Region(range.start, range.length));
		if (region != null) {
			// don't clone the style range, but simply reuse it.
			range.start= region.getOffset();
			range.length= region.getLength();
			return range;
		}
		return null;
	}

	/**
	 * Reset the registered code mining providers.
	 *
	 * @since 3.16
	 */
	void resetCodeMinings() {
		super.setCodeMiningProviders(null);
	}

	/**
	 * Check if async completion is supported for this source viewer or not.
	 *
	 * @return <code>true</code> if async completion is supported, otherwise <code>false</code>.
	 */
	public final boolean isAsyncCompletionActive() {
		return fPreferenceStore.getBoolean(PreferenceConstants.CODEASSIST_NONUITHREAD_COMPUTATION) && !CompletionProposalComputerRegistry.getDefault().computingCompletionRequiresUIThread();
	}
}
