/*
 * Created on 2004-12-18
 *
 * Part of the Symbian development plug-in for Eclipse
 * Copyright (C) Mikołaj Zalewski, 2004
 */
package pl.zalewski.mikolaj.symbdev.core.internal.mmpparser;

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

import org.eclipse.core.resources.IMarker;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IWorkspaceRunnable;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
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.IDocumentExtension;

import pl.zalewski.mikolaj.symbdev.SymbdevPlugin;
import pl.zalewski.mikolaj.symbdev.core.internal.mmpmodel.MMPModel;
import pl.zalewski.mikolaj.symbdev.core.mmpmodel.BldInfModel;
import pl.zalewski.mikolaj.symbdev.core.mmpmodel.MMPCommands;
import pl.zalewski.mikolaj.symbdev.core.mmpmodel.MMPFileReference;
import pl.zalewski.mikolaj.symbdev.core.mmpmodel.MMPModelException;

/**
 * A parser of MMP files. This parser reads an MMP file and feeds
 * the model. We take a rather brute-force approach and reparse the
 * whole file after a user modification - the MMP files are usually
 * small
 * 
 * @author mikolaj
 */
public class MMPParser {

    static Pattern tokenize=Pattern.compile("(\\s*)([^\\s]+)");
    static final public String LINE_END="\r\n"; 
    static final public String BACKSLASH_ESCAPE=" \\\r\n";
    
    static final public int COMMAND_WIDTH=17;
    
    /**
     * The lastest version of the document has been parsed 
     */
    public boolean fClean=false;
    
    /**
     * Create a parser associated with the given document
     * @param doc The document
     * @param model The MMP model to feed
     */
    public MMPParser(IDocument doc, MMPModel model, IResource file) {
        super();
        fDocument=doc;
        fModel=model;
        fFile=file;
    }
    
    /**
     * Returns single strings without spaces (tokens) of one command
     * beginning at index fromIndex
     * @param doc A string containing the MMP document
     * @param formIndex The index where to begin parsing
     * 
     * @return The first element is a LinkedList containing MMPToken
     * of read tokens, the second is an Integer with the command
     * length
     */
    protected Object[] tokenizeCommand(String doc, int fromIndex)
    {
        return tokenizeCommand(doc, fromIndex, true);
    }
    
    /**
     * Returns single strings without spaces (tokens) of one command
     * beginning at index fromIndex
     * @param doc A string containing the MMP document
     * @param formIndex The index where to begin parsing
     * @param mark if it should put mark on errors found
     * 
     * @return The first element is a LinkedList containing MMPToken
     * of read tokens, the second is an Integer with the command
     * length
     */
    protected Object[] tokenizeCommand(String doc, int fromIndex,
            boolean mark)
    {
        int beginline=fromIndex;
        int endline;
        boolean moreLines=false;
        LinkedList tokens=new LinkedList();
        
        do
        {
	        endline=doc.indexOf('\n', beginline);
	        if (endline==-1) endline=doc.length();
	        	else endline++;
	        String line=doc.substring(beginline, endline);
	        
	        // get the essential part of the line
	        StringBuffer essline=new StringBuffer(line);
	        int comment=essline.indexOf("//");
	        if (comment!=-1)
	            essline.setLength(comment);
	        
	        // trim
	        int pos=essline.length()-1;
	        while (pos>=0 && essline.charAt(pos)<=0x20)
                pos--;
	        essline.setLength(pos+1);
	        
	        if (essline.length()>0 && essline.charAt(essline.length()-1)=='\\')
	        {
	            moreLines=true;
	            essline.setLength(essline.length()-1);
	        } else moreLines=false;
	        
            Matcher m=tokenize.matcher(essline);
	        while (m.find())
	        {
	            MMPToken t=new MMPToken();
	            t.fStartPos=beginline+m.end(1);
	            t.fContent=essline.substring(m.end(1), m.end());
	            tokens.add(t);
	        }
	        
	        if (moreLines)
	        {
	            beginline=endline;
	            if (beginline>=doc.length())
	            {
	                if (mark)
	                    markProblem(beginline-1, beginline, IMarker.SEVERITY_ERROR,
	                    	"Unexpected end of file");
	                moreLines=false;
	            }
	        }
        } while (moreLines);
        return new Object[]{tokens, new Integer(endline-fromIndex)};
    }
    
    /**
     * Parses the MMP file
     */
    synchronized public void parse() {
        fUpdatedModel=parseAndReturnModel();
        SymbdevPlugin.getStandardDisplay().asyncExec(
                new Runnable() 
                {
                    public void run()
                    {
                        MMPModel updatedModel;
                        if (!fClean)   // the file will be reparsed
                            return;
                        synchronized (MMPParser.this) {
                            updatedModel=fUpdatedModel;
                            fUpdatedModel=null;
                        }
                        if (updatedModel!=null)
                            fModel.loadModel(updatedModel);
                    }
                });
        applyMarkers();
    }
    
    synchronized public MMPModel parseAndReturnModel()
    {
//        System.out.println("PARSING: begin");
        fClean=true;
        String buffer=fDocument.get();
        MMPModel tempModel=new MMPModel();
        fMarkersList=new LinkedList();
        fEnvStack=new Stack();
        fCommands=new LinkedList();
        int pos=0;
        int line=0;
        fMainParser=new MainCommandParser(tempModel, this);
        fCurrentParser=fMainParser;
        while (pos<buffer.length())
        {
            line++;
            Object[] res=tokenizeCommand(buffer, pos);
            LinkedList tokens=(LinkedList)res[0];
            int length=((Integer)res[1]).intValue();
            
            if (SymbdevPlugin.getDefault().isTracing(SymbdevPlugin.TRACE_MMP))
            {
	            System.out.println("Line "+line+": length="+length+", tokens: ");
	            Iterator it=tokens.iterator();
	            while (it.hasNext())
	            {
	                MMPToken t=(MMPToken)it.next();
	                System.out.println("  '"+t.fContent+"' at "+t.fStartPos);
	            }
            }
            
            pos+=length;
            MMPCommand cmd=new MMPCommand(length, tokens.size(), fCurrentParser.fEnvironment);
            if (tokens.size()!=0)
                cmd.fCommand=((MMPToken)tokens.get(0)).fContent.toUpperCase();
            fCommands.add(cmd);
            
            if (tokens.size()==0)
                continue;
            fCurrentCommand=cmd;
            fCurrentParser.parseCommand(tokens);
            fCurrentCommand=null;
        }
        fMainParser.commandsEnd();
        return tempModel;
//        System.out.println("PARSING: end");
    }
    
    public synchronized MMPModel giveUpdatedModel() {
        MMPModel updatedModel=fUpdatedModel;
        fUpdatedModel=null;
        return updatedModel;
    }

    static public boolean validToken(String token)
    {
        for (int i=0; i<token.length(); i++)
            if (token.charAt(i)<20)
                return false;
        return true;
    }
    
    static public void validateToken(String token, String message)
    	throws MMPModelException
    {
        if (!validToken(token))
            throw new MMPModelException(MMPModelException.INVALID_TOKEN, message);        
    }
    
    /**
     * Creates a line from the given command and args. Checks if all
     * tokens are correct. If not, an MMPModelException is thrown
     * @param cmd The command token
     * @param args The arguments tokens
     * @return The line string
     * @throws MMPModelException If the tokens are invalid
     */
    static String createLine(String cmd, String[] args)
    	throws MMPModelException
    {
        validateToken(cmd, "Commmand code contains a space");
        for (int i=0; i<args.length; i++)
            validateToken(args[i], "Command argument token contains space");
        
        StringBuffer line=new StringBuffer(cmd);
        for (int i=cmd.length(); i<COMMAND_WIDTH; i++)
            line.append(' ');
        for (int i=0; i<args.length; i++)
        {
            line.append(' ');
            line.append(args[i]);
        }
        return line.toString();
    }

    static class CommandPos
    {
        /**
         * The position of the beginning of the command in the file
         */
        int fPos;
        /**
         * The position of the command in the list
         */
        int fListPos;
        /**
         * The found command equals to the searched one
         */
        boolean fMatch;
        /**
         * The command (may be null if the position is before the
         * first command in the file)
         */
        MMPCommand fCmd;
    }

