content: /*******************************************************************************
   * Copyright (c) 2000, 2008 IBM Corporation and others.
   * All rights reserved. This program and the accompanying materials
   * are made available under the terms of the Eclipse Public License v1.0
   * which accompanies this distribution, and is available at
   * http://www.eclipse.org/legal/epl-v10.html
   *
   * Contributors:
   *     IBM Corporation - initial API and implementation
   *******************************************************************************/
  
  package org.eclipse.wst.jsdt.internal.ui.javaeditor;
  
  import java.util.ArrayList;
  import java.util.List;
  
  import org.eclipse.jface.preference.IPreferenceStore;
  import org.eclipse.jface.preference.PreferenceConverter;
  import org.eclipse.jface.resource.StringConverter;
  import org.eclipse.jface.text.Position;
  import org.eclipse.jface.text.Region;
  import org.eclipse.jface.text.TextAttribute;
  import org.eclipse.jface.util.IPropertyChangeListener;
  import org.eclipse.jface.util.PropertyChangeEvent;
  import org.eclipse.swt.SWT;
  import org.eclipse.swt.custom.StyleRange;
  import org.eclipse.swt.graphics.Color;
  import org.eclipse.swt.graphics.RGB;
  import org.eclipse.wst.jsdt.internal.ui.text.JavaPresentationReconciler;
  import org.eclipse.wst.jsdt.ui.text.IColorManager;
  import org.eclipse.wst.jsdt.ui.text.IColorManagerExtension;
  import org.eclipse.wst.jsdt.ui.text.JavaScriptSourceViewerConfiguration;
  
  /**
   * Semantic highlighting manager
   *
   * 
   */
  public class SemanticHighlightingManager implements IPropertyChangeListener {
  
  	/**
  	 * Highlighting.
  	 */
  	static class Highlighting { // TODO: rename to HighlightingStyle
  
  		/** Text attribute */
  		private TextAttribute fTextAttribute;
  		/** Enabled state */
  		private boolean fIsEnabled;
  
  		/**
  		 * Initialize with the given text attribute.
  		 * @param textAttribute The text attribute
  		 * @param isEnabled the enabled state
  		 */
  		public Highlighting(TextAttribute textAttribute, boolean isEnabled) {
  			setTextAttribute(textAttribute);
  			setEnabled(isEnabled);
  		}
  
  		/**
  		 * @return Returns the text attribute.
  		 */
  		public TextAttribute getTextAttribute() {
  			return fTextAttribute;
  		}
  
  		/**
  		 * @param textAttribute The background to set.
  		 */
  		public void setTextAttribute(TextAttribute textAttribute) {
  			fTextAttribute= textAttribute;
  		}
  
  		/**
  		 * @return the enabled state
  		 */
  		public boolean isEnabled() {
  			return fIsEnabled;
  		}
  
  		/**
  		 * @param isEnabled the new enabled state
  		 */
  		public void setEnabled(boolean isEnabled) {
  			fIsEnabled= isEnabled;
  		}
  	}
  
  	/**
  	 * Highlighted Positions.
  	 */
  	static class HighlightedPosition extends Position {
  
  		/** Highlighting of the position */
  		private Highlighting fStyle;
  
  		/** Lock object */
  		private Object fLock;
  
  		/**
  		 * Initialize the styled positions with the given offset, length and foreground color.
  		 *
  		 * @param offset The position offset
  		 * @param length The position length
  		 * @param highlighting The position's highlighting
  		 * @param lock The lock object
  		 */
  		public HighlightedPosition(int offset, int length, Highlighting highlighting, Object lock) {
  			super(offset, length);
  			fStyle= highlighting;
  			fLock= lock;
  		}
  
  		/**
  		 * @return Returns a corresponding style range.
  		 */
  		public StyleRange createStyleRange() {
  			int len= 0;
  			if (fStyle.isEnabled())
  				len= getLength();
  
  			TextAttribute textAttribute= fStyle.getTextAttribute();
  			int style= textAttribute.getStyle();
  			int fontStyle= style & (SWT.ITALIC | SWT.BOLD | SWT.NORMAL);
  			StyleRange styleRange= new StyleRange(getOffset(), len, textAttribute.getForeground(), textAttribute.getBackground(), fontStyle);
  			styleRange.strikeout= (style & TextAttribute.STRIKETHROUGH) != 0;
  			styleRange.underline= (style & TextAttribute.UNDERLINE) != 0;
  
  			return styleRange;
  		}
  
  		/**
  		 * Uses reference equality for the highlighting.
  		 *
  		 * @param off The offset
  		 * @param len The length
  		 * @param highlighting The highlighting
  		 * @return <code>true</code> iff the given offset, length and highlighting are equal to the internal ones.
  		 */
  		public boolean isEqual(int off, int len, Highlighting highlighting) {
  			synchronized (fLock) {
  				return !isDeleted() && getOffset() == off && getLength() == len && fStyle == highlighting;
  			}
  		}
  
  		/**
  		 * Is this position contained in the given range (inclusive)? Synchronizes on position updater.
  		 *
  		 * @param off The range offset
  		 * @param len The range length
  		 * @return <code>true</code> iff this position is not delete and contained in the given range.
  		 */
  		public boolean isContained(int off, int len) {
  			synchronized (fLock) {
  				return !isDeleted() && off <= getOffset() && off + len >= getOffset() + getLength();
  			}
  		}
  
  		public void update(int off, int len) {
  			synchronized (fLock) {
  				super.setOffset(off);
  				super.setLength(len);
  			}
  		}
  
  		/*
  		 * @see org.eclipse.jface.text.Position#setLength(int)
  		 */
  		public void setLength(int length) {
  			synchronized (fLock) {
  				super.setLength(length);
  			}
  		}
  
  		/*
  		 * @see org.eclipse.jface.text.Position#setOffset(int)
  		 */
  		public void setOffset(int offset) {
  			synchronized (fLock) {
  				super.setOffset(offset);
  			}
  		}
  
  		/*
  		 * @see org.eclipse.jface.text.Position#delete()
  		 */
  		public void delete() {
  			synchronized (fLock) {
  				super.delete();
  			}
  		}
  
  		/*
  		 * @see org.eclipse.jface.text.Position#undelete()
  		 */
  		public void undelete() {
  			synchronized (fLock) {
  				super.undelete();
  			}
  		}
  
  		/**
  		 * @return Returns the highlighting.
  		 */
  		public Highlighting getHighlighting() {
  			return fStyle;
  		}
  	}
  
  	/**
  	 * Highlighted ranges.
  	 */
  	public static class HighlightedRange extends Region {
  		/** The highlighting key as returned by {@link SemanticHighlighting#getPreferenceKey()}. */
  		private String fKey;
  
  		/**
  		 * Initialize with the given offset, length and highlighting key.
  		 *
  		 * @param offset
  		 * @param length
  		 * @param key the highlighting key as returned by {@link SemanticHighlighting#getPreferenceKey()}
  		 */
  		public HighlightedRange(int offset, int length, String key) {
  			super(offset, length);
  			fKey= key;
  		}
  
  		/**
  		 * @return the highlighting key as returned by {@link SemanticHighlighting#getPreferenceKey()}
  		 */
  		public String getKey() {
  			return fKey;
  		}
  
  		/*
  		 * @see org.eclipse.jface.text.Region#equals(java.lang.Object)
  		 */
  		public boolean equals(Object o) {
  			return super.equals(o) && o instanceof HighlightedRange && fKey.equals(((HighlightedRange)o).getKey());
  		}
  
  		/*
  		 * @see org.eclipse.jface.text.Region#hashCode()
  		 */
  		public int hashCode() {
  			return super.hashCode() | fKey.hashCode();
  		}
  	}
  
  	/** Semantic highlighting presenter */
  	private SemanticHighlightingPresenter fPresenter;
  	/** Semantic highlighting reconciler */
  	private SemanticHighlightingReconciler fReconciler;
  
  	/** Semantic highlightings */
  	private SemanticHighlighting[] fSemanticHighlightings;
  	/** Highlightings */
  	private Highlighting[] fHighlightings;
  
  	/** The editor */
  	private JavaEditor fEditor;
  	/** The source viewer */
  	private JavaSourceViewer fSourceViewer;
  	/** The color manager */
  	private IColorManager fColorManager;
  	/** The preference store */
  	private IPreferenceStore fPreferenceStore;
  	/** The source viewer configuration */
  	private JavaScriptSourceViewerConfiguration fConfiguration;
  	/** The presentation reconciler */
  	private JavaPresentationReconciler fPresentationReconciler;
  
  	/** The hard-coded ranges */
  	private HighlightedRange[][] fHardcodedRanges;
  
  	/**
  	 * Install the semantic highlighting on the given editor infrastructure
  	 *
  	 * @param editor The Java editor
  	 * @param sourceViewer The source viewer
  	 * @param colorManager The color manager
  	 * @param preferenceStore The preference store
  	 */
  	public void install(JavaEditor editor, JavaSourceViewer sourceViewer, IColorManager colorManager, IPreferenceStore preferenceStore) {
  		fEditor= editor;
  		fSourceViewer= sourceViewer;
  		fColorManager= colorManager;
  		fPreferenceStore= preferenceStore;
  		if (fEditor != null) {
  			fConfiguration= editor.createJavaSourceViewerConfiguration();
  			fPresentationReconciler= (JavaPresentationReconciler) fConfiguration.getPresentationReconciler(sourceViewer);
  		} else {
  			fConfiguration= null;
  			fPresentationReconciler= null;
  		}
  
  		fPreferenceStore.addPropertyChangeListener(this);
  
  		if (isEnabled())
  			enable();
  	}
  
  	/**
  	 * Install the semantic highlighting on the given source viewer infrastructure. No reconciliation will be performed.
  	 *
  	 * @param sourceViewer the source viewer
  	 * @param colorManager the color manager
  	 * @param preferenceStore the preference store
  	 * @param hardcodedRanges the hard-coded ranges to be highlighted
  	 */
  	public void install(JavaSourceViewer sourceViewer, IColorManager colorManager, IPreferenceStore preferenceStore, HighlightedRange[][] hardcodedRanges) {
  		fHardcodedRanges= hardcodedRanges;
  		install(null, sourceViewer, colorManager, preferenceStore);
  	}
  
  	/**
  	 * Enable semantic highlighting.
  	 */
  	private void enable() {
  		initializeHighlightings();
  
  		fPresenter= new SemanticHighlightingPresenter();
  		fPresenter.install(fSourceViewer, fPresentationReconciler);
  
  		if (fEditor != null) {
  			fReconciler= new SemanticHighlightingReconciler();
  			fReconciler.install(fEditor, fSourceViewer, fPresenter, fSemanticHighlightings, fHighlightings);
  		} else {
  			fPresenter.updatePresentation(null, createHardcodedPositions(), new HighlightedPosition[0]);
  		}
  	}
  
  	/**
  	 * Computes the hard-coded positions from the hard-coded ranges
  	 *
  	 * @return the hard-coded positions
  	 */
  	private HighlightedPosition[] createHardcodedPositions() {
  		List positions= new ArrayList();
  		for (int i= 0; i < fHardcodedRanges.length; i++) {
  			HighlightedRange range= null;
  			Highlighting hl= null;
  			for (int j= 0; j < fHardcodedRanges[i].length; j++ ) {
  				hl= getHighlighting(fHardcodedRanges[i][j].getKey());
  				if (hl.isEnabled()) {
  					range= fHardcodedRanges[i][j];
  					break;
  				}
  			}
  
  			if (range != null)
  				positions.add(fPresenter.createHighlightedPosition(range.getOffset(), range.getLength(), hl));
  		}
  		return (HighlightedPosition[]) positions.toArray(new HighlightedPosition[positions.size()]);
  	}
  
  	/**
  	 * Returns the highlighting corresponding to the given key.
  	 *
  	 * @param key the highlighting key as returned by {@link SemanticHighlighting#getPreferenceKey()}
  	 * @return the corresponding highlighting
  	 */
  	private Highlighting getHighlighting(String key) {
  		for (int i= 0; i < fSemanticHighlightings.length; i++) {
  			SemanticHighlighting semanticHighlighting= fSemanticHighlightings[i];
  			if (key.equals(semanticHighlighting.getPreferenceKey()))
  				return fHighlightings[i];
  		}
  		return null;
  	}
  
  	/**
  	 * Uninstall the semantic highlighting
  	 */
  	public void uninstall() {
  		disable();
  
  		if (fPreferenceStore != null) {
  			fPreferenceStore.removePropertyChangeListener(this);
  			fPreferenceStore= null;
  		}
  
  		fEditor= null;
  		fSourceViewer= null;
  		fColorManager= null;
  		fConfiguration= null;
  		fPresentationReconciler= null;
  		fHardcodedRanges= null;
  	}
  
  	/**
  	 * Disable semantic highlighting.
  	 */
  	private void disable() {
  		if (fReconciler != null) {
  			fReconciler.uninstall();
  			fReconciler= null;
  		}
  
  		if (fPresenter != null) {
  			fPresenter.uninstall();
  			fPresenter= null;
  		}
  
  		if (fSemanticHighlightings != null)
  			disposeHighlightings();
  	}
  
  	/**
  	 * @return <code>true</code> iff semantic highlighting is enabled in the preferences
  	 */
  	private boolean isEnabled() {
  		return SemanticHighlightings.isEnabled(fPreferenceStore);
  	}
  
  	/**
  	 * Initialize semantic highlightings.
  	 */
  	private void initializeHighlightings() {
  		fSemanticHighlightings= SemanticHighlightings.getSemanticHighlightings();
  		fHighlightings= new Highlighting[fSemanticHighlightings.length];
  
  		for (int i= 0, n= fSemanticHighlightings.length; i < n; i++) {
  			SemanticHighlighting semanticHighlighting= fSemanticHighlightings[i];
  			String colorKey= SemanticHighlightings.getColorPreferenceKey(semanticHighlighting);
  			addColor(colorKey);
  
  			String boldKey= SemanticHighlightings.getBoldPreferenceKey(semanticHighlighting);
  			int style= fPreferenceStore.getBoolean(boldKey) ? SWT.BOLD : SWT.NORMAL;
  
  			String italicKey= SemanticHighlightings.getItalicPreferenceKey(semanticHighlighting);
  			if (fPreferenceStore.getBoolean(italicKey))
  				style |= SWT.ITALIC;
  
  			String strikethroughKey= SemanticHighlightings.getStrikethroughPreferenceKey(semanticHighlighting);
  			if (fPreferenceStore.getBoolean(strikethroughKey))
  				style |= TextAttribute.STRIKETHROUGH;
  
  			String underlineKey= SemanticHighlightings.getUnderlinePreferenceKey(semanticHighlighting);
  			if (fPreferenceStore.getBoolean(underlineKey))
  				style |= TextAttribute.UNDERLINE;
  
  			boolean isEnabled= fPreferenceStore.getBoolean(SemanticHighlightings.getEnabledPreferenceKey(semanticHighlighting));
  
  			fHighlightings[i]= new Highlighting(new TextAttribute(fColorManager.getColor(PreferenceConverter.getColor(fPreferenceStore, colorKey)), null, style), isEnabled);
  		}
  	}
  
  	/**
  	 * Dispose the semantic highlightings.
  	 */
  	private void disposeHighlightings() {
  		for (int i= 0, n= fSemanticHighlightings.length; i < n; i++)
  			removeColor(SemanticHighlightings.getColorPreferenceKey(fSemanticHighlightings[i]));
  
  		fSemanticHighlightings= null;
  		fHighlightings= null;
  	}
  
  	/*
  	 * @see org.eclipse.jface.util.IPropertyChangeListener#propertyChange(org.eclipse.jface.util.PropertyChangeEvent)
  	 */
  	public void propertyChange(PropertyChangeEvent event) {
  		handlePropertyChangeEvent(event);
  	}
  
  	/**
  	 * Handle the given property change event
  	 *
  	 * @param event The event
  	 */
  	private void handlePropertyChangeEvent(PropertyChangeEvent event) {
  		if (fPreferenceStore == null)
  			return; // Uninstalled during event notification
  
  		if (fConfiguration != null)
  			fConfiguration.handlePropertyChangeEvent(event);
  
  		if (SemanticHighlightings.affectsEnablement(fPreferenceStore, event)) {
  			if (isEnabled())
  				enable();
  			else
  				disable();
  		}
  
  		if (!isEnabled())
  			return;
  		
  		boolean refreshNeeded= false;
  
  		for (int i= 0, n= fSemanticHighlightings.length; i < n; i++) {
  			SemanticHighlighting semanticHighlighting= fSemanticHighlightings[i];
  
  			String colorKey= SemanticHighlightings.getColorPreferenceKey(semanticHighlighting);
  			if (colorKey.equals(event.getProperty())) {
  				adaptToTextForegroundChange(fHighlightings[i], event);
  				fPresenter.highlightingStyleChanged(fHighlightings[i]);
  				refreshNeeded= true;
  				continue;
  			}
  
  			String boldKey= SemanticHighlightings.getBoldPreferenceKey(semanticHighlighting);
  			if (boldKey.equals(event.getProperty())) {
  				adaptToTextStyleChange(fHighlightings[i], event, SWT.BOLD);
  				fPresenter.highlightingStyleChanged(fHighlightings[i]);
  				refreshNeeded= true;
  				continue;
  			}
  
  			String italicKey= SemanticHighlightings.getItalicPreferenceKey(semanticHighlighting);
  			if (italicKey.equals(event.getProperty())) {
  				adaptToTextStyleChange(fHighlightings[i], event, SWT.ITALIC);
  				fPresenter.highlightingStyleChanged(fHighlightings[i]);
  				refreshNeeded= true;
  				continue;
  			}
  
  			String strikethroughKey= SemanticHighlightings.getStrikethroughPreferenceKey(semanticHighlighting);
  			if (strikethroughKey.equals(event.getProperty())) {
  				adaptToTextStyleChange(fHighlightings[i], event, TextAttribute.STRIKETHROUGH);
  				fPresenter.highlightingStyleChanged(fHighlightings[i]);
  				refreshNeeded= true;
  				continue;
  			}
  
  			String underlineKey= SemanticHighlightings.getUnderlinePreferenceKey(semanticHighlighting);
  			if (underlineKey.equals(event.getProperty())) {
  				adaptToTextStyleChange(fHighlightings[i], event, TextAttribute.UNDERLINE);
  				fPresenter.highlightingStyleChanged(fHighlightings[i]);
  				refreshNeeded= true;
  				continue;
  			}
  
  			String enabledKey= SemanticHighlightings.getEnabledPreferenceKey(semanticHighlighting);
  			if (enabledKey.equals(event.getProperty())) {
  				adaptToEnablementChange(fHighlightings[i], event);
  				fPresenter.highlightingStyleChanged(fHighlightings[i]);
  				refreshNeeded= true;
  				continue;
  			}
  		}
  		
  		if (refreshNeeded && fReconciler != null)
  			fReconciler.refresh();
  	}
  
  	private void adaptToEnablementChange(Highlighting highlighting, PropertyChangeEvent event) {
  		Object value= event.getNewValue();
  		boolean eventValue;
  		if (value instanceof Boolean)
  			eventValue= ((Boolean) value).booleanValue();
  		else if (IPreferenceStore.TRUE.equals(value))
  			eventValue= true;
  		else
  			eventValue= false;
  		highlighting.setEnabled(eventValue);
  	}
  
  	private void adaptToTextForegroundChange(Highlighting highlighting, PropertyChangeEvent event) {
  		RGB rgb= null;
  
  		Object value= event.getNewValue();
  		if (value instanceof RGB)
  			rgb= (RGB) value;
  		else if (value instanceof String)
  			rgb= StringConverter.asRGB((String) value);
  
  		if (rgb != null) {
  
  			String property= event.getProperty();
  			Color color= fColorManager.getColor(property);
  
  			if ((color == null || !rgb.equals(color.getRGB())) && fColorManager instanceof IColorManagerExtension) {
  				IColorManagerExtension ext= (IColorManagerExtension) fColorManager;
  				ext.unbindColor(property);
  				ext.bindColor(property, rgb);
  				color= fColorManager.getColor(property);
  			}
  
  			TextAttribute oldAttr= highlighting.getTextAttribute();
  			highlighting.setTextAttribute(new TextAttribute(color, oldAttr.getBackground(), oldAttr.getStyle()));
  		}
  	}
  
  	private void adaptToTextStyleChange(Highlighting highlighting, PropertyChangeEvent event, int styleAttribute) {
  		boolean eventValue= false;
  		Object value= event.getNewValue();
  		if (value instanceof Boolean)
  			eventValue= ((Boolean) value).booleanValue();
  		else if (IPreferenceStore.TRUE.equals(value))
  			eventValue= true;
  
  		TextAttribute oldAttr= highlighting.getTextAttribute();
  		boolean activeValue= (oldAttr.getStyle() & styleAttribute) == styleAttribute;
  
  		if (activeValue != eventValue)
  			highlighting.setTextAttribute(new TextAttribute(oldAttr.getForeground(), oldAttr.getBackground(), eventValue ? oldAttr.getStyle() | styleAttribute : oldAttr.getStyle() & ~styleAttribute));
  	}
  
  	private void addColor(String colorKey) {
  		if (fColorManager != null && colorKey != null && fColorManager.getColor(colorKey) == null) {
  			RGB rgb= PreferenceConverter.getColor(fPreferenceStore, colorKey);
  			if (fColorManager instanceof IColorManagerExtension) {
  				IColorManagerExtension ext= (IColorManagerExtension) fColorManager;
  				ext.unbindColor(colorKey);
  				ext.bindColor(colorKey, rgb);
  			}
  		}
  	}
  
  	private void removeColor(String colorKey) {
  		if (fColorManager instanceof IColorManagerExtension)
  			((IColorManagerExtension) fColorManager).unbindColor(colorKey);
  	}
  
  	/**
  	 * Returns this hightlighter's reconciler.
  	 * 
  	 * @return the semantic highlighter reconciler or <code>null</code> if none
  	 * 
  	 */
  	public SemanticHighlightingReconciler getReconciler() {
  		return fReconciler;
  	}
  }
  