/*******************************************************************************
 * Copyright (c) 2001, 2009 IBM Corporation and others.
 * All rights reserved. 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
 *     Jens Lukowski/Innoopract - initial renaming/restructuring
 *     
 *******************************************************************************/
package org.eclipse.wst.sse.ui.internal.reconcile;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.eclipse.core.resources.IMarker;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.Platform;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.reconciler.DirtyRegion;
import org.eclipse.jface.text.reconciler.IReconcileResult;
import org.eclipse.jface.text.reconciler.IReconcileStep;
import org.eclipse.jface.text.reconciler.IReconcilingStrategy;
import org.eclipse.jface.text.reconciler.IReconcilingStrategyExtension;
import org.eclipse.jface.text.source.IAnnotationModel;
import org.eclipse.jface.text.source.IAnnotationModelExtension;
import org.eclipse.jface.text.source.ISourceViewer;
import org.eclipse.jface.text.source.SourceViewer;
import org.eclipse.wst.sse.ui.internal.IReleasable;
import org.eclipse.wst.sse.ui.internal.ITemporaryAnnotation;
import org.eclipse.wst.sse.ui.internal.Logger;
import org.eclipse.wst.sse.ui.internal.StructuredMarkerAnnotation;


/**
 * A base ReconcilingStrategy. Subclasses must implement
 * createReconcileSteps(). This class should not know about
 * IStructuredDocument, only IDocument.
 * 
 * @author pavery
 */
public abstract class AbstractStructuredTextReconcilingStrategy implements IReconcilingStrategy, IReconcilingStrategyExtension, IReleasable {

	/** debug flag */
	protected static final boolean DEBUG;
	static {
		String value = Platform.getDebugOption("org.eclipse.wst.sse.ui/debug/reconcilerjob"); //$NON-NLS-1$
		DEBUG = value != null && value.equalsIgnoreCase("true"); //$NON-NLS-1$
	}

	// these limits are safetys for "runaway" validation cases
	// should be used to safeguard potentially dangerous loops or potentially
	// long annotations
	// (since the painter seems to affect performance when painting long
	// annotations)
	public static final int ANNOTATION_LENGTH_LIMIT = 25;
	public static final int ELEMENT_ERROR_LIMIT = 25;

	private IDocument fDocument = null;
	private IProgressMonitor fProgressMonitor = null;
	private ISourceViewer fSourceViewer = null;

	// list of "validator" annotations
	// for gray/un-gray capability
	private HashSet fMarkerAnnotations = null;

	/**
	 * Creates a new strategy. The source viewer must be set manually
	 * after creation before a reconciler using this constructor will work.
	 */
	public AbstractStructuredTextReconcilingStrategy() {
		init();
	}

	/**
	 * Creates a new strategy.
	 * 
	 * @param editor
	 */
	public AbstractStructuredTextReconcilingStrategy(ISourceViewer sourceViewer) {
		fSourceViewer = sourceViewer;
		init();
	}

	/**
	 * This is where we add results to the annotationModel, doing any special
	 * "extra" processing.
	 */
	protected void addResultToAnnotationModel(IReconcileResult result) {
		if (!(result instanceof TemporaryAnnotation))
			return;
		// can be null when closing the editor
		if (getAnnotationModel() != null) {
			TemporaryAnnotation tempAnnotation = (TemporaryAnnotation) result;

			StructuredMarkerAnnotation sma = getCorrespondingMarkerAnnotation(tempAnnotation);
			if (sma != null) {
				// un-gray out the marker annotation
				sma.setGrayed(false);
			}

			getAnnotationModel().addAnnotation(tempAnnotation, tempAnnotation.getPosition());
		}
	}

	/**
	 * @param object
	 * @return if this strategy is responisble for adding this type of key
	 */
	protected boolean canHandlePartition(String partition) {
		// String[] haystack = getPartitionTypes();
		// for (int i = 0; i < haystack.length; i++) {
		// if (haystack[i].equals(partition))
		// return true;
		// }
		// return false;
		return false;
	}

	// /**
	// * @param step
	// * @return
	// */
	// protected boolean containsStep(IReconcileStep step) {
	// if (fFirstStep instanceof StructuredReconcileStep)
	// return ((StructuredReconcileStep) fFirstStep).isSiblingStep(step);
	// return false;
	// }

	/**
	 * This is where you should create the steps for this strategy
	 */
	abstract public void createReconcileSteps();

	/**
	 * Remove ALL temporary annotations that this strategy can handle.
	 */
	protected TemporaryAnnotation[] getAllAnnotationsToRemove() {
		List removals = new ArrayList();
		IAnnotationModel annotationModel = getAnnotationModel();
		if (annotationModel != null) {
			Iterator i = annotationModel.getAnnotationIterator();
			while (i.hasNext()) {
				Object obj = i.next();
				if (!(obj instanceof ITemporaryAnnotation))
					continue;

				ITemporaryAnnotation annotation = (ITemporaryAnnotation) obj;
				ReconcileAnnotationKey key = (ReconcileAnnotationKey) annotation.getKey();
				// then if this strategy knows how to add/remove this
				// partition type
				if (canHandlePartition(key.getPartitionType()) /*
																 * &&
																 * containsStep(key.getStep())
																 */)
					removals.add(annotation);
			}
		}
		return (TemporaryAnnotation[]) removals.toArray(new TemporaryAnnotation[removals.size()]);
	}

	protected IAnnotationModel getAnnotationModel() {
		IAnnotationModel model = null;
		if (fSourceViewer != null) {
			model = fSourceViewer.getAnnotationModel();
		}
		return model;
	}

	protected TemporaryAnnotation[] getAnnotationsToRemove(DirtyRegion dr, List stepsRun) {

		List remove = new ArrayList();
		IAnnotationModel annotationModel = getAnnotationModel();
		// can be null when closing the editor
		if (getAnnotationModel() != null) {

			// clear validator annotations
			getMarkerAnnotations().clear();

			Iterator i = annotationModel.getAnnotationIterator();
			while (i.hasNext()) {

				Object obj = i.next();

				// check if it's a validator marker annotation
				// if it is save it for comparision later (to "gray" icons)
				if (obj instanceof StructuredMarkerAnnotation) {
					StructuredMarkerAnnotation sma = (StructuredMarkerAnnotation) obj;

					if (sma.getAnnotationType() == TemporaryAnnotation.ANNOT_ERROR || sma.getAnnotationType() == TemporaryAnnotation.ANNOT_WARNING)
						fMarkerAnnotations.add(sma);
				}

				if (!(obj instanceof TemporaryAnnotation))
					continue;

				TemporaryAnnotation annotation = (TemporaryAnnotation) obj;
				ReconcileAnnotationKey key = (ReconcileAnnotationKey) annotation.getKey();

				// then if this strategy knows how to add/remove this
				// partition type
				if (canHandlePartition(key.getPartitionType()) && stepsRun.contains(key.getStep())) {
					if (key.getScope() == ReconcileAnnotationKey.PARTIAL && annotation.getPosition().overlapsWith(dr.getOffset(), dr.getLength())) {
						remove.add(annotation);
					}
					else if (key.getScope() == ReconcileAnnotationKey.TOTAL) {
						remove.add(annotation);
					}
				}
			}
		}
		return (TemporaryAnnotation[]) remove.toArray(new TemporaryAnnotation[remove.size()]);
	}


	protected abstract boolean containsStep(IReconcileStep step);

	/**
	 * Gets partition types from all steps in this strategy.
	 * 
	 * @return parition types from all steps
	 */
	// public String[] getPartitionTypes() {
	// if (fFirstStep instanceof StructuredReconcileStep)
	// return ((StructuredReconcileStep) fFirstStep).getPartitionTypes();
	// return new String[0];
	// }
	public void init() {
		createReconcileSteps();
	}

	/**
	 * @see org.eclipse.jface.text.reconciler.IReconcilingStrategyExtension#initialReconcile()
	 */
	public void initialReconcile() {
		// do nothing
	}

	/**
	 * @return
	 */
	protected boolean isCanceled() {
		if (DEBUG && (fProgressMonitor != null && fProgressMonitor.isCanceled()))
			System.out.println("** STRATEGY CANCELED **:" + this.getClass().getName()); //$NON-NLS-1$
		return fProgressMonitor != null && fProgressMonitor.isCanceled();
	}

	/**
	 * Process the results from the reconcile steps in this strategy.
	 * 
	 * @param results
	 */
	private void process(final IReconcileResult[] results) {
		if (DEBUG)
			System.out.println("[trace reconciler] > STARTING PROCESS METHOD with (" + results.length + ") results"); //$NON-NLS-1$ //$NON-NLS-2$

		if (results == null)
			return;

		for (int i = 0; i < results.length && i < ELEMENT_ERROR_LIMIT && !isCanceled(); i++) {

			if (isCanceled()) {
				if (DEBUG)
					System.out.println("[trace reconciler] >** PROCESS (adding) WAS CANCELLED **"); //$NON-NLS-1$
				return;
			}
			addResultToAnnotationModel(results[i]);
		}

		if (DEBUG) {
			StringBuffer traceString = new StringBuffer();
			for (int j = 0; j < results.length; j++)
				traceString.append("\n (+) :" + results[j] + ":\n"); //$NON-NLS-1$ //$NON-NLS-2$
			System.out.println("[trace reconciler] > PROCESSING (" + results.length + ") results in AbstractStructuredTextReconcilingStrategy " + traceString); //$NON-NLS-1$ //$NON-NLS-2$
		}
	}

	/**
	 * @see org.eclipse.jface.text.reconciler.IReconcilingStrategy#reconcile(org.eclipse.jface.text.reconciler.DirtyRegion,
	 *      org.eclipse.jface.text.IRegion)
	 */
	public void reconcile(DirtyRegion dirtyRegion, IRegion subRegion) {
		// not used
		// we only have validator strategy now

		// // external files may be null
		// if (isCanceled() || fFirstStep == null)
		// return;
		//
		// TemporaryAnnotation[] annotationsToRemove = new
		// TemporaryAnnotation[0];
		// IReconcileResult[] annotationsToAdd = new IReconcileResult[0];
		// StructuredReconcileStep structuredStep = (StructuredReconcileStep)
		// fFirstStep;
		//        
		// annotationsToRemove = getAnnotationsToRemove(dirtyRegion);
		// annotationsToAdd = structuredStep.reconcile(dirtyRegion,
		// subRegion);
		//        
		// smartProcess(annotationsToRemove, annotationsToAdd);
	}

	/**
	 * @param partition
	 * @see org.eclipse.jface.text.reconciler.IReconcilingStrategy#reconcile(org.eclipse.jface.text.IRegion)
	 */
	public void reconcile(IRegion partition) {
		// not used, we use:
		// reconcile(DirtyRegion dirtyRegion, IRegion subRegion)
	}

	/**
	 * Calls release() on all the steps in this strategy. Currently done in
	 * StructuredRegionProcessor.SourceWidgetDisposeListener#widgetDisposed(...)
	 */
	public void release() {
		// release steps (each step calls release on the next)
		// if (fFirstStep != null && fFirstStep instanceof IReleasable)
		// ((IReleasable) fFirstStep).release();
		// we don't to null out the steps, in case
		// it's reconfigured later
	}

