/*******************************************************************************
 * Copyright (c) 2017 the TeXlipse team 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:
 *     The TeXlipse team - initial API and implementation
 *******************************************************************************/
package org.eclipse.texlipse.editor;

import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.ITextSelection;
import org.eclipse.jface.text.Position;
import org.eclipse.jface.text.Region;
import org.eclipse.jface.text.source.Annotation;
import org.eclipse.jface.text.source.IAnnotationModel;
import org.eclipse.jface.text.source.ISourceViewer;
import org.eclipse.jface.util.IPropertyChangeListener;
import org.eclipse.jface.util.PropertyChangeEvent;
import org.eclipse.jface.viewers.IPostSelectionProvider;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.texlipse.TexlipsePlugin;
import org.eclipse.texlipse.properties.TexlipseProperties;
import org.eclipse.texlipse.texparser.LatexParserUtils;
import org.eclipse.ui.texteditor.AbstractTextEditor;

/**
 * This class implements a PostSelectionChangeListener which creates annotations
 * to highlight
 * <ul>
 * <li>associated begin or end</li>
 * <li>all references of a label</li>
 * </ul>
 * in the current document.
 * 
 * @author Boris von Loesch
 *
 */
public class TexlipseAnnotationUpdater implements ISelectionChangedListener {

    private final List<Annotation> fOldAnnotations= new LinkedList<Annotation>();
    private AbstractTextEditor fEditor;
    private Job fUpdateJob;
    private final static String ANNOTATION_TYPE = "org.eclipse.texlipse.defAnnotation";
    private boolean fEnabled;

    /**
     * Creates a new TexlipseAnnotationUpdater and adds itself to the TexEditor via
     * <code>addPostSelectionChangedListener</code>
     * @param editor The TexEditor
     */
    public TexlipseAnnotationUpdater (AbstractTextEditor editor) {
        //Add this listener to the current editors IPostSelectionListener (lazy update)
        ((IPostSelectionProvider) editor.getSelectionProvider()).addPostSelectionChangedListener(this);
        fEditor = editor;
        fEnabled = TexlipsePlugin.getDefault().getPreferenceStore().getBoolean(
                TexlipseProperties.TEX_EDITOR_ANNOTATATIONS);
        
        //Add a PropertyChangeListener
        TexlipsePlugin.getDefault().getPreferenceStore().addPropertyChangeListener(new  
                IPropertyChangeListener() {
            
            public void propertyChange(PropertyChangeEvent event) {    
                String property = event.getProperty();
                if (TexlipseProperties.TEX_EDITOR_ANNOTATATIONS.equals(property)) {
                    boolean enabled = TexlipsePlugin.getDefault().getPreferenceStore().getBoolean(
                                    TexlipseProperties.TEX_EDITOR_ANNOTATATIONS);
                    fEnabled = enabled;
                }
            }
        });
    }


    public void selectionChanged(SelectionChangedEvent event) {
        update((ISourceViewer) event.getSource());
    }


    /**
     * Updates the annotations. It first checks if the current selection is
     * already annotated, if not it clears all annotations and tries to detect
     * if the current selection is part of a \[a-zA-Z]*ref, \label, \begin{...}
     * or \end{...} string. If the last is true, it searches with regular expressions
     * to find the associated part(s) and highlights them (The last uses a non UI-Job
     * which do not influence the responsiveness of the editor). 
     * 
     * @param viewer
     */
    private void update(ISourceViewer viewer) {
        final IDocument document = viewer.getDocument();
        final IAnnotationModel model = viewer.getAnnotationModel();
        ISelection selection = fEditor.getSelectionProvider().getSelection();
        
        if (testSelection(selection, model)) return;
        
        if (fUpdateJob != null) {
            fUpdateJob.cancel();
        }
        removeOldAnnotations(model);
        
        if (!fEnabled) { 
            //Feature is turned off, but we have to delete the old annotations 
            return;
        }
        
        if (selection instanceof ITextSelection) {
            try {
                //TODO Split this and create new classes for the different annotations
                final ITextSelection textSelection = (ITextSelection) selection;
                final int offset = textSelection.getOffset();
                final int lineNr = document.getLineOfOffset(offset);
                final int lineOff = document.getLineOffset(lineNr);
                final String line = document.get(lineOff, document.getLineLength(lineNr));
                IRegion r = LatexParserUtils.getCommand(line, offset - lineOff);
                if (r == null) return;
                
                final String command = line.substring(r.getOffset(), r.getOffset() + r.getLength()).trim();
                if ("\\begin".equals(command) || "\\end".equals(command)) {
                    //TODO Its maybe better/faster to use the AST here
                    IRegion r2 = LatexParserUtils.getCommandArgument(line, r.getOffset());
                    if (r2 == null) return;

                    final IRegion startRegion = new Region(lineOff + r.getOffset(), r2.getOffset() + r2.getLength() - r.getOffset() + 1);

                    final String refName = line.substring(r2.getOffset(), r2.getOffset() + r2.getLength());
                    //Create a job to update the annotations in the background
                    fUpdateJob = createMatchEnvironmentJob(document, model, offset, command, startRegion, refName);
                    fUpdateJob.setPriority(Job.DECORATE);
                    fUpdateJob.setSystem(true);
                    fUpdateJob.schedule();
                }
                else if (command.endsWith("ref") || "\\label".equals(command)) {
                    //TODO Its maybe better/faster to use the AST here
                    IRegion r2 = LatexParserUtils.getCommandArgument(line, r.getOffset());
                    if (r2 == null) return;

                    final String refName = line.substring(r2.getOffset(), r2.getOffset() + r2.getLength());
                    //Create a job to update the annotations in the background
                    fUpdateJob = createMatchReferenceJob(document, model, refName);
                    fUpdateJob.setPriority(Job.DECORATE);
                    fUpdateJob.setSystem(true);
                    fUpdateJob.schedule();
                }
            } catch (BadLocationException ex) {
                //Do not inform the user cause this is only a decorator
            }
        }
    }

