/**
 * Copyright (c) 2005-2013 by Appcelerator, Inc. All Rights Reserved.
 * Licensed under the terms of the Eclipse Public License (EPL).
 * Please see the license.txt included with this distribution for details.
 * Any modifications to this file must keep this entire header intact.
 */
package org.python.pydev.editor;

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.runtime.Assert;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.content.IContentType;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.ISynchronizable;
import org.eclipse.jface.text.ITypedRegion;
import org.eclipse.jface.text.Position;
import org.eclipse.jface.text.Region;
import org.eclipse.jface.text.TextUtilities;
import org.eclipse.jface.text.reconciler.DirtyRegion;
import org.eclipse.jface.text.reconciler.IReconcilingStrategy;
import org.eclipse.jface.text.reconciler.IReconcilingStrategyExtension;
import org.eclipse.jface.text.source.Annotation;
import org.eclipse.jface.text.source.IAnnotationModel;
import org.eclipse.jface.text.source.IAnnotationModelExtension;
import org.eclipse.jface.text.source.ISourceViewer;
import org.eclipse.ui.texteditor.spelling.ISpellingProblemCollector;
import org.eclipse.ui.texteditor.spelling.SpellingAnnotation;
import org.eclipse.ui.texteditor.spelling.SpellingContext;
import org.eclipse.ui.texteditor.spelling.SpellingProblem;
import org.eclipse.ui.texteditor.spelling.SpellingService;
import org.python.pydev.core.IPythonPartitions;
import org.python.pydev.core.log.Log;

/**
 * Based on SpellingReconcileStrategy.
 * 
 * Reconcile strategy used for spell checking.
 */
public class PyReconciler implements IReconcilingStrategy, IReconcilingStrategyExtension {

    /**
     * Spelling problem collector.
     */
    private class SpellingProblemCollector implements ISpellingProblemCollector {

        /** Annotation model. */
        private IAnnotationModel fAnnotationModel;

        /** Annotations to add. */
        private Map<SpellingAnnotation, Position> fAddAnnotations;

        /**
         * Initializes this collector with the given annotation model.
         *
         * @param annotationModel the annotation model
         */
        public SpellingProblemCollector(IAnnotationModel annotationModel) {
            Assert.isLegal(annotationModel != null);
            fAnnotationModel = annotationModel;
        }

        /*
         * @see org.eclipse.ui.texteditor.spelling.ISpellingProblemCollector#accept(org.eclipse.ui.texteditor.spelling.SpellingProblem)
         */
        @Override
        public void accept(SpellingProblem problem) {
            fAddAnnotations
                    .put(new SpellingAnnotation(problem), new Position(problem.getOffset(), problem.getLength()));
        }

        /*
         * @see org.eclipse.ui.texteditor.spelling.ISpellingProblemCollector#beginCollecting()
         */
        @Override
        public void beginCollecting() {
            fAddAnnotations = new HashMap<SpellingAnnotation, Position>();
        }

        /*
         * @see org.eclipse.ui.texteditor.spelling.ISpellingProblemCollector#endCollecting()
         */
        @Override
        public void endCollecting() {

            List<Object> toRemove = new ArrayList<Object>();

            Object fLockObject;
            if (fAnnotationModel instanceof ISynchronizable) {
                fLockObject = ((ISynchronizable) fAnnotationModel).getLockObject();
            } else {
                fLockObject = new Object();
            }

            //let other threads execute before getting the lock on the annotation model
            Thread.yield();

            Thread thread = Thread.currentThread();
            int initiaThreadlPriority = thread.getPriority();
            try {
                //before getting the lock, let's execute with normal priority, to optimize the time that we'll 
                //retain that object locked (the annotation model is used on lots of places, so, retaining the lock
                //on it on a minimum priority thread is not a good thing.
                thread.setPriority(Thread.NORM_PRIORITY);
                Iterator<SpellingAnnotation> iter;

                synchronized (fLockObject) {
                    iter = fAnnotationModel.getAnnotationIterator();
                    while (iter.hasNext()) {
                        Object n = iter.next();
                        if (n instanceof SpellingAnnotation) {
                            toRemove.add(n);
                        }
                    }
                    iter = null;
                }

                Annotation[] annotationsToRemove = toRemove.toArray(new Annotation[toRemove.size()]);

                //let other threads execute before getting the lock (again) on the annotation model
                Thread.yield();
                synchronized (fLockObject) {
                    if (fAnnotationModel instanceof IAnnotationModelExtension) {
                        ((IAnnotationModelExtension) fAnnotationModel).replaceAnnotations(annotationsToRemove,
                                fAddAnnotations);
                    } else {
                        for (int i = 0; i < annotationsToRemove.length; i++) {
                            fAnnotationModel.removeAnnotation(annotationsToRemove[i]);
                        }
                        for (iter = fAddAnnotations.keySet().iterator(); iter.hasNext();) {
                            Annotation annotation = iter.next();
                            fAnnotationModel.addAnnotation(annotation, fAddAnnotations.get(annotation));
                        }
                    }
                }

            } finally {
                thread.setPriority(initiaThreadlPriority);
            }
            fAddAnnotations = null;
        }
    }

    /** The text editor to operate on. */
    private ISourceViewer fViewer;

    /** The document to operate on. */
    private IDocument fDocument;

    /** The progress monitor. */
    private IProgressMonitor fProgressMonitor;

    private SpellingService fSpellingService;

    /** The spelling context containing the Java source content type. */
    private SpellingContext fSpellingContext;

    /**
     * Set containing the models that are being checked at the current moment (this is used
     * so that when there are multiple editors binded to the same model, we only make the check in one of those
     * models -- the others don't need to do anything, as it's based on the annotation model that's shared
     * among them)
     * 
     * It's static so that we can share it among threads.
     */
    private static HashSet<IAnnotationModel> modelBeingChecked = new HashSet<IAnnotationModel>();

    /**
     * Creates a new comment reconcile strategy.
     * 
     * @param viewer the source viewer
     * @param spellingService the spelling service to use
     */
    public PyReconciler(ISourceViewer viewer, SpellingService spellingService) {
        Assert.isNotNull(viewer);
        Assert.isNotNull(spellingService);
        fViewer = viewer;
        fSpellingService = spellingService;
        fSpellingContext = new SpellingContext();
        fSpellingContext.setContentType(getContentType());
    }

    /*
     * @see org.eclipse.jface.text.reconciler.IReconcilingStrategyExtension#initialReconcile()
     */
    @Override
    public void initialReconcile() {
        reconcile(new Region(0, fDocument.getLength()));
    }

    /*
     * Currently only calls the 'whole doc reconcile'.
     * 
     * To make it work we'd need to get the dirtyRegion + region of the text next to it, remove annotations matching and
     * do a new reconcile in that region.
     * 
     * Note that the dirty region marks whether it was a removal or insertion.
     * 
     * @see org.eclipse.jface.text.reconciler.IReconcilingStrategy#reconcile(org.eclipse.jface.text.reconciler.DirtyRegion,org.eclipse.jface.text.IRegion)
     */
    @Override
    public void reconcile(DirtyRegion dirtyRegion, IRegion subRegion) {
        reconcile(subRegion);
    }

    /*
     * @see org.eclipse.jface.text.reconciler.IReconcilingStrategy#reconcile(org.eclipse.jface.text.IRegion)
     */
    @Override
    public void reconcile(IRegion region) {
        IAnnotationModel annotationModel = fViewer.getAnnotationModel();

        if (annotationModel == null) {
            return;
        }

        //Bug: https://sourceforge.net/tracker/index.php?func=detail&aid=2013310&group_id=85796&atid=577329
        //When having multiple editors for the same document, only one of the reconcilers actually needs to
        //work (because the others are binded to the same annotation model, so, having one do the work is enough)
        synchronized (modelBeingChecked) {
            if (modelBeingChecked.contains(annotationModel)) {
                return;
            }
            modelBeingChecked.add(annotationModel);
        }

        try {
            //we're not using incremental updates!!! -- that's why the region is ignored and fDocument.len is used.
            //PyEditConfiguration#getReconciler should also be configured for that if needed.
            ITypedRegion[] partitions = TextUtilities.computePartitioning(fDocument,
                    IPythonPartitions.PYTHON_PARTITION_TYPE, 0, fDocument.getLength(), false);

            ArrayList<IRegion> regions = new ArrayList<IRegion>();
            for (ITypedRegion partition : partitions) {
                if (fProgressMonitor != null && fProgressMonitor.isCanceled()) {
                    return;
                }

                String type = partition.getType();

                if (!type.equals(IPythonPartitions.PY_DEFAULT) && !type.equals(IPythonPartitions.PY_BACKQUOTES)) {
                    //only calculate for regions that are strings and comments.
                    regions.add(new Region(partition.getOffset(), partition.getLength()));
                }
            }

            int size = regions.size();
            if (size > 0) {
                //only create the collector when actually needed for the current model
                ISpellingProblemCollector spellingProblemCollector = new SpellingProblemCollector(annotationModel);
                fSpellingService.check(fDocument, regions.toArray(new IRegion[size]), fSpellingContext,
                        spellingProblemCollector, fProgressMonitor);
            }

        } catch (BadLocationException e) {
            //Ignore: can happen if the document changes during the reconciling.

        } catch (Exception e) {
            Log.log(e);

        } finally {
            synchronized (modelBeingChecked) {
                modelBeingChecked.remove(annotationModel);
            }
        }
    }

    /**
     * Returns the content type of the underlying editor input.
     *
     * @return the content type of the underlying editor input or
     *         <code>null</code> if none could be determined
     */
    protected IContentType getContentType() {
        return Platform.getContentTypeManager().getContentType("org.python.pydev.pythonfile");
    }

    /*
     * @see org.eclipse.jface.text.reconciler.IReconcilingStrategy#setDocument(org.eclipse.jface.text.IDocument)
     */
    @Override
    public void setDocument(IDocument document) {
        fDocument = document;

        //Note: if we have multiple editors for the same doc, the document and the annotation model will be the same 
        //for multiple reconcilers
    }

    /*
     * @see org.eclipse.jface.text.reconciler.IReconcilingStrategyExtension#setProgressMonitor(org.eclipse.core.runtime.IProgressMonitor)
     */
    @Override
    public final void setProgressMonitor(IProgressMonitor monitor) {
        fProgressMonitor = monitor;
    }

}