	private void removeAnnotations(TemporaryAnnotation[] annotationsToRemove) {

		IAnnotationModel annotationModel = getAnnotationModel();
		// can be null when closing the editor
		if (annotationModel != null) {
			for (int i = 0; i < annotationsToRemove.length; i++) {
				if (isCanceled()) {
					if (DEBUG)
						System.out.println("[trace reconciler] >** REMOVAL WAS CANCELLED **"); //$NON-NLS-1$
					return;
				}
				StructuredMarkerAnnotation sma = getCorrespondingMarkerAnnotation(annotationsToRemove[i]);
				if (sma != null) {
					// gray out the marker annotation
					sma.setGrayed(true);
				}
				// remove the temp one
				annotationModel.removeAnnotation(annotationsToRemove[i]);

			}
		}

		if (DEBUG) {
			StringBuffer traceString = new StringBuffer();
			for (int i = 0; i < annotationsToRemove.length; i++)
				traceString.append("\n (-) :" + annotationsToRemove[i] + ":\n"); //$NON-NLS-1$ //$NON-NLS-2$
			System.out.println("[trace reconciler] > REMOVED (" + annotationsToRemove.length + ") annotations in AbstractStructuredTextReconcilingStrategy :" + traceString); //$NON-NLS-1$ //$NON-NLS-2$
		}
	}

	private StructuredMarkerAnnotation getCorrespondingMarkerAnnotation(TemporaryAnnotation tempAnnotation) {

		Iterator it = getMarkerAnnotations().iterator();
		while (it.hasNext()) {
			StructuredMarkerAnnotation markerAnnotation = (StructuredMarkerAnnotation) it.next();
			String message = ""; //$NON-NLS-1$
			try {
				message = (String) markerAnnotation.getMarker().getAttribute(IMarker.MESSAGE);
			}
			catch (CoreException e) {
				if (DEBUG)
					Logger.logException(e);
			}
			// it would be nice to check line number here...
			if (message != null && message.equals(tempAnnotation.getText()))
				return markerAnnotation;
		}
		return null;
	}

	private void removeAllAnnotations() {
		removeAnnotations(getAllAnnotationsToRemove());
	}

	/**
	 * The user needs to manually set the viewer if the default
	 * constructor was used. 
	 *
	 * @param viewer
	 */
	public void setViewer(SourceViewer viewer) {
		fSourceViewer = viewer;
	}

	/**
	 * Set the document for this strategy.
	 * 
	 * @param document
	 * @see org.eclipse.jface.text.reconciler.IReconcilingStrategy#setDocument(org.eclipse.jface.text.IDocument)
	 */
	public void setDocument(IDocument document) {

		// remove all old annotations since it's a new document
		removeAllAnnotations();

		if (document == null)
			release();

		// if (getFirstStep() != null)
		// getFirstStep().setInputModel(new DocumentAdapter(document));

		fDocument = document;
	}

	public IDocument getDocument() {
		return fDocument;
	}

	/**
	 * @param monitor
	 * @see org.eclipse.jface.text.reconciler.IReconcilingStrategyExtension#setProgressMonitor(org.eclipse.core.runtime.IProgressMonitor)
	 */
	public void setProgressMonitor(IProgressMonitor monitor) {
		// fProgressMonitor = monitor;
		// if (fFirstStep != null)
		// fFirstStep.setProgressMonitor(fProgressMonitor);
	}

	/**
	 * Check if the annotation is already there, if it is, no need to remove
	 * or add again. This will avoid a lot of "flickering" behavior.
	 * 
	 * @param annotationsToRemove
	 * @param annotationsToAdd
	 */
	protected void smartProcess(TemporaryAnnotation[] annotationsToRemove, IReconcileResult[] annotationsToAdd) {
//		Comparator comp = getTemporaryAnnotationComparator();
//		List sortedRemovals = Arrays.asList(annotationsToRemove);
//		Collections.sort(sortedRemovals, comp);
//
//		List sortedAdditions = Arrays.asList(annotationsToAdd);
//		Collections.sort(sortedAdditions, comp);
//
//		List filteredRemovals = new ArrayList(sortedRemovals);
//		List filteredAdditions = new ArrayList(sortedAdditions);
//
//		boolean ignore = false;
//		int lastFoundAdded = 0;
//		for (int i = 0; i < sortedRemovals.size(); i++) {
//			TemporaryAnnotation removal = (TemporaryAnnotation) sortedRemovals.get(i);
//			for (int j = lastFoundAdded; j < sortedAdditions.size(); j++) {
//				TemporaryAnnotation addition = (TemporaryAnnotation) sortedAdditions.get(j);
//				// quick position check here
//				if (removal.getPosition().equals(addition.getPosition())) {
//					lastFoundAdded = j;
//					// remove performs TemporaryAnnotation.equals()
//					// which checks text as well
//					filteredAdditions.remove(addition);
//					ignore = true;
//					if (DEBUG)
//						System.out.println(" ~ smart process ignoring: " + removal.getPosition().getOffset()); //$NON-NLS-1$
//					break;
//				}
//			}
//			if (ignore) {
//				filteredRemovals.remove(removal);
//			}
//			ignore = false;
//		}
		if (getAnnotationModel() instanceof IAnnotationModelExtension) {
//			TemporaryAnnotation[] filteredRemovalArray = (TemporaryAnnotation[]) filteredRemovals.toArray(new TemporaryAnnotation[filteredRemovals.size()]);
//			// apply "grey"-ness
//			for (int i = 0; i < filteredRemovalArray.length; i++) {
//				if (isCanceled()) {
//					if (DEBUG)
//						System.out.println("[trace reconciler] >** replacing WAS CANCELLED **"); //$NON-NLS-1$
//					return;
//				}
//				StructuredMarkerAnnotation sma = getCorrespondingMarkerAnnotation(filteredRemovalArray[i]);
//				if (sma != null) {
//					// gray out the marker annotation
//					sma.setGrayed(true);
//				}
//			}
//			Map annotationsToAddMap = new HashMap();
//			for (int i = 0; i < filteredAdditions.size(); i++) {
//				TemporaryAnnotation temporaryAnnotation = (TemporaryAnnotation) filteredAdditions.get(i);
//				annotationsToAddMap.put(temporaryAnnotation, temporaryAnnotation.getPosition());
//			}
//			if (isCanceled()) {
//				if (DEBUG)
//					System.out.println("[trace reconciler] >** PROCESS (replacing) WAS CANCELLED **"); //$NON-NLS-1$
//				return;
//			}
//			/*
//			 * Using the extension means we can't enforce the
//			 * ELEMENT_ERROR_LIMIT limit.
//			 */
//			((IAnnotationModelExtension) getAnnotationModel()).replaceAnnotations(filteredRemovalArray, annotationsToAddMap);

			Map annotationsToAddMap = new HashMap();
			for (int i = 0; i < annotationsToAdd.length; i++) {
				TemporaryAnnotation temporaryAnnotation = (TemporaryAnnotation) annotationsToAdd[i];
				annotationsToAddMap.put(temporaryAnnotation, temporaryAnnotation.getPosition());
			}
			if (isCanceled()) {
				if (DEBUG)
					System.out.println("[trace reconciler] >** PROCESS (replacing) WAS CANCELLED **"); //$NON-NLS-1$
				return;
			}
			((IAnnotationModelExtension) getAnnotationModel()).replaceAnnotations(annotationsToRemove, annotationsToAddMap);
		}
		else {
//			removeAnnotations((TemporaryAnnotation[]) filteredRemovals.toArray(new TemporaryAnnotation[filteredRemovals.size()]));
//			process((IReconcileResult[]) filteredAdditions.toArray(new IReconcileResult[filteredAdditions.size()]));
			removeAnnotations(annotationsToRemove);
			process(annotationsToAdd);
		}
	}

//	private Comparator getTemporaryAnnotationComparator() {
//		if (fComparator == null) {
//			fComparator = new Comparator() {
//				public int compare(Object arg0, Object arg1) {
//					TemporaryAnnotation ta1 = (TemporaryAnnotation) arg0;
//					TemporaryAnnotation ta2 = (TemporaryAnnotation) arg1;
//					int result = ta1.getPosition().getOffset() - ta2.getPosition().getOffset();
//					if(result != 0)
//						return result;
//					return Collator.getInstance().compare(ta1.getText(), ta2.getText());
//				}
//			};
//		}
//		return fComparator;
//	}

	public HashSet getMarkerAnnotations() {
		if (fMarkerAnnotations == null)
			fMarkerAnnotations = new HashSet();
		return fMarkerAnnotations;
	}
}