    /**
     * Finds the best place for the command <code>cmd</code>. The search
     * can be made in two modes - with findFirst==false of findFirst==true.
     * If a command <code>cmd</code>is not found in the MMP file then
     * both modes gives the same result giving a position after a
     * command ret.fCmd. If such a command exists and findFirst is false
     * the position will be after the last such command (given in
     * ret.fCmd). If findFirst is true the position will be at the
     * beginning of the first such command given in ret.fCmd 
     * @param cmd The command keyword
     * @param env The environment in which the command should reside
     * @param it The iterator to the list of commands
     * @param findFirst Search mode
     * @return The position to insert the command
     */
    CommandPos findBestPlace(String cmd, MMPEnvironment env, Iterator it, boolean findFirst, int stopOnCommandId)
    {
        int newcmdid=MainCommandParser.getKeywordId(cmd);
        
        int opt_pos=0;
        int opt_id=0;
        int opt_list_pos=0;
        MMPCommand opt_cmd=null;
        int list_pos=0;
        int pos=0;

        MMPCommand mmpcmd=null;
        while (it.hasNext())
        {
            mmpcmd=(MMPCommand)it.next();
            if (mmpcmd.fCommand==null || mmpcmd.fEnvironment!=env)
            {
                list_pos++;
	            pos+=mmpcmd.fLength;
                continue;
            }
            
            // TODO: choose enviroment parser
            int cmdid=MainCommandParser.getKeywordId(mmpcmd.fCommand);
            if (cmdid==stopOnCommandId)
                break;
            if (cmdid<=newcmdid && cmdid>=opt_id)
            {
                opt_list_pos=list_pos;
                opt_pos=pos;
                opt_id=cmdid;
                opt_cmd=mmpcmd;
             
                if (findFirst && cmdid==newcmdid &&
                        mmpcmd.fCommand.equalsIgnoreCase(cmd))
                    break;
            }            
            pos+=mmpcmd.fLength;
            list_pos++;
        }
        
        CommandPos ret=new CommandPos();
        ret.fPos=opt_pos;
        ret.fListPos=opt_list_pos;
        ret.fCmd=opt_cmd;
        ret.fMatch=(opt_id==newcmdid && opt_cmd!=null &&
                opt_cmd.fCommand.equalsIgnoreCase(cmd));
        if ((!findFirst || !ret.fMatch)  && ret.fCmd!=null)
        {
            ret.fPos+=ret.fCmd.fLength;
            ret.fListPos++;
        }
        return ret;
    }

    /**
     * Finds the best place for the command <code>cmd</code>. The search
     * can be made in two modes - with findFirst==false of findFirst==true.
     * If a command <code>cmd</code>is not found in the MMP file then
     * both modes gives the same result giving a position after a
     * command ret.fCmd. If such a command exists and findFirst is false
     * the position will be after the last such command (given in
     * ret.fCmd). If findFirst is true the position will be at the
     * beginning of the first such command given in ret.fCmd 
     * @param cmd The command keyword
     * @param env The environment in which the command should reside
     * @param it The iterator to the list of commands
     * @param findFirst Search mode
     * @return The position to insert the command
     */
    CommandPos findBestPlace(String cmd, MMPEnvironment env, Iterator it, boolean findFirst)
    {
        return findBestPlace(cmd, env, it, findFirst, -1);
    }
    
    /**
     * Finds the best place for the command <code>cmd</code>. 
     * The function will return the position to insert the command.
     * If the field fListPos of the returned value is false,
     * both SOURCEPATH and the command should be placed there
     *  
     * @param cmd The command keyword
     * @param prefix The SOURCEPATH for this file
     * @param it The file name
     * @return The position to insert the command
     */
    CommandPos findSourceBestPlace(String cmd, String prefix, String filename)
    {
        int list_pos=0;
        int pos=0;

        // best SOURCEPATH place
        boolean exactMatch=false;
        int max_match=0;
        int match_list_pos=0;
        int match_file_pos=0;
        boolean match_pre=false;
        
        MMPCommand mmpcmd=null;
        // pass one - find the best SOURCEPATH
        Iterator it=fCommands.iterator();
        while (it.hasNext())
        {
            mmpcmd=(MMPCommand)it.next();
            if (mmpcmd.fCommand==null || mmpcmd.fEnvironment!=null) {
                list_pos++;
	            pos+=mmpcmd.fLength;
                continue;
            }

            if (mmpcmd.fCommand.equalsIgnoreCase(MMPCommands.SOURCEPATH) &&
                    mmpcmd.fExtraInfo!=null) {
                String path=(String)mmpcmd.fExtraInfo;
                if (prefix.equalsIgnoreCase(path)) {
                    exactMatch=true;
                    match_list_pos=list_pos;
                    match_file_pos=pos;
                    break;
                }
                
                int i;
                for (i=0; i<prefix.length() && i<path.length(); i++)
                    if (Character.toUpperCase(prefix.charAt(i))!=
                        Character.toUpperCase(path.charAt(i)))
                        break;
                if (i>max_match || (i==max_match && !match_pre)) {
                    max_match=i;
                    match_list_pos=list_pos;
                    match_file_pos=pos;
                    match_pre=path.compareToIgnoreCase(prefix)>0;
                }
            }
            
            list_pos++;
            pos+=mmpcmd.fLength;
        }
        
        // pass 2 - inserting the command. There are three possibilities
        if (exactMatch) {      // add to this to this SOURCEPATH
            int sourcePathId=MainCommandParser.getKeywordId(MMPCommands.SOURCEPATH);
            CommandPos ret=findBestPlace(cmd, null,
                    fCommands.listIterator(match_list_pos+1), // skip the SOURCEPATH command
                    false, sourcePathId);
            ret.fListPos+=match_list_pos+1;
            ret.fPos+=match_file_pos+((MMPCommand)fCommands.get(match_list_pos)).fLength;
            ret.fMatch=true;
            return ret;
        }
        
        if (match_pre) { // insert a SOURCEPATH before the found element
            CommandPos ret=new CommandPos();
            ret.fCmd=(MMPCommand)fCommands.get(match_list_pos);
            ret.fPos=match_file_pos;
            ret.fListPos=match_list_pos;
            ret.fMatch=false;
            return ret;
        } 
        
        // insert SOUCEPATH after the found element
        
        // find the last element that depends on the found SOURCEPATH
        // and insert after it
        it=fCommands.listIterator(match_list_pos+1);  // skip the SOURCEPATH command
        int insert_list_pos=list_pos=match_list_pos+1;
        int insert_file_pos=pos=
            match_file_pos+((MMPCommand)fCommands.get(match_list_pos)).fLength;
        while (it.hasNext())
        {
            mmpcmd=(MMPCommand)it.next();
            if (MMPCommands.SOURCEPATH.equalsIgnoreCase(mmpcmd.fCommand))
                break;
            
            // TODO: support different environment parsers
            if (MainCommandParser.doesDependOnSourcePath(mmpcmd.fCommand))
            {
                insert_list_pos=list_pos+1;
                insert_file_pos=pos+mmpcmd.fLength;
            }
            list_pos++;
            pos+=mmpcmd.fLength;
        }
        CommandPos ret=new CommandPos();
        ret.fCmd=(MMPCommand)fCommands.get(insert_list_pos);
        ret.fPos=insert_file_pos;
        ret.fListPos=insert_list_pos;
        ret.fMatch=false;
        return ret;
    }
    
    /**
     * Finds the n-th command cmd
     * @param cmd The name of the commands to search
     * @param n The number of the command to find 
     * @return The position of the command or <code>null</code> if
     * not found. fPos points to the beginning of the command.
     * fMatch is allways true. 
     */
    CommandPos findNthCommand(String cmd, int n)
    {
        CommandPos out=new CommandPos();
        out.fMatch=true;
        out.fListPos=0;
        int cmds_found=0;
        Iterator it=fCommands.iterator();
        
        while (it.hasNext())
        {
            MMPCommand mmpcmd=(MMPCommand)it.next();
            if (mmpcmd.fCommand!=null)
            {
//                System.out.println("findNthCommand["+cmds_found+":"+n+"]: command '"+mmpcmd.fCommand+"'\n");
                if (mmpcmd.fEnvironment==null &&
                    mmpcmd.fCommand.equalsIgnoreCase(cmd))
                {
                    if (cmds_found==n)
                    {
                        out.fCmd=mmpcmd;
                    	return out;
                    }
                    cmds_found++;
                }
            }
            out.fPos+=mmpcmd.fLength;
            out.fListPos++;
        }
        return null;
    }
    
    LinkedList retokenizeCommand(int pos, MMPCommand cmd)
    	throws BadLocationException
    {
        String line=fDocument.get(pos, cmd.fLength);
        Object[] ret=tokenizeCommand(line, 0, false);
        return (LinkedList)ret[0];        
    }
    
    String prepareLine(String line)
    {
        if (line.endsWith("\\"))
            line=line+BACKSLASH_ESCAPE;
        line=line+LINE_END;
        return line;
    }
    
    void rewriteAddCommand(String cmd, String line, int pos, int listPos, int numTokens)
    	throws BadLocationException
    {
        line=prepareLine(line);
        fDocument.replace(pos, 0, line);
        MMPCommand mmpcmd=new MMPCommand(line.length(), numTokens, null);
        mmpcmd.fCommand=cmd;
        fCommands.add(listPos, mmpcmd);
    }
    
    void rewriteReplaceCommand(String line, int pos, MMPCommand cmd, int numTokens)
    	throws BadLocationException
    {
        line=prepareLine(line);
        fDocument.replace(pos, cmd.fLength, line);
        cmd.fLength=line.length();
        cmd.fNumTokens=numTokens;
    }
    
    void rewriteRemoveCommand(int pos, MMPCommand cmd, Iterator it)
    	throws BadLocationException
    {
        fDocument.replace(pos, cmd.fLength, "");
        it.remove();
    }
    
    void rewriteAddToken(String newToken, int pos, MMPCommand cmd)
    	throws BadLocationException
    {
        if (newToken.endsWith("\\"))
            newToken=newToken+BACKSLASH_ESCAPE;
        MMPToken last=(MMPToken)(retokenizeCommand(pos, cmd).getLast());
        fDocument.replace(pos+last.fStartPos+last.fContent.length(),
                0, " "+newToken);
        cmd.fNumTokens++;
        cmd.fLength+=newToken.length()+1;
    }
    
    void rewriteReplaceToken(String newToken, int tokenId, int pos, MMPCommand cmd)
		throws BadLocationException, CoreException    
    {
        LinkedList l=retokenizeCommand(pos, cmd);
        if (l.size()<=tokenId)
            throw new MMPModelException(MMPModelException.INVALID_ARGUMENT,
                    "Token id for removal is out of range");
        MMPToken token=(MMPToken)l.get(tokenId);
        if (tokenId<l.size()-1)
        {
            MMPToken next=(MMPToken)l.get(tokenId+1);
            fDocument.replace(pos+token.fStartPos, next.fStartPos-token.fStartPos, newToken+" ");
            cmd.fLength-=next.fStartPos-token.fStartPos;
            cmd.fLength+=newToken.length()+1;
        } else
        {
            if (newToken.endsWith("\\"))
                newToken=newToken+BACKSLASH_ESCAPE;
            fDocument.replace(pos+token.fStartPos, token.fContent.length(), newToken);
            cmd.fLength-=token.fContent.length();
            cmd.fLength+=newToken.length();
        }
    }
    
    void rewriteRemoveToken(int tokenId, int pos, MMPCommand cmd)
    	throws BadLocationException, CoreException
    {
        LinkedList l=retokenizeCommand(pos, cmd);
        if (l.size()<=tokenId)
            throw new MMPModelException(MMPModelException.INVALID_ARGUMENT,
                    "Token id for removal is out of range");
        MMPToken token=(MMPToken)l.get(tokenId);
        if (tokenId<l.size()-1)
        {
            MMPToken next=(MMPToken)l.get(tokenId+1);
            fDocument.replace(pos+token.fStartPos, next.fStartPos-token.fStartPos, "");
            cmd.fLength-=next.fStartPos-token.fStartPos;
        } else
        {
            fDocument.replace(pos+token.fStartPos, token.fContent.length(), "");
            cmd.fLength-=token.fContent.length();
        }
        cmd.fNumTokens--;
    }
    
    /**
     * Removes every occurance of the given command from the main
     * block in the MMP file.  The file may need to be reparsed.
     * This method is synchronized so if another thread uses the
     * parser this method will wait  
     * @param cmd The method to remove
     */
    synchronized public void removeCommand(String cmd)
    	throws MMPModelException
    {
        ensureClean();
        int pos=0;
        Iterator it=fCommands.iterator();
        IDocumentExtension ext=null;
        try {
	        while (it.hasNext())
	        {
	            MMPCommand mmpcmd=(MMPCommand)it.next();
	            if (mmpcmd.fCommand!=null)
	            {
	                if (mmpcmd.fEnvironment==null &&
	                    mmpcmd.fCommand.equalsIgnoreCase(cmd))
	                {
	                    rewriteRemoveCommand(pos, mmpcmd, it);
	                    continue;
	                }
	            }
	            pos+=mmpcmd.fLength;
	        }
        }
        catch (BadLocationException e)
        {
            throw new MMPModelException(MMPModelException.DOCUMENT_ERROR,
                    "Internal error - bad document position");
        }
    }
    
    /**
     * Removes the <code>n</code>th (counting from 0) occurance of
     * the given command from the main block in the MMP file.  The
     * file may need to be reparsed. This method is synchronized so
     * if another thread uses the parser this method will wait  
     * @param cmd The method to remove
     * @param n The command to remove
     */
    synchronized public void removeCommand(String cmd, int n)
    	throws MMPModelException
    {
        ensureClean();
        CommandPos pos=findNthCommand(cmd, n);
        if (pos==null)
	        throw new MMPModelException(MMPModelException.ITEM_NOT_FOUND,
            	"Couldn't find the command to delete");
        try {
            Iterator it=fCommands.listIterator(pos.fListPos);
            it.next();
            rewriteRemoveCommand(pos.fPos, pos.fCmd, it);
        }
        catch (BadLocationException e)
        {
            throw new MMPModelException(MMPModelException.DOCUMENT_ERROR,
                    "Internal error - bad document position");
        }
    }
    
    /**
     * Replaces the options of the n-th command <code>cmd</code> with
     * the given parameters
     * @param cmd The commands name
     * @param args The new options to insert in the file
     * @param n The number of the command to replace
     */
    synchronized public void replaceCommand(String cmd, String[] args, int n)
    	throws MMPModelException
    {
        ensureClean();
        CommandPos pos=findNthCommand(cmd, n);
        if (pos==null)
	        throw new MMPModelException(MMPModelException.ITEM_NOT_FOUND,
            	"Couldn't find the command to delete");
        try {
            String line=createLine(cmd, args);
            rewriteReplaceCommand(line, pos.fPos, pos.fCmd, args.length+1);
        }
        catch (BadLocationException e)
        {
            throw new MMPModelException(MMPModelException.DOCUMENT_ERROR,
                    "Internal error - bad document position");
        }        
    }
    
    /**
     * Adds a new statement with this command to the MMP file.
     * The file may need to be reparsed. This method is synchronized
     * so if another thread uses the parser this method will wait  
     * @param cmd The command in the statement to add
     * @param args Extra tokens for this command
     * @param replace If <code>true</code> and such a command exists in
     * the MMP file it's arguments will be replaced with these
     * arguments. If more than one command exists the commands except
     * the first one will be removed 
     */
    synchronized public void addCommand(String cmd, String[] args, boolean replace)
    	throws CoreException
    {
        ensureClean();
        String line=createLine(cmd, args);
        
        Iterator it=fCommands.iterator();
        CommandPos cmdpos=findBestPlace(cmd, null, it, replace);
        try {
		    if (replace && cmdpos.fMatch)
		    {
		        rewriteReplaceCommand(line, cmdpos.fPos,
		                cmdpos.fCmd, args.length+1);
		        
		        // remove next commands
		        int pos=cmdpos.fPos+line.length();
		        while (it.hasNext())
		        {
		            MMPCommand mmpcmd=(MMPCommand)it.next();
		            if (mmpcmd.fEnvironment==null &&
		                cmd.equalsIgnoreCase(mmpcmd.fCommand))
		                rewriteRemoveCommand(pos, mmpcmd, it);
		                else pos+=mmpcmd.fLength;
		        }
		    } else {
		        rewriteAddCommand(cmd, line, cmdpos.fPos,
		                cmdpos.fListPos, args.length+1);
		    }
        } catch (BadLocationException e) {
            throw new MMPModelException(MMPModelException.DOCUMENT_ERROR,
        		"Internal error - bad document position");
        }
    }
        
    /**
     * Adds a new statement with this command to the MMP file.
     * The file may need to be reparsed. This method is synchronized
     * so if another thread uses the parser this method will wait  
     * @param cmd The command to add
     * @param replace If <code>true</code> such a command exists in
     * the MMP file it's arguments will be replaced with these
     * arguments. If more than one command exists the commands except
     * the first one will be removed 
     */
    public void addCommand(String cmd, boolean replace)
    	throws CoreException
    {
        addCommand(cmd, new String[0], replace);
    }
    
    /**
     * Adds a file to the MMP file. A SOURCEPATH command is added if needed 
     * @param cmd The command for this file (e.g. RESOURCE)
     * @param path The path to the file to add
     * @param current The filesystem path where the MMP file resides
     * @throws CoreException If something goes wrong
     */
    synchronized public MMPFileReference addFileCommand(String cmd, IPath path)
    	throws CoreException
    {
        ensureClean();
        IPath current=fFile.getParent().getLocation();
        int m=path.matchingFirstSegments(current);
        StringBuffer rel=new StringBuffer();
        for (int i=m; i<current.segmentCount(); i++)
            rel.append("..\\");
        for (int i=m; i<path.segmentCount()-1; i++)
        {
            rel.append(path.segment(i));
            rel.append("\\");
        }
        if (rel.length()>1)
            rel.setLength(rel.length()-1);    // chop the last '\\'
        	else rel.replace(0, rel.length(), ".");
        String dir=rel.toString();
        String file=path.segment(path.segmentCount()-1);
        
        String line=createLine(cmd, new String[]{file});
        
        CommandPos pos=findSourceBestPlace(cmd, dir, file);
        try {
	        rewriteAddCommand(cmd, line, pos.fPos, pos.fListPos, 2);
	        if (!pos.fMatch)       // insert the SOURCEPATH before the command
	        {
	            String sourcepath=createLine(MMPCommands.SOURCEPATH, new String[]{dir});
	            rewriteAddCommand(MMPCommands.SOURCEPATH, sourcepath,
	                    pos.fPos, pos.fListPos, 2);            
	        }
        }
        catch (BadLocationException e)
        {
            throw new MMPModelException(MMPModelException.DOCUMENT_ERROR,
    			"Internal error - bad document position");            
        }
        return new MMPFileReference(file, dir);
    }
        
    /**
     * Adds a token for command <code>cmd</code>. If this command
     * exists the token is appened to the last such command in the
     * MMP file. If it doesn't a new command is added with this token  
     * @param cmd The method to add
     * @param token The token to add
     */
    synchronized public void appendToken(String cmd, String token)
		throws CoreException
	{
	    validateToken(cmd, "Commmand code contains a space");
	    validateToken(token, "Command argument token contains space");
	    
	    CommandPos cmdpos=findBestPlace(cmd, null, fCommands.iterator(), false);
	    try {
	        if (cmdpos.fMatch)
	        {
	            int pos=cmdpos.fPos-cmdpos.fCmd.fLength;
	            rewriteAddToken(token, pos, cmdpos.fCmd);
	        } else
	        {
	            String append=createLine(cmd, new String[]{token});
	            rewriteAddCommand(cmd, append, cmdpos.fPos,
	                    cmdpos.fListPos, 2);
	        }
	    } catch (BadLocationException e) {
	        throw new MMPModelException(MMPModelException.DOCUMENT_ERROR,
	        	"Internal error - bad document position", e);
	    }
	}

    synchronized public void removeCmdToken(String cmd, int nr)
		throws CoreException
	{
        ensureClean();
        Iterator it=fCommands.iterator();
        int pos=0;
        while (it.hasNext())
        {
            MMPCommand mmpcmd=(MMPCommand)it.next();
            if (mmpcmd.fEnvironment!=null || mmpcmd.fCommand==null)
            {
                pos+=mmpcmd.fLength;
                continue;
            }
            
            if (mmpcmd.fCommand.equalsIgnoreCase(cmd))
            {
                int numopts=mmpcmd.fNumTokens-1;
                if (nr<numopts)
                {
                    try {
                        if (numopts>1)
                            rewriteRemoveToken(nr+1, pos, mmpcmd);
                        else rewriteRemoveCommand(pos, mmpcmd, it);
                    } catch (BadLocationException e) {
                        throw new MMPModelException(MMPModelException.ITEM_NOT_FOUND,
                                "Token to be deleted not found");
                    }
                    
                    break;
                } else nr-=numopts;
            }
            pos+=mmpcmd.fLength;
        }
	}

    synchronized public void replaceCmdToken(String cmd, int nr, String newToken)
    	throws CoreException
    {
        ensureClean();
        validateToken(newToken, "New token contains spaces");
        Iterator it=fCommands.iterator();
        int pos=0;
        while (it.hasNext())
        {
            MMPCommand mmpcmd=(MMPCommand)it.next();
            if (mmpcmd.fEnvironment!=null || mmpcmd.fCommand==null)
            {
                pos+=mmpcmd.fLength;
                continue;
            }
        
            if (mmpcmd.fCommand.equalsIgnoreCase(cmd))
            {
                int numopts=mmpcmd.fNumTokens-1;
                if (nr<numopts)
                {
                    try {
	                     rewriteReplaceToken(newToken, nr+1, pos, mmpcmd);
	                } catch (BadLocationException e) {
	                    throw new MMPModelException(MMPModelException.ITEM_NOT_FOUND,
	                            "Token to be deleted not found");
	                }	                
	                break;
	            } else nr-=numopts;
	        }
	        pos+=mmpcmd.fLength;
    	}
	}    
    
    /**
     * Class containing information about the markers that will be
     * added after parsing
     */
    static class MarkerInfo
    {
        int start;
        int end;
        int severity;
        String message;
    }
    
    LinkedList fMarkersList;
    
    /**
     * Called by command parsers
     * @return The root of the SDK
     */
    IPath getSDKRoot()
    {
        return null;
    }
    
    /**
     * Called by command parsers
     * @return The directory with bld.inf for this project
     */
    IPath getGroupDir()
    {
        return null;
    }

    /**
     * Marks an error/warning. Called by command parsers
     * @param start The first character to mark
     * @param end The last character to mark
     * @param severity Severity (see IMarker.SEVERITY_*)
     * @param message The error message
     */
    void markProblem(int start, int end, int severity, String message)
    {
        MarkerInfo mi=new MarkerInfo();
        mi.start=start;
        mi.end=(start!=end?end:end+1);
        mi.severity=severity;
        mi.message=message;
        fMarkersList.add(mi);
    }
    
    public void applyMarkers()
    {
        final LinkedList markersList=fMarkersList;
        fMarkersList=null;
        Job applyJob = new Job("Apply markers") {
            public IStatus run(IProgressMonitor monitor) { 
		        try {
			        ResourcesPlugin.getWorkspace().run(new IWorkspaceRunnable()
			            {
			                public void run(IProgressMonitor monitor) throws CoreException {
			                    // clear previous markers
			                    fFile.deleteMarkers(BldInfModel.PROBLEM_MARKER_ID, false, IResource.DEPTH_ZERO);
			                    
			                    // add new ones
			                    Iterator it=markersList.iterator();
			                    while (it.hasNext())
			                    {
			                        MarkerInfo mi=(MarkerInfo)it.next();
			                        IMarker m=fFile.createMarker(BldInfModel.PROBLEM_MARKER_ID);
			                        m.setAttribute(IMarker.CHAR_START, mi.start);
			                        m.setAttribute(IMarker.CHAR_END, mi.end);
			                        m.setAttribute(IMarker.MESSAGE, mi.message);
			                        m.setAttribute(IMarker.SEVERITY, mi.severity);
			                    }
		                }
		            }, fFile, 0, null);
		        }
		        catch (CoreException e) {
		            return e.getStatus();
		        }
		        return Status.OK_STATUS;
            }
        };
        applyJob.setPriority(Job.SHORT);
        applyJob.setRule(fFile);
        applyJob.setSystem(true);
        applyJob.schedule();
    }
    
    /**
     * Called by the command parsers if they have found a new
     * START BLOCKNAME ... END block.
     * @param parser A parser to parse this block
     */
    void startEnvironment(AbstractCommandParser parser)
    {
        fEnvStack.push(fCurrentParser);
        fCurrentParser=parser;
    }
    
    /**
     * Called by the command parsers if they have found an END
     * statement
     * @return <code>false</code> if there was no open block 
     */
    boolean endEnvironment()
    {
        if (fEnvStack.empty())
           return false; 
        
        fCurrentParser=(AbstractCommandParser)fEnvStack.pop();
        return true;
    }
    
    /**
     * Called by the parsers if they want to store something in the
     * fExtraInfo field of the MMPCommand
     */
    public void setExtraInfo(Object o)
    {
        if (fCurrentCommand!=null)
            fCurrentCommand.fExtraInfo=o;
    }

    /**
     * Make sure the parser's information is up to date with the
     * document. A race condition is still possible if the document
     * is modified during the parsing... 
     */
    protected void ensureClean()
    {
        if (!fClean)
            parse();
    }
    
    /**
     * During parsing points to the object of the current command
     */
    protected MMPCommand fCurrentCommand;
    
    MainCommandParser fMainParser;
    AbstractCommandParser fCurrentParser;
    IDocument fDocument;
    LinkedList fCommands;
    IResource fFile;
    MMPModel fModel;
    MMPModel fUpdatedModel;
    Stack fEnvStack;
}
