/******************************************************************************
* Copyright (C) 2013  Fabio Zadrozny 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:
*     Fabio Zadrozny <fabiofz@gmail.com>       - initial API and implementation
*     Alexander Kurtakov <akurtako@redhat.com> - ongoing maintenance
******************************************************************************/
package org.python.pydev.shared_core.parsing;

import java.util.ArrayList;
import java.util.List;

import org.eclipse.core.resources.IMarker;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.jface.text.DocumentEvent;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IDocumentListener;
import org.python.pydev.shared_core.log.Log;
import org.python.pydev.shared_core.model.ISimpleNode;
import org.python.pydev.shared_core.structure.Tuple;

/**
 * PyParser uses org.python.parser to parse the document (lexical analysis) It
 * is attached to PyEdit (a view), and it listens to document changes On every
 * document change, the syntax tree is regenerated The reparsing of the document
 * is done on a ParsingThread
 * 
 * Clients that need to know when new parse tree has been generated should
 * register as parseListeners.
 */

public abstract class BaseParser implements IParser {

    /**
     * just for tests, when we don't have any editor
     */
    public static boolean ACCEPT_NULL_INPUT_EDITOR = false;

    /**
     * this is the document we should parse 
     */
    protected volatile IDocument document;

    /**
     * ast for the last successful parsing
     */
    protected ISimpleNode root = null;

    /**
     * listens to changes in the document
     */
    protected IDocumentListener documentListener;

    /**
     * listeners that get notified of successful or unsuccessful parser achievements
     */
    protected ArrayList<IParserObserver> parserListeners;

    /**
     * this is the object that will keep parser schedules for us (and will call us for doing parsing when requested)
     */
    protected ParserScheduler scheduler;

    /**
     * indicates we should do analysis only on doc save
     */
    protected boolean useAnalysisOnlyOnDocSave;

    /**
     * Identifies whether this parser is disposed.
     */
    protected volatile boolean disposed = false;

    /**
     * Should only be called for testing. Does not register as a thread.
     */
    protected BaseParser(BaseParserManager parseManager) {

        parserListeners = new ArrayList<IParserObserver>();
        scheduler = new ParserScheduler(this, parseManager);

        documentListener = new IDocumentListener() {

            @Override
            public void documentChanged(DocumentEvent event) {
                if (useAnalysisOnlyOnDocSave) {
                    //if we're doing analysis only on doc change, the parser will not give any changes
                    //to the scheduler, so, we won't have any parse events to respond to
                    return;

                }
                String text = event.getText();

                boolean parseNow = true;
                if (event == null || text == null) {
                    parseNow = false;
                }
                if (parseNow) {
                    if (text.indexOf("\n") == -1 && text.indexOf("\r") == -1) {
                        parseNow = false;

                    }
                }

                if (!parseNow) {
                    // carriage return in changed text means parse now, anything
                    // else means parse later
                    scheduler.parseLater();
                } else {
                    scheduler.parseNow();
                }
            }

            @Override
            public void documentAboutToBeChanged(DocumentEvent event) {
            }
        };

    }

    /**
     * should be called when the editor is disposed
     */
    @Override
    public void dispose() {
        this.disposed = true;
        this.scheduler.dispose();

        // remove the listeners
        if (document != null) {
            document.removeDocumentListener(documentListener);
        }
        synchronized (parserListeners) {
            parserListeners.clear();
        }
    }

    public ISimpleNode getRoot() {
        return root;
    }

    @Override
    public void notifySaved() {
        //force parse on save
        forceReparse();
    }

    /**
     * @return false if we asked a reparse and it will not be scheduled because a reparse is already in action.
     */
    @Override
    public boolean forceReparse(Object... argsToReparse) {
        if (disposed) {
            return true; //reparse didn't happen, but no matter what happens, it won't happen anyways
        }
        return scheduler.parseNow(true, argsToReparse);
    }

    /**
     * This is the input from the editor that we're using in the parse
     */
    protected/*IEditorInput*/Object input;

    @Override
    public void setDocument(IDocument document, Object input) {
        setDocument(document, true, input);
    }

    public synchronized void setDocument(IDocument doc, boolean addToScheduler, Object input) {
        this.input = input;
        // Cleans up old listeners
        if (this.document != null) {
            this.document.removeDocumentListener(documentListener);
        }

        // Set up new listener
        this.document = doc;
        if (doc == null) {
            Log.log("No document in PyParser::setDocument?");
            return;
        }

        doc.addDocumentListener(documentListener);

        if (addToScheduler) {
            // Reparse document on the initial set (force it)
            scheduler.parseNow(true);
        }
    }

    // ---------------------------------------------------------------------------- listeners
    /** stock listener implementation */
    @Override
    public void addParseListener(IParserObserver listener) {
        Assert.isNotNull(listener);
        synchronized (parserListeners) {
            if (!parserListeners.contains(listener)) {
                parserListeners.add(listener);
            }
        }
    }

    /** stock listener implementation */
    @Override
    public void removeParseListener(IParserObserver listener) {
        Assert.isNotNull(listener);
        synchronized (parserListeners) {
            parserListeners.remove(listener);
        }
    }

    // ---------------------------------------------------------------------------- notifications
    /**
     * stock listener implementation event is fired whenever we get a new root
     * @param original 
     */
    protected void fireParserChanged(ChangedParserInfoForObservers info) {
        this.root = info.root;
        List<IParserObserver> temp;
        synchronized (parserListeners) {
            temp = new ArrayList<IParserObserver>(parserListeners);
        }

        for (IParserObserver l : temp) {
            try {
                //work on a copy (because listeners may want to remove themselves and we cannot afford concurrent modifications here)
                if (l instanceof IParserObserver3) {
                    ((IParserObserver3) l).parserChanged(info);

                } else if (l instanceof IParserObserver2) {
                    ((IParserObserver2) l).parserChanged(info.root, info.file, info.doc, info.argsToReparse);

                } else {
                    l.parserChanged(info.root, info.file, info.doc, info.docModificationStamp);
                }
            } catch (Exception e) {
                Log.log(e);
            }
        }
    }

    /**
     * stock listener implementation event is fired when parse fails
     * @param original 
     */
    protected void fireParserError(ErrorParserInfoForObservers info) {
        List<IParserObserver> temp;
        synchronized (parserListeners) {
            temp = new ArrayList<IParserObserver>(parserListeners);
        }
        for (IParserObserver l : temp) {//work on a copy (because listeners may want to remove themselves and we cannot afford concurrent modifications here)
            if (l instanceof IParserObserver3) {
                ((IParserObserver3) l).parserError(info);

            } else if (l instanceof IParserObserver2) {
                ((IParserObserver2) l).parserError(info.error, info.file, info.doc, info.argsToReparse);

            } else {
                l.parserError(info.error, info.file, info.doc);
            }
        }
    }

    // ---------------------------------------------------------------------------- parsing

    public static class ParseOutput {

        public final long modificationStamp;
        public final Throwable error;
        public final ISimpleNode ast;

        public ParseOutput(Tuple<ISimpleNode, Throwable> astInfo, long modificationStamp) {
            this.ast = astInfo.o1;
            this.error = astInfo.o2;
            this.modificationStamp = modificationStamp;
        }

        public ParseOutput(ISimpleNode ast, Throwable error, long modificationStamp) {
            this.ast = ast;
            this.error = error;
            this.modificationStamp = modificationStamp;
        }

        public ParseOutput() {
            this.ast = null;
            this.error = null;
            this.modificationStamp = -1;
        }

    }

    /**
     * Parses the document, generates error annotations
     * 
     * @param argsToReparse: will be passed to fireParserError / fireParserChanged so that the IParserObserver2
     * can check it. This is useful when the reparse was done with some specific thing in mind, so that its requestor
     * can pass some specific thing to the parser observers
     * 
     * @return a tuple with the SimpleNode root(if parsed) and the error (if any).
     *         if we are able to recover from a reparse, we have both, the root and the error.
     */
    @Override
    public abstract ParseOutput reparseDocument(Object... argsToReparse);

    /**
     * This function will remove the markers related to errors.
     * @param resource the file that should have the markers removed
     * @throws CoreException
     */
    public static void deleteErrorMarkers(IResource resource) throws CoreException {
        IMarker[] markers = resource.findMarkers(IMarker.PROBLEM, false, IResource.DEPTH_ZERO);
        if (markers.length > 0) {
            resource.deleteMarkers(IMarker.PROBLEM, false, IResource.DEPTH_ZERO);
        }
    }

    @Override
    public void resetTimeoutPreferences(boolean useAnalysisOnlyOnDocSave) {
        this.useAnalysisOnlyOnDocSave = useAnalysisOnlyOnDocSave;
    }

    public List<IParserObserver> getObservers() {
        synchronized (parserListeners) {
            return new ArrayList<IParserObserver>(this.parserListeners);
        }
    }

}
