/**
 * Copyright (c) 2006 IBM Corporation and others.
 * All rights reserved.   This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v2.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v20.html
 * 
 * Contributors: 
 *   IBM - Initial API and implementation
 */
package org.eclipse.emf.codegen.merge.java.facade.ast;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.EnumConstantDeclaration;
import org.eclipse.jdt.core.dom.EnumDeclaration;
import org.eclipse.jdt.core.dom.PackageDeclaration;
import org.eclipse.jdt.core.dom.rewrite.ASTRewrite;
import org.eclipse.jdt.core.dom.rewrite.ITrackedNodePosition;
import org.eclipse.jdt.core.dom.rewrite.ListRewrite;
import org.eclipse.jdt.core.formatter.IndentManipulation;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.Document;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
import org.eclipse.text.edits.InsertEdit;
import org.eclipse.text.edits.MalformedTreeException;
import org.eclipse.text.edits.MultiTextEdit;
import org.eclipse.text.edits.ReplaceEdit;
import org.eclipse.text.edits.TextEdit;

import org.eclipse.emf.codegen.merge.java.facade.FacadeHelper;
import org.eclipse.emf.codegen.merge.java.facade.JCompilationUnit;
import org.eclipse.emf.codegen.merge.java.facade.JNode;
import org.eclipse.emf.codegen.merge.java.facade.JType;

/**
 * Wraps {@link CompilationUnit} object.
 * 
 * @since 2.2.0
 */
public class ASTJCompilationUnit extends ASTJNode<CompilationUnit> implements JCompilationUnit
{
  /**
   * Name for the name property of the compilation unit.
   */
  public static final String NAME_PROPERTY = "ASTJCompilationUnit.name";

  /**
   * Pattern to extract and replace header.
   * Header is any number of block or line comments, spaces or newline characters
   * before any Java code (package, import or type declaration).
   */
  protected final static Pattern HEADER_PATTERN = Pattern.compile("^(?:(?:/\\*(?:.|[\\n\\r])*?\\*/)|(?://.*(?:[\\n\\r])+)|(?:\\s+))+");  

  /**
   * Map of nodes (<code>ASTNode</code>) to contents of the node (<code>String</code>).
   * This map is used during rewrite to track the nodes positions and set the exact contents of them.
   */
  private Map<ASTNode, String> allTrackedContentsMap = new HashMap<ASTNode, String>();
  
  /**
   * Set of all nodes that have been commented out
   */
  private Set<ASTNode> commentedOutNodes = null;  
  
  /**
   * Header of the compilation unit to be set during rewrite.
   * If it is null, the header will not be modified.
   */
  protected String headerString = null;
  
  /**
   * Original contents of the compilation unit
   */
  protected char[] originalContents;
  
  /**
   * @param compilationUnit
   */
  public ASTJCompilationUnit(CompilationUnit compilationUnit)
  {
    super(compilationUnit);
  }
  
  @Override
  public void dispose()
  {
    allTrackedContentsMap.clear();
    headerString = null;
    originalContents = null;
    if (commentedOutNodes != null)
    {
      commentedOutNodes.clear();
      commentedOutNodes = null;
    }
    
    super.dispose();
  }
  
  /**
   * Sets original contents of the compilation unit to be used for converting nodes
   * to strings.
   * 
   * @param originalContents
   */
  public void setOriginalContents(char[] originalContents)
  {
    this.originalContents = originalContents;
  }  

  /**
   * @return original contents of the compilation unit
   */
  public char[] getOriginalContents()
  {
    return originalContents;
  }
  
  /**
   * Returns the name of the main type suffixed with java extension.
   * <p>
   * In the absence of type defaults to original name that compilation unit was created with.
   * <p>
   * Created for compatibility with JDOM implementation of <code>getName()</code> for {@link JCompilationUnit}.
   * 
   * @return name of the main type suffixed with java extension
   * 
   * @see org.eclipse.emf.codegen.merge.java.facade.JNode#getName()
   */
  public String getName()
  {
    JType type = getFacadeHelper().getMainType(this);
    return type != null ? type.getName() + ".java" : (String)getWrappedObject().getProperty(NAME_PROPERTY);
  }
  
  /**
   * Sets the name of the main type of compilation unit.
   * 
   * @see FacadeHelper#getMainType(JCompilationUnit)
   * @see org.eclipse.emf.codegen.merge.java.facade.JNode#setName(java.lang.String)
   * @see org.eclipse.emf.codegen.merge.java.facade.JNode#getQualifiedName()
   */    
  public void setName(String name)
  {
    JType type = getFacadeHelper().getMainType(this);
    if (type != null)
    {
      type.setName(name);
    }
    else
    {
      getASTNode().setProperty(NAME_PROPERTY, name);
    }
  }  
  
  /**
   * Same as the <code>getName()</code>.
   * 
   * @see org.eclipse.emf.codegen.merge.java.facade.AbstractJNode#computeQualifiedName()
   */
  @Override
  protected String computeQualifiedName()
  {
    return getName();
  }    

  /**
   * Returns the list of children in order: package declaration (<code>JPackage</code>), imports (<code>JImport</code>), and types (<code>JType</code>).
   * 
   * @see org.eclipse.emf.codegen.merge.java.facade.AbstractJNode#getChildren()
   */
  @Override
  public List<JNode> getChildren()
  {
    if (!isDisposed())
    {
      CompilationUnit astCompilationUnit = getASTNode();
      List<JNode> children = new ArrayList<JNode>();
      PackageDeclaration astPackage = astCompilationUnit.getPackage();
      if (astPackage != null)
      {
        JNode child = getFacadeHelper().convertToNode(astPackage);
        if (child != null)
        {
          children.add(child);
        }
      }
      
      ListRewrite importsListRewrite = rewriter.getListRewrite(astCompilationUnit, CompilationUnit.IMPORTS_PROPERTY);
      for (Object importDeclaration : importsListRewrite.getRewrittenList())
      {
        JNode child = getFacadeHelper().convertToNode(importDeclaration);
        if (child != null)
        {
          children.add(child);
        }
      }
      
      ListRewrite typesListRewrite = rewriter.getListRewrite(astCompilationUnit, CompilationUnit.TYPES_PROPERTY);
      for (Object type : typesListRewrite.getRewrittenList())
      {
        JNode child = getFacadeHelper().convertToNode(type);
        if (child != null)
        {
          children.add(child);
        }
      }
      if (!children.isEmpty())
      {
        return Collections.unmodifiableList(children);
      }
    }
    return Collections.emptyList();
  }

  public String getHeader()
  {
    if (headerString == null)
    {
      Matcher matcher = HEADER_PATTERN.matcher(new String(originalContents));
      if (matcher.find())
      {
        String headerString = matcher.group();
        if (ASTFacadeHelper.DEBUG)
        {
          getFacadeHelper().logInfo("Got header <" + headerString + ">");
        }
        return headerString;
      }
      else
      {
        return "";
      }
    }
    return headerString;
  }
  
  /**
   * This implementation remembers the header string, and does replacement of the header in the final
   * document after any other changes.
   * <p>
   * For the files with no package declaration, 
   * the new header might be inserted at the beginning of the file instead of being replaced.
   * 
   * @see org.eclipse.emf.codegen.merge.java.facade.JCompilationUnit#setHeader(java.lang.String)
   */
  public void setHeader(String header)
  {
    if (header != null)
    {
      this.headerString = header;
    }    
  }
  
  /**
   * Method to replace the header in the given document.
   * 
   * @param targetDoc
   */
  protected void setHeader(IDocument targetDoc)
  {
    String targetDocString = targetDoc.get();
    Matcher matcher = HEADER_PATTERN.matcher(targetDocString);
    // replace or append
    if (matcher.find())
    {
      targetDocString = headerString.concat(targetDocString.substring(matcher.end()));
    }
    else 
    {
      targetDocString = headerString.concat(targetDocString);
    }
    targetDoc.set(targetDocString);
  }    

  /**
   * Returns the rewritten text after applying all the changes made to JNode tree.
   * 
   * @see org.eclipse.emf.codegen.merge.java.facade.ast.ASTJNode#getContents()
   */
  @Override
  public String getContents()
  {
    // enable tracking for nodes that have string content
    AbstractRewriter contentsReplacer = null;
    if (allTrackedContentsMap != null && !allTrackedContentsMap.isEmpty())
    {
      contentsReplacer = new NodeContentsReplacer();
    }
    
    // enable tracking for commented out nodes
    AbstractRewriter commenter = null;
    if (commentedOutNodes != null && !commentedOutNodes.isEmpty())
    {
      commenter = new NodeCommenter();
    }

    String contents = new String(originalContents);    
    IDocument targetDoc = new Document(contents);
    TextEdit edits = rewriter.rewriteAST(targetDoc, getFacadeHelper().getJavaCoreOptions());
    
    if (edits.getChildrenSize() != 0 || edits.getLength() != 0 || commenter != null || contentsReplacer != null || headerString != null)
    {
      // apply changes using ASTRewrite
      //
      try
      {
        edits.apply(targetDoc);
        if (ASTFacadeHelper.DEBUG)
        {
          getFacadeHelper().logInfo("Document after ASTRewrite:\n<" + targetDoc.get() + ">\nEnd of document.");
        }
      }
      catch (MalformedTreeException e)
      {
        if (ASTFacadeHelper.DEBUG)
        {
          getFacadeHelper().logError("Error applying edits: ", e);
        }
      }
      catch (BadLocationException e)
      {
        if (ASTFacadeHelper.DEBUG)
        {
          getFacadeHelper().logError("Error applying edits: ", e);
        }
      }
  
      // apply additional edits like replacing or commenting out nodes
      //
      try 
      {
        TextEdit additionalEdits = null;
        
        if (contentsReplacer != null)
        {
          additionalEdits = contentsReplacer.createEdits(additionalEdits, targetDoc);
        }
        
        if (commenter != null)
        {
          additionalEdits = commenter.createEdits(additionalEdits, targetDoc);
        }    
        
        if (additionalEdits != null)
        {
          additionalEdits.apply(targetDoc);
        }
      }
      catch (Exception e) 
      {
        if (ASTFacadeHelper.DEBUG)
        {
          getFacadeHelper().logError("Error creating and applying replace edits: ", e);
        }
      }
      
      // apply header
      if (headerString != null)
      {
        setHeader(targetDoc);
      }      
      
      contents = targetDoc.get();
    }
    return contents;
  }
  
  /* (non-Javadoc)
   * @see org.eclipse.emf.codegen.merge.java.facade.ast.ASTJNode#addChild(org.eclipse.emf.codegen.merge.java.facade.JNode)
   */
  @Override
  public boolean addChild(ASTJNode<?> child)
  {
    if (child.getParent() != null)
    {
      return false;
    }

    if (child instanceof ASTJImport)
    {
      insertLast(child, CompilationUnit.IMPORTS_PROPERTY);
    }
    else if (child instanceof ASTJAbstractType<?>)
    {
      insertLast(child, CompilationUnit.TYPES_PROPERTY);
    }
    else if (child instanceof ASTJPackage)
    {
      setNodeProperty(getASTNode(), child.getASTNode(), CompilationUnit.PACKAGE_PROPERTY);
    }
    else
    {
      return false;
    }
    child.setParent(this);
    return true;
  }
  
  /* (non-Javadoc)
   * @see org.eclipse.emf.codegen.merge.java.facade.ast.ASTJNode#insertSibling(org.eclipse.emf.codegen.merge.java.facade.JNode, org.eclipse.emf.codegen.merge.java.facade.JNode, boolean)
   */
  @Override
  public boolean insertSibling(ASTJNode<?> node, ASTJNode<?> newSibling, boolean before)
  {
    if (newSibling.getParent() != null)
    {
      return false;
    }

    if (newSibling instanceof ASTJImport)
    {
      if (node instanceof ASTJImport)
      {
        insert(newSibling, CompilationUnit.IMPORTS_PROPERTY, node, before);
      }
      else if (node instanceof ASTJPackage)
      {
        insertFirst(newSibling, CompilationUnit.IMPORTS_PROPERTY);
      }
      else if (node instanceof ASTJAbstractType<?>)
      {
        insertLast(newSibling, CompilationUnit.IMPORTS_PROPERTY);
      }
      else
      {
        return false;
      }
    }
    else if (newSibling instanceof ASTJAbstractType<?>)
    {
      if (node instanceof ASTJAbstractType<?>)
      {
        insert(newSibling, CompilationUnit.TYPES_PROPERTY, node, before);
      }
      else if (node instanceof ASTJImport)
      {
        insertFirst(newSibling, CompilationUnit.TYPES_PROPERTY);
      }
      else if (node instanceof ASTJPackage)
      {
        insertFirst(newSibling, CompilationUnit.TYPES_PROPERTY);
      }
      else
      {
        return false;
      }
    }
    else if (newSibling instanceof ASTJPackage)
    {
      setNodeProperty(getASTNode(), newSibling.getASTNode(), CompilationUnit.PACKAGE_PROPERTY);
    }
    else
    {
      return false;
    }
    newSibling.setParent(this);
    return true;
  }
  
  /**
   * Removes the given node.
   * <p>
   * This implementation will not perform moving of the node if the node is inserted after being removed.
   * Hence, the removed node must not be inserted again.
   * 
   * @see org.eclipse.emf.codegen.merge.java.facade.ast.ASTJNode#remove(ASTJNode)
   */
  @Override
  public boolean remove(ASTJNode<?> node)
  {
    if (node.getParent() != this)
    {
      return false;
    }

    if (node instanceof ASTJImport)
    {
      remove(node, CompilationUnit.IMPORTS_PROPERTY);
    }
    else if (node instanceof ASTJAbstractType<?>)
    {
      remove(node, CompilationUnit.TYPES_PROPERTY);
    }
    else if (node instanceof ASTJPackage)
    {
      setNodeProperty(getASTNode(), null, CompilationUnit.PACKAGE_PROPERTY);
    }
    else
    {
      return false;
    }
    node.setParent(null);
    return true;
  }

  /**
   * @return map of all nodes of the compilation unit and its children (<code>ASTNode</code> to <code>String</code> contents of the node).
   * <p>
   * This map will be used during rewrite to track the nodes and set the exact contents of them.
   */
  protected Map<ASTNode, String> getAllTrackedContentsMap()
  {
    return allTrackedContentsMap;
  }
  
  /**
   * @return set of nodes that have been commented out
   */
  protected Set<ASTNode> getCommentedOutNodes()
  {
    if (commentedOutNodes == null)
    {
      commentedOutNodes = new HashSet<ASTNode>();
    }
    return commentedOutNodes;
  }  
  
  /**
   * Base class for additional rewriters used during rewrite process.
   */
  protected abstract class AbstractRewriter
  {
    /**
     * Creates additional edits to be applied on the document
     * 
     * @param existingEdits existing edits on  
     * @param doc document after call to {@link ASTRewrite#rewriteAST()} or {@link ASTRewrite#rewriteAST(IDocument, Map)}.
     * @return existingEdits with new edits added
     */
    public TextEdit createEdits(TextEdit existingEdits, IDocument doc)
    {
      if (existingEdits == null)
      {
        existingEdits = new MultiTextEdit();
      }
      return addEdits(existingEdits, doc);
    }

    protected abstract TextEdit addEdits(TextEdit existingEdits, IDocument doc);
  }

  protected class NodeContentsReplacer extends AbstractRewriter
  {
    protected Map<ITrackedNodePosition, String> trackedNodePositionsMap;

    /**
     * Enables tracking for all nodes in tracked contents map ({@link #getAllTrackedContentsMap()}).
     * <p>
     * This constructor must be called before call to {@link ASTRewrite#rewriteAST()} or {@link ASTRewrite#rewriteAST(IDocument, Map)}.
     * 
     * @see ASTRewrite#track(ASTNode)
     */
    public NodeContentsReplacer()
    {
      Map<ASTNode, String> allTrackedContentsMap = getAllTrackedContentsMap();
      trackedNodePositionsMap = new HashMap<ITrackedNodePosition, String>(Math.max((int)(allTrackedContentsMap.keySet().size() / .75f) + 1, 16));
      for (ASTNode node : allTrackedContentsMap.keySet())
      {
        try
        {
          ITrackedNodePosition trackedNodePosition = rewriter.track(node);
          trackedNodePositionsMap.put(trackedNodePosition, allTrackedContentsMap.get(node));
        }
        catch (Exception e)
        {
          if (ASTFacadeHelper.DEBUG)
          {
            getFacadeHelper().logError("Enabling tracking in " + getName(), e);
          }
        }
      }
    }

    /**
     * Creates a {@link TextEdit} that replaces contents of all tracked nodes.
     * 
     * @param existingEdits existing edits on  
     * @param doc document after call to {@link ASTRewrite#rewriteAST()} or {@link ASTRewrite#rewriteAST(IDocument, Map)}.
     * @return existingEdits with new edits added
     * 
     * @see #trackedNodePositionsMap
     */
    @Override
    protected TextEdit addEdits(TextEdit existingEdits, IDocument doc)
    {
      for (Map.Entry<ITrackedNodePosition, String> entry : trackedNodePositionsMap.entrySet())
      {
        ITrackedNodePosition position = entry.getKey();
        String contents = entry.getValue();
        try
        {
          ReplaceEdit replaceEdit = new ReplaceEdit(position.getStartPosition(), position.getLength(), contents);
          existingEdits.addChild(replaceEdit);
        }
        // this should never happen
        catch (Exception e)
        {
          // log the error, ignore the change and continue
          if (ASTFacadeHelper.DEBUG)
          {
            getFacadeHelper().logError("Creating ReplaceEdit for <" + contents + "> in " + getName(), e);
          }
        }
      }
      return existingEdits;
    }
  }

  protected class NodeCommenter extends AbstractRewriter
  {
    /**
     * String to be inserted at the beginning of lines to indicate the line comment
     */
    protected static final String LINE_COMMENT_STRING = "//";

    protected static final String EMPTY_STRING = "";

    /**
     * Map of commented out nodes to their tracked positions
     */
    protected Map<ASTNode, ITrackedNodePosition> commentedOutPositions = new HashMap<ASTNode, ITrackedNodePosition>();

    /**
     * Responsible for inserting line breaks at the beginning and the end of the commented out nodes
     */
    protected LineBreakInserter lineBreakInserter;
    
    /**
     * Map of insert offsets to InsertEdit objects created.
     * Used to prevent adding different InsertEdit objects at the same offset.
     */
    protected Map<Integer, InsertEdit> addedInsertEdits = new HashMap<Integer, InsertEdit>();
    
    /**
     * List of currently added text edits that have to be reverted (removed) in case of an exception.
     */
    protected List<TextEdit> textEditsToRevert = new ArrayList<TextEdit>();
    
    /**
     * Document after call to {@link ASTRewrite#rewriteAST()} or {@link ASTRewrite#rewriteAST(IDocument, Map)}.
     */
    protected IDocument doc;
    
    /**
     * Enables tracking for all commented out nodes.
     * <p>
     * This constructor must be called before call to {@link ASTRewrite#rewriteAST()} or {@link ASTRewrite#rewriteAST(IDocument, Map)}.
     * 
     * @see ASTRewrite#track(ASTNode)
     */    
    public NodeCommenter()
    {
      for (ASTNode node : getCommentedOutNodes())
      {
        try
        {
          commentedOutPositions.put(node, rewriter.track(node));
        }
        catch (Exception e)
        {
          if (ASTFacadeHelper.DEBUG)
          {
            getFacadeHelper().logError("Enabling tracking in " + getName(), e);
          }
        }
      }
    }

    /**
     * Adds a {@link TextEdit} that comments out required nodes.
     * 
     * @param existingEdits existing edits on  
     * @param doc document after call to {@link ASTRewrite#rewriteAST()} or {@link ASTRewrite#rewriteAST(IDocument, Map)}.
     * @return existingEdits with new edits added
     * 
     * @see NodeContentsReplacer#trackedNodePositionsMap
     */    
    @Override
    protected TextEdit addEdits(TextEdit existingEdits, IDocument doc)
    {
      this.doc = doc;
      lineBreakInserter = new LineBreakInserter();      

      // loop for all commented out nodes
      for (Map.Entry<ASTNode, ITrackedNodePosition> entry : commentedOutPositions.entrySet())
      {
        ASTNode node = entry.getKey();
        ITrackedNodePosition nodePosition = entry.getValue();
        try
        {
          // insert line break and comment out the first line if needed
          // note that first line might move backward beyond node start (e.g. if a comma of the previous enum constant has been commented out)
          int firstLine = addLineBreakBeforeNode(existingEdits, nodePosition, node);

          // comment out all lines of the node itself, from firstLine to last line
          int lastLine = doc.getLineOfOffset(nodePosition.getStartPosition() + nodePosition.getLength());
          commentOutLines(existingEdits, firstLine, lastLine);

          // if the node is less than 1 line long, but its contents is replaced by multiple lines,
          // comment out each line in it
          if (firstLine == lastLine && getAllTrackedContentsMap().containsKey(node))
          {
            findAndCommentOutReplaceEdit(existingEdits, nodePosition);
          }
          
          // if there is anything after the node on the same line, insert line break
          addLineBreakAfterNode(existingEdits, nodePosition, node);
          
          // reset text edits; since this node is processed successfully, there is no need to revert these changes
          textEditsToRevert.clear();
        }
        catch (Exception e)
        {
          // revert all current edits for this node
          for (TextEdit edit : textEditsToRevert)
          {
            existingEdits.removeChild(edit);
          }
          textEditsToRevert.clear();

          // log the error, ignore the change and continue
          if (ASTFacadeHelper.DEBUG)
          {
            try
            {
              getFacadeHelper().logError(
                "Unable to comment out <" + doc.get().substring(nodePosition.getStartPosition(), nodePosition.getLength()) + "> in "
                  + getName() + " : " + e.toString() + ". There should be no tracked changes to commented out nodes.");
            }
            catch (Exception innerException)
            {
              getFacadeHelper().logError(
                "Unable to comment out node in " + getName() + " : " + e.toString() + ". Unable to get contents of the node either : "
                  + innerException.toString() + ". There should be no tracked changes to commented out nodes.");
            }
          }
        }
      }
      return existingEdits;
    }

    /**
     * Add line break before the node if needed. If there was a line break added at this position (e.g. after the previous node), replace it.
     * Added line break will contain the marker to comment out the first line of the node.
     * If <code>InsertEdit</code> has been created, the returned line number is the line after the line break, i.e. the next
     * line that has to be commented out.
     * If no changes are made, returned line number is the first line of the node.
     * 
     * @param existingEdits
     * @param nodePosition
     * @param node
     * @return line number of the next line that has to be commented out
     * @throws BadLocationException
     * 
     * @see LineBreakInserter#createLineBreakBeforeNode(ITrackedNodePosition, ASTNode)
     */
    protected int addLineBreakBeforeNode(TextEdit existingEdits, ITrackedNodePosition nodePosition, ASTNode node) throws BadLocationException
    {
      // insert line break at the first line if there is something before the node start and the beginning of the line
      InsertEdit lineBreakEdit = lineBreakInserter.createLineBreakBeforeNode(nodePosition, node);
      if (lineBreakEdit != null)
      {
        // replace existing InsertEdit at this position
        // (case when previous node ends at the same position that current node starts at)
        InsertEdit existingEdit = addedInsertEdits.get(lineBreakEdit.getOffset());
        if (existingEdit != null)
        {
          existingEdits.removeChild(existingEdit);
          addedInsertEdits.values().remove(existingEdit);
        }
        existingEdits.addChild(lineBreakEdit);
        textEditsToRevert.add(lineBreakEdit);
        addedInsertEdits.put(lineBreakEdit.getOffset(), lineBreakEdit);
        
        // this line has been commented out, return next line number
        return doc.getLineOfOffset(lineBreakEdit.getOffset()) + 1;
      }
      else
      {
        // return the line number of the start of the node
        return doc.getLineOfOffset(nodePosition.getStartPosition());
      }
    }
    
    /**
     * Creates and adds <code>InsertEdit</code>s that comment out all lines between <code>firstLine</code> and
     * <code>lastLine</code> inclusively.
     * <p>
     * If there is a ReplaceEdit that covers positions where <code>InsertEdit</code>s are inserted,
     * then ReplaceEdit is replaced by another ReplaceEdit with modified text with all lines commented out.
     * <p>
     * If there is any other problem adding new <code>InsertEdit</code>s to existing edits, original exception
     * is re-thrown. 
     * 
     * @param existingEdits
     * @param firstLine
     * @param lastLine
     * @throws BadLocationException
     */
    protected void commentOutLines(TextEdit existingEdits, int firstLine, int lastLine) throws BadLocationException
    {
      for (int i = firstLine; i <= lastLine; i++)
      {
        InsertEdit edit = new InsertEdit(doc.getLineOffset(i), LINE_COMMENT_STRING);
        try 
        {
          existingEdits.addChild(edit);
          textEditsToRevert.add(edit);
          addedInsertEdits.put(edit.getOffset(), edit);          
        }
        catch (MalformedTreeException e)
        {
          // handle the case when there is a replace edit that covers these lines
          TextEdit causeEdit = e.getChild();
          if (causeEdit instanceof ReplaceEdit)
          {
            ReplaceEdit newReplaceEdit = commentOutReplaceEdit((ReplaceEdit)causeEdit);
            // skip all lines that replace edit covers
            i = doc.getLineOfOffset(newReplaceEdit.getOffset() + newReplaceEdit.getLength());
          }
          else
          {
            // should not happen, re-throw exception
            throw e;
          }
        }
      }
    }

    /**
     * Replaces given {@link ReplaceEdit} by new ReplaceEdit with each line commented out.
     * <p>
     * New ReplaceEdit has the same offset and length as the given ReplaceEdit. Text of new ReplaceEdit
     * has each line but the first one commented out. Given ReplaceEdit is removed from its parent,
     * and new ReplaceEdit is inserted in its place.
     * @param replaceEdit
     * @return new ReplaceEdit
     */
    protected ReplaceEdit commentOutReplaceEdit(ReplaceEdit replaceEdit)
    {
      TextEdit parent = replaceEdit.getParent();
      String newText = commentOutEachLine(replaceEdit.getText());
      ReplaceEdit newEdit = new ReplaceEdit(replaceEdit.getOffset(), replaceEdit.getLength(), newText);
      parent.removeChild(replaceEdit);
      parent.addChild(newEdit);
      return newEdit;
    }

    /**
     * Finds first ReplaceEdit in existing edits that covers node position range, and comments out
     * each line in it.
     * <p> 
     * This method is used in the case when existing replace edit covers only a part of one line,
     * but the contents that it replaces is longer than 1 line. In this case, such replace edit
     * will be found and its contents changed by this method.
     * 
     * @param existingEdits
     * @param nodePosition range of existing node that has a corresponding ReplaceEdit for node's range
     * @see #commentOutReplaceEdit(ReplaceEdit)
     */
    protected void findAndCommentOutReplaceEdit(TextEdit existingEdits, ITrackedNodePosition nodePosition)
    {
      // create and try to add dummy edit to find the ReplaceEdit
      // this should be faster than lookup since underneath of addChild() binary search is used
      ReplaceEdit dummyEdit = new ReplaceEdit(nodePosition.getStartPosition() + 1, 0, "");
      try
      {
        existingEdits.addChild(dummyEdit);
      }
      catch (MalformedTreeException e)
      {
        TextEdit causeEdit = e.getChild();
        if (causeEdit instanceof ReplaceEdit)
        {
          commentOutReplaceEdit((ReplaceEdit)causeEdit);
        }
        else if (ASTFacadeHelper.DEBUG)
        {
          // this should never happen
          getFacadeHelper().logError("Unable to find ReplaceEdit for node in " + getName() + " : " + e.toString());
        }
      }
      finally
      {
        // make sure that dummy edit is not in the tree
        try
        {
          existingEdits.removeChild(dummyEdit);
        }
        catch (Exception e)
        {
          // Ignore
        }
      }
    }

    /**
     * Comments out each line but the first one in the given text, and returns resulting new text.
     * @param text
     * @return new text with each line but the first one commented out
     */
    protected String commentOutEachLine(String text)
    {
      // assume length will grow by 10% (average line length is 20 characters)
      StringBuilder sb = new StringBuilder(text.length() + text.length() / 10);
      char[] textContent = text.toCharArray();
      int lastPos = 0, currentPos = 0;
      
      for (int i = 0; i < textContent.length; i++)
      {
        if (textContent[i] == '\n')
        {
          currentPos = i;
        }
        else if (textContent[i] == '\r')
        {
          if (i + 1 < textContent.length && textContent[i + 1] == '\n')
          {
            // next position is checked as well
            currentPos = ++i;
          }
          else
          {
            currentPos = i;
          }
        }
        
        if (lastPos != currentPos)
        {
          // char at currentPos is copied as well
          sb.append(textContent, lastPos, currentPos - lastPos + 1);
          sb.append(LINE_COMMENT_STRING);
          // lastPos, currentPos points at the next chars that will be copied later
          lastPos = ++currentPos;
        }
      }
      
      // copy last piece if any
      if (currentPos < textContent.length)
      {
        sb.append(textContent, currentPos, textContent.length - currentPos);
      }      
      
      return sb.toString();
    }    
    
    /**
     * If there is anything after the node, inserts the line break to prevent commenting
     * out extra content.
     * 
     * @param existingEdits
     * @param nodePosition
     * @param node
     * @throws BadLocationException
     * 
     * @see LineBreakInserter#createLineBreakAfterNode(ITrackedNodePosition, ASTNode)
     */
    protected void addLineBreakAfterNode(TextEdit existingEdits, ITrackedNodePosition nodePosition, ASTNode node) throws BadLocationException
    {
      InsertEdit lineBreakEdit = lineBreakInserter.createLineBreakAfterNode(nodePosition, node);
      if (lineBreakEdit != null)
      {
        // do not add a new line if there is one
        InsertEdit existingEdit = addedInsertEdits.get(lineBreakEdit.getOffset());
        if (existingEdit == null)
        {
          existingEdits.addChild(lineBreakEdit);
          textEditsToRevert.add(lineBreakEdit);
          addedInsertEdits.put(lineBreakEdit.getOffset(), lineBreakEdit);
        }
      }
    }    
    
    /**
     * Class that inserts extra line breaks between nodes when nodes are being commented out.
     * 
     * @see #createLineBreakBeforeNode(ITrackedNodePosition, ASTNode)
     * @see #createLineBreakAfterNode(ITrackedNodePosition, ASTNode)
     */
    protected class LineBreakInserter
    {
      protected char[] charContent;
    
      protected LineBreakInserter()
      {
        this.charContent = getDocument().get().toCharArray();
      }
    
      /**
       * @return document to be used to lookup line numbers and line information
       */
      protected IDocument getDocument()
      {
        return doc;
      }
      
      /**
       * Creates line break at the beginning of the node when there is another node declared at the same line.
       * The returned edit also contains "//" after the line break - the first line of the node becomes already commented out.
       * @param nodePosition
       * @param node
       * 
       * @return <code>InsertEdit</code> or <code>null</code> if none required
       * @throws BadLocationException
       */
      protected InsertEdit createLineBreakBeforeNode(ITrackedNodePosition nodePosition, ASTNode node) throws BadLocationException
      {
        int startPos = nodePosition.getStartPosition();
        IRegion lineInfo = getDocument().getLineInformationOfOffset(startPos);
        
        // if needed, comment out a comma of the previous enum constant
        if (node.getNodeType() == ASTNode.ENUM_CONSTANT_DECLARATION)
        {
          InsertEdit insertEdit = commentOutEnumConstantSeparator((EnumConstantDeclaration)node, lineInfo, nodePosition);
          if (insertEdit != null)
          {
            return insertEdit;
          }
        }
        
        // if there is anything before the node on the same line, create line break and comment out the first line of the node
        if (!isWhitespace(lineInfo.getOffset(), startPos))
        {
          return new InsertEdit(startPos, createLineBreakString(lineInfo.getOffset(), true));
        }
        else
        {
          // there is only whitespace on this line - no need for line break
          return null;
        }
      }
    
      /**
       * Creates line break at the end of the node when there is another node declared at the same line.
       * @param nodePosition
       * @param node
       * 
       * @return <code>InsertEdit</code> or <code>null</code> if none required
       * @throws BadLocationException
       */
      protected InsertEdit createLineBreakAfterNode(ITrackedNodePosition nodePosition, ASTNode node) throws BadLocationException
      {
        int endPos = nodePosition.getStartPosition() + nodePosition.getLength();        
        IRegion lineInfo = getDocument().getLineInformationOfOffset(endPos);
    
        // for enum constants, insert line break after the comma if there is anything after the comma
        if (node.getNodeType() == ASTNode.ENUM_CONSTANT_DECLARATION)
        {
          return createLineBreakAfterEnumConstant(node, lineInfo, nodePosition);
        }
        
        // if there is any content after the node on the same line, insert line break (to prevent commenting out extra content)
        else if (!isWhitespace(endPos, lineInfo.getOffset() + lineInfo.getLength()))
        {
          return new InsertEdit(endPos, createLineBreakString(lineInfo.getOffset(), false));
        }
        else
        {
          // there is only whitespace on this line - no need for line break
          return null;
        }
      }
      
      /**
       * @param lineStart the first character of the line excluding <code>CR</code> or <code>LF</code> characters.
       * @return indent of the line starting at <code>lineStart</code>, empty string if there is no indent or <code>lineStart</code> is invalid position
       * @see IndentManipulation#isIndentChar(char)
       */
      protected String getIndent(int lineStart)
      {
        if (lineStart >= 0 && lineStart < charContent.length)
        {
          int i = lineStart;
          while (i < charContent.length && IndentManipulation.isIndentChar(charContent[i]))
          {
            i++;
          }
          return new String(charContent, lineStart, i - lineStart);
        }
        return EMPTY_STRING;
      }
    
      /**
       * Determines if there are only whitespace characters in the given range in char array.
       * @param start
       * @param end
       * @return <code>true</code> if only whitespace characters are between <code>start</code> and <code>end</code>
       * 
       * @see Character#isWhitespace(char)
       */
      protected boolean isWhitespace(int start, int end)
      {
        if (start >= 0 && end < charContent.length && start <= end)
        {
          for (int i = start; i < end; i++)
          {
            if (!Character.isWhitespace(charContent[i]))
            {
              return false;
            }
          }
        }
        return true;
      }
      
      /**
       * Creates line break string containing line delimiter, line comment string if <code>isCommentedOut</code> is <code>true</code>,
       * and same indent string as the line that break is inserted at. 
       * @param startOfLineOffset
       * @param isCommentedOut
       * @return line break string
       * @throws BadLocationException
       */
      protected String createLineBreakString(int startOfLineOffset, boolean isCommentedOut) throws BadLocationException
      {
        return createLineBreakString(getDocument().getLineDelimiter(getDocument().getLineOfOffset(startOfLineOffset)), startOfLineOffset, isCommentedOut);
      }
      
      /**
       * Creates line break string containing line delimiter, line comment string if <code>isCommentedOut</code> is <code>true</code>,
       * and indent string that is the same as of the line that break is inserted at. 
       * 
       * @param lineDelimiter
       * @param startOfLineOffset
       * @param isCommentedOut
       * @return line break string
       */
      protected String createLineBreakString(String lineDelimiter, int startOfLineOffset, boolean isCommentedOut)
      {
        String indent = getIndent(startOfLineOffset);
        StringBuilder sb = new StringBuilder(indent.length() + 10);
        sb.append(lineDelimiter);
        if (isCommentedOut)
        {
          sb.append(LINE_COMMENT_STRING);
        }
        sb.append(indent);
        return sb.toString();
      }

      /**
       * Creates an InsertEdit that comments out separator after the previous enum constant.
       * <p>
       * Separator of the previous constant needs to be commented out when all following constants are commented out.
       * <p>
       * For example, if there are constants <code>C1, C2, C3;</code> and constants C2 and C3 are commented out, comma after C1 should be commented out as well.
       * When this method is called on the first line of C2, the returned edit will comment out a comma after C1. Calling this method on C3 will return <code>null</code> since C2 and C3 are both
       * commented out.
       * <p>
       * Returned edit (if any) may or may not contain a line break depending whether there is any content before the comma on the same line.
       * 
       * @param enumConstant
       * @param lineInfo
       * @param nodePosition
       * @return <code>InsertEdit</code> or <code>null</code> if none required
       * @throws BadLocationException
       */
      private InsertEdit commentOutEnumConstantSeparator(EnumConstantDeclaration enumConstant, IRegion lineInfo, ITrackedNodePosition nodePosition) throws BadLocationException
      {
        // if previous node is not commented out, but all the following nodes are, comment out a comma (constant separator)
        ASTJNode<?> astjNode = (ASTJNode<?>)getFacadeHelper().convertToNode(enumConstant);
        if (astjNode != null)
        {
          ASTNode parent = astjNode.getParent().getASTNode();
          List<?> enumConstants = rewriter.getListRewrite(parent, EnumDeclaration.ENUM_CONSTANTS_PROPERTY).getRewrittenList();
          int constantIndex = enumConstants.indexOf(enumConstant);
          if (constantIndex > 0 && constantIndex < enumConstants.size())
          {
            ASTNode previousNode = (ASTNode)enumConstants.get(constantIndex - 1);
            List<?> followingConstants = enumConstants.subList(constantIndex, enumConstants.size());
            // if previous node is not commented out, but all following are
            if (!commentedOutPositions.containsKey(previousNode) && commentedOutPositions.keySet().containsAll(followingConstants))
            {
              // insert new line at the end of previous constant
              int commaPosition = nodePosition.getStartPosition() - 1;
              while (commaPosition >=0 && Character.isWhitespace(charContent[commaPosition]))
              {
                commaPosition--;
              }
              
              // we should be able to find the comma because the range for enum constants includes all preceding comments up to the previous constant
              // if TargetSourceRangeComputer of ASTRewrite changes, this logic should change to skip comments
              // see org.eclipse.emf.codegen.merge.java.facade.ast.CommentAwareSourceRangeComputer#getEnumConstantSourceRange(ASTNode)
              if (commaPosition >=0 && charContent[commaPosition] == ',')
              {
                // if comma is on a line by itself, comment it out but do not insert the line break
                int line = getDocument().getLineOfOffset(commaPosition);
                int startOfLineOffset = getDocument().getLineOffset(line);
                if (isWhitespace(startOfLineOffset, commaPosition))
                {
                  return new InsertEdit(startOfLineOffset, LINE_COMMENT_STRING);
                }
                else
                {
                  return new InsertEdit(commaPosition, createLineBreakString(getDocument().getLineDelimiter(line), startOfLineOffset, true));
                }
              }
            }
          }
        }
        return null;
      }

      /**
       * If needed, creates a line break after enum constant.
       * 
       * @param node
       * @param lineInfo
       * @param nodePosition
       * @return <code>InsertEdit</code> or <code>null</code> if none required
       * @throws BadLocationException
       */
      private InsertEdit createLineBreakAfterEnumConstant(ASTNode node, IRegion lineInfo, ITrackedNodePosition nodePosition) throws BadLocationException
      {
        int nodeEndPos = nodePosition.getStartPosition() + nodePosition.getLength();
        int endOfLine = lineInfo.getOffset() + lineInfo.getLength();
        int i = nodeEndPos;

        // skip whitespace, up to the end of the line
        while (i <= endOfLine && Character.isWhitespace(charContent[i]))
        {
          i++;
        }

        // insert line break if there is non-whitespace before end of the line
        if (i < endOfLine)
        {
          // if current char is comma, insert line break after it
          //
          // we should be able to find the comma because the range for enum constants includes all trailing comments up to the separator
          // if TargetSourceRangeComputer of ASTRewrite changes, this logic should change to skip comments
          // see org.eclipse.emf.codegen.merge.java.facade.ast.CommentAwareSourceRangeComputer#getEnumConstantSourceRange(ASTNode)
          if (charContent[i] == ',')
          {
            // do not insert line break if there is only whitespace after comma
            if (!isWhitespace(i + 1, endOfLine))
            {
              return new InsertEdit(i + 1, createLineBreakString(lineInfo.getOffset(), false));
            }
          }
          else
          {
            // there is anything else but comma after constant - insert line break
            return new InsertEdit(i, createLineBreakString(lineInfo.getOffset(), false));
          }
        }
        // there is only whitespace after enum constant on the same line - do not add line breaks
        return null;
      }
      
    }
  }
}