    /**
     * Creates and returns a background job which searches and highlights all \label and \*ref. 
     * @param document
     * @param model
     * @param refName   The name of the reference
     * @return The job
     */
    private Job createMatchReferenceJob(final IDocument document, final IAnnotationModel model, final String refName) {
        return
            new Job("Update Annotations") {
                public IStatus run(IProgressMonitor monitor) {
                    String text = document.get();
                    String refNameRegExp = refName.replaceAll("\\*", "\\\\*");
                    final String simpleRefRegExp = "\\\\([a-zA-Z]*ref|label)\\s*\\{" + refNameRegExp + "\\}";
                    Matcher m = (Pattern.compile(simpleRefRegExp)).matcher(text);
                    while (m.find()) {
                        if (monitor.isCanceled()) return Status.CANCEL_STATUS;
                        IRegion match = LatexParserUtils.getCommand(text, m.start());
                        //Test if it is a real LaTeX command
                        if (match != null) {
                            IRegion fi = new Region(m.start(), m.end()-m.start());
                            createNewAnnotation(fi, "References", model);
                        }
                    }
                    return Status.OK_STATUS;
                }
        };
    }

    /**
     * Creates and returns a new background job which searches and highlights the matching \end or \begin environment.
     * @param document
     * @param model
     * @param offset        The offset of the selection (cursor)
     * @param command       \begin or \end
     * @param startRegion   A region which contains the command and the argument (e.g \begin{environment})
     * @param envName       The name of the environment
     * @return  The Job
     */
    private Job createMatchEnvironmentJob(final IDocument document, final IAnnotationModel model, final int offset, 
            final String command, final IRegion startRegion, final String envName) {
        return new Job("Update Annotations") {
                public IStatus run(IProgressMonitor monitor) {
                    String text = document.get();
                    boolean forward = false;
                    if ("\\begin".equals(command)) forward = true;
                    if (forward) {
                        IRegion endRegion = LatexParserUtils.findMatchingEndEnvironment(text, envName, startRegion.getOffset());
                        if (endRegion != null) {
                            createNewAnnotation(endRegion, "Environment", model);
                            createNewAnnotation(startRegion, "Environment", model);
                        }
                    } else {
                        IRegion endRegion = LatexParserUtils.findMatchingBeginEnvironment(text, envName, startRegion.getOffset());
                        if (endRegion != null) {
                            createNewAnnotation(endRegion, "Environment", model);
                            createNewAnnotation(startRegion, "Environment", model);
                        }
                    }
                    return Status.OK_STATUS;
                }
        };
    }


    /**
     * Tests if the selection is already annotated
     * @param selection current selection
     * @param model The AnnotationModel
     * @return true, if selection is already annotated
     */
    private boolean testSelection (ISelection selection, IAnnotationModel model) {
        if (selection instanceof ITextSelection) {
            final ITextSelection textSelection = (ITextSelection) selection;
            //Iterate over all existing annotations
            for (Iterator<Annotation> iter = fOldAnnotations.iterator(); iter.hasNext();) {
                Annotation anno = iter.next();
                Position p = model.getPosition(anno);
                if (p != null && p.offset <= textSelection.getOffset() && p.offset+p.length >= textSelection.getOffset()) { 
                    return true;
                }
            }
        }
        return false;
    }
    
    /**
     * Removes all existing annotations
     * @param model AnnotationModel
     */
    private void removeOldAnnotations(IAnnotationModel model) {

        for (Iterator<Annotation> it= fOldAnnotations.iterator(); it.hasNext();) {
            Annotation annotation= (Annotation) it.next();
            model.removeAnnotation(annotation);
        }

        fOldAnnotations.clear();
    }

    /**
     * Creates a new annotation
     * @param r The IRegion which should be highlighted
     * @param annString The name of the annotation (not important)
     * @param model The AnnotationModel
     */
    private void createNewAnnotation(IRegion r, String annString, IAnnotationModel model) {
            Annotation annotation= new Annotation(ANNOTATION_TYPE, false, annString);
            Position position= new Position(r.getOffset(), r.getLength());
            model.addAnnotation(annotation, position);
            fOldAnnotations.add(annotation);
    }
    
}