/**
 * 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.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.Annotation;
import org.eclipse.jdt.core.dom.Comment;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.StructuralPropertyDescriptor;
import org.eclipse.jdt.core.dom.rewrite.ASTRewrite;
import org.eclipse.jdt.core.dom.rewrite.TargetSourceRangeComputer;


/**
 * Class that computes the ranges for the nodes for use by {@link ASTRewrite}. The ranges for the nodes will include
 * the comments between the nodes in order to preserve as many comments as possible when
 * nodes are moved or removed.
 * <p>
 * The default range for any node will include the comments immediately preceding the node and
 * the line comment at the last line of the node.
 * <p>
 * If the node and the previous node have not been marked for removal,
 * the range will also include all leading comments up to the previous node.
 * <p>
 * If the node is marked for removal, the range for node itself will be the default range. 
 * The leading comments between the node for removal and previous node will be in the range for
 * the previous node.
 * <p>
 * When the node is going to be removed, {@link #markNodeForRemoval(ASTNode)} must be called.
 * To undo the removal (in a situation where the removed node is inserted into the tree, in other words,
 * node is moved), {@link #unmarkNodeForRemoval(ASTNode)} must be called.
 * All nodes must be marked as removed before {@link ASTRewrite#rewriteAST()} or {@link ASTRewrite#rewriteAST(org.eclipse.jface.text.IDocument, Map)} is called.
 * <p>
 * Note that {@link ASTRewrite} removes all content after the removed node up to the start of the next node.
 * Hence, if two consecutive nodes are removed, all comments between them are lost. 
 *  
 * @see ASTRewrite#setTargetSourceRangeComputer(TargetSourceRangeComputer)
 * @see TargetSourceRangeComputer
 */
public class CommentAwareSourceRangeComputer extends TargetSourceRangeComputer
{
  /**
   * Node types that this range computer changes range for.
   */
  protected static final Set<Integer> NODE_TYPES_WITH_SPECIAL_RANGE;
  static
  {
    NODE_TYPES_WITH_SPECIAL_RANGE = new HashSet<Integer>();

    NODE_TYPES_WITH_SPECIAL_RANGE.add(ASTNode.ANNOTATION_TYPE_DECLARATION);
    NODE_TYPES_WITH_SPECIAL_RANGE.add(ASTNode.ANNOTATION_TYPE_MEMBER_DECLARATION);

    NODE_TYPES_WITH_SPECIAL_RANGE.add(ASTNode.TYPE_DECLARATION);
    NODE_TYPES_WITH_SPECIAL_RANGE.add(ASTNode.ENUM_DECLARATION);
    NODE_TYPES_WITH_SPECIAL_RANGE.add(ASTNode.ENUM_CONSTANT_DECLARATION);

    // this could be method body, for example
    NODE_TYPES_WITH_SPECIAL_RANGE.add(ASTNode.BLOCK);  
    
    NODE_TYPES_WITH_SPECIAL_RANGE.add(ASTNode.SINGLE_MEMBER_ANNOTATION);
    NODE_TYPES_WITH_SPECIAL_RANGE.add(ASTNode.NORMAL_ANNOTATION);
    NODE_TYPES_WITH_SPECIAL_RANGE.add(ASTNode.MARKER_ANNOTATION);
    
    NODE_TYPES_WITH_SPECIAL_RANGE.add(ASTNode.METHOD_DECLARATION);
    NODE_TYPES_WITH_SPECIAL_RANGE.add(ASTNode.FIELD_DECLARATION);
    
    NODE_TYPES_WITH_SPECIAL_RANGE.add(ASTNode.IMPORT_DECLARATION);
  }
  
  /**
   * Array of comments from compilation unit
   */
  protected Comment[] commentArray = null;

  /**
   * Start positions of all comments
   */
  protected int[] commentStartPositions = null;

  /**
   * End positions of all comments
   */
  protected int[] commentEndPositions = null;

  /**
   * Compilation unit used to get extended ranges of the nodes
   */
  protected CompilationUnit compilationUnit = null;

  /**
   * Map of nodes to the trailing <code>Comment</code> nodes that must be included
   * in the range for the node
   */
  protected Map<ASTNode, Comment> includeTrailingCommentMapper = new HashMap<ASTNode, Comment>();

  /**
   * Set of nodes that must have default range returned for them 
   */
  protected Set<ASTNode> nodesWithDefaultRange = new HashSet<ASTNode>();

  /**
   * Original source used to create compilation unit
   */
  protected String source = null;
  
  /**
   * @param compilationUnit to use to get a list of comments and node positions
   * @param source original source used to create compilation unit
   */
  public CommentAwareSourceRangeComputer(CompilationUnit compilationUnit, String source)
  {
    this.compilationUnit = compilationUnit;
    this.source = source;

    List<?> commentList = compilationUnit.getCommentList();
    if (commentList != null)
    {
      this.commentArray = commentList.toArray(new Comment[commentList.size()]);
      this.commentStartPositions = new int [commentList.size()];
      this.commentEndPositions = new int [commentList.size()];
      int i = 0;
      for (Iterator<?> iter = commentList.iterator(); iter.hasNext();)
      {
        Comment comment = (Comment)iter.next();
        commentStartPositions[i] = comment.getStartPosition();
        commentEndPositions[i] = commentStartPositions[i] + comment.getLength();
        i++;
      }      
    }
    else
    {
      this.commentArray = new Comment [0];
      this.commentStartPositions = new int [0];
      this.commentEndPositions = new int [0];
    }
  }

  /**
   * Finds the farthest trailing comment of <code>node</code> before the next node. 
   * If <code>includeHangingCommentsOnly</code> is
   * set to true, only comments up to the extended range of the next node will be considered.
   * If <code>includeHangingCommentsOnly</code> is
   * set to false, the first comment after the default range of the previous node is returned.
   * 
   * @param node
   * @param nextNode
   * @param includeHangingCommentsOnly
   * @return comment, <code>null</code> if not found
   */
  protected Comment findTrailingComments(ASTNode node, ASTNode nextNode, boolean includeHangingCommentsOnly)
  {
    if (node == null || nextNode == null)
    {
      return null;
    }

    // range to look for comments
    int rangeStartPos = compilationUnit.getExtendedStartPosition(node) + compilationUnit.getExtendedLength(node);
    int rangeEndPos;
    if (includeHangingCommentsOnly)
    {
      rangeEndPos = compilationUnit.getExtendedStartPosition(nextNode);
    }
    else
    {
      rangeEndPos = nextNode.getStartPosition();
    }

    int commentIndex = findLastCommentInRangeIndex(rangeStartPos, rangeEndPos);
    return commentIndex == -1 ? null : commentArray[commentIndex];
  }

  /**
   * Finds the farthest leading comment of <code>node</code> after the previous node. 
   * <p>
   * If <code>includeHangingCommentsOnly</code> is
   * set to true, only comments up to the extended range of the previous node will be considered.
   * If <code>includeHangingCommentsOnly</code> is
   * set to false, the first comment after the default source range of the previous node is returned.
   * 
   * @param node
   * @param previousNode the previous node
   * @param includeHangingCommentsOnly
   * @return comment, <code>null</code> if not found
   * 
   * @see #computeDefaultSourceRange(ASTNode)
   */
  protected Comment findLeadingComment(ASTNode node, ASTNode previousNode, boolean includeHangingCommentsOnly)
  {
    if (node == null || previousNode == null)
    {
      return null;
    }

    // range to look for comments
    int rangeStartPos;
    if (includeHangingCommentsOnly)
    {
      rangeStartPos = compilationUnit.getExtendedStartPosition(previousNode) + compilationUnit.getExtendedLength(previousNode);
    }
    else
    {
      // get the end position of the range for the previous node
      SourceRange range = computeDefaultSourceRange(previousNode);
      rangeStartPos = range.getStartPosition() + range.getLength();//previousNode.getStartPosition() + previousNode.getLength();
    }

    int rangeEndPos = compilationUnit.getExtendedStartPosition(node);

    int commentIndex = findFirstCommentInRangeIndex(rangeStartPos, rangeEndPos);
    return commentIndex == -1 ? null : commentArray[commentIndex];
  }

  /**
   * Finds index of the last comment in the given range. 
   * Start and end position of the resulting comment must be between <code>rangeStartPos</code>
   * and <code>rangeEndPos</code> inclusively.
   * 
   * @param rangeStartPos
   * @param rangeEndPos
   * @return comment index, <code>-1</code> if not found
   */
  final protected int findLastCommentInRangeIndex(int rangeStartPos, int rangeEndPos)
  {
    int commentIndex = Arrays.binarySearch(commentEndPositions, rangeEndPos);

    if (commentIndex < 0)
    {
      // find insertion point from returned result (-(insertion point) - 1)
      // commentIndex is the index of the first element with end position > rangeEndPos
      commentIndex = -(commentIndex) - 1;

      // if there is an element with end position <= rangeEndPos
      if (commentIndex > 0)
      {
        // the previous element must have end position <= rangeEndPos
        commentIndex--;
        if (commentStartPositions[commentIndex] >= rangeStartPos)
        {
          return commentIndex;
        }
      }
    }
    // else - there is a comment that ends exactly at rangeEndPos    
    else
    {
      if (commentStartPositions[commentIndex] >= rangeStartPos)
      {
        return commentIndex;
      }
    }

    return -1;
  }

  /**
   * Finds index of the first comment that is completely in the given range. 
   * Start and end position of the resulting comment must be between <code>rangeStartPos</code>
   * and <code>rangeEndPos</code> inclusively.
   * 
   * @param rangeStartPos
   * @param rangeEndPos
   * @return comment index, <code>-1</code> if not found
   */
  final protected int findFirstCommentInRangeIndex(int rangeStartPos, int rangeEndPos)
  {
    int commentIndex = Arrays.binarySearch(commentStartPositions, rangeStartPos);

    if (commentIndex < 0)
    {
      // find insertion point from returned result (-(insertion point) - 1)
      // commentIndex is an index of the first element that has start position > rangeStartPos 
      commentIndex = -(commentIndex) - 1;

      // if there is a comment with start position > rangeStartPos
      if (commentIndex < commentArray.length)
      {
        if (commentEndPositions[commentIndex] <= rangeEndPos)
        {
          return commentIndex;
        }
      }
    }
    // else - there is a comment that starts exactly at rangeStartPos
    else
    {
      if (commentEndPositions[commentIndex] <= rangeEndPos)
      {
        return commentIndex;
      }
    }

    return -1;
  }

  /**
   * Extends the given range to include the range of <code>nodeToAdd</code>.
   * <p>
   * Range of <code>nodeToAdd</code> must be outside and after the given <code>range</code>.
   * Otherwise, given <code>range</code> is returned.
   * 
   * @param nodeToAdd
   * @param range
   * @return new range
   */
  protected SourceRange extendRangeForward(ASTNode nodeToAdd, SourceRange range)
  {
    if (nodeToAdd != null && nodeToAdd.getStartPosition() > range.getStartPosition() + range.getLength())
    {
      return new SourceRange(range.getStartPosition(), nodeToAdd.getStartPosition() + nodeToAdd.getLength() - range.getStartPosition());
    }
    return range;
  }

  /**
   * Extends the given range to include the range of <code>nodeToAdd</code>.
   * <p>
   * Range of <code>nodeToAdd</code> must be outside and before the given <code>range</code>. 
   * Otherwise, given <code>range</code> is returned. 
   * 
   * @param nodeToAdd
   * @param range
   * @return new range
   */
  protected SourceRange extendRangeBackward(ASTNode nodeToAdd, SourceRange range)
  {
    if (nodeToAdd != null)
    {
      int nodeEndPos = nodeToAdd.getStartPosition() + nodeToAdd.getLength();
      if (nodeEndPos >= 0 && nodeEndPos < range.getStartPosition())
      {
        return new SourceRange(nodeToAdd.getStartPosition(), range.getStartPosition() + range.getLength() - nodeToAdd.getStartPosition());
      }
    }
    return range;
  }
  
  /**
   * Finds the start position of preceding comments of the given node.
   * <p>
   * Uses source to check if there is only whitespace between comments.
   * <p>
   * This method uses extended start position of the node as a starting position
   * to look for comments.
   * 
   * @param node
   * @return start position of the first preceding comment, or extended start position if there are no comments
   */
  protected int computeStartOfPrecedingComments(ASTNode node)
  {
    int nodeStartPosition = compilationUnit.getExtendedStartPosition(node);
    
    if (nodeStartPosition >= 0)
    {
      // find start position of farthest preceding comment
      ASTNode prevNode = getPreviousNode(node);
      int minStartPosition = prevNode == null ? 0 : compilationUnit.getExtendedStartPosition(prevNode) + compilationUnit.getExtendedLength(prevNode);
      int commentIndex = findLastCommentInRangeIndex(minStartPosition, nodeStartPosition);
      while(commentIndex >= 0)
      {
        int commentStartPosition = commentArray[commentIndex].getStartPosition();
        int commentEndPosition = commentStartPosition + commentArray[commentIndex].getLength();
        if (commentStartPosition >= minStartPosition && isWhitespace(commentEndPosition, nodeStartPosition))
        {
          nodeStartPosition = commentStartPosition;
          commentIndex--;
        }
        else
        {
          break;
        }
      }
    }
    return nodeStartPosition;
  }
  
  /**
   * Finds the end position of trailing comments of the given node.
   * <p>
   * Uses source to check if there is only whitespace between comments.
   * <p>
   * This method uses extended end position of the node as a starting position
   * to look for comments.
   * 
   * @param node
   * @return end position of the last trailing comment, or extended end position if there are no comments
   */
  protected int computeEndOfTrailingComments(ASTNode node)
  {
    int nodeEndPosition = compilationUnit.getExtendedStartPosition(node) + compilationUnit.getExtendedLength(node);
    
    if (nodeEndPosition >= 0)
    {
      // find start position of farthest preceding comment
      ASTNode nextNode = getNextNode(node);
      int maxEndPosition = nextNode == null ? source.length() : compilationUnit.getExtendedStartPosition(nextNode);
      int commentIndex = findFirstCommentInRangeIndex(nodeEndPosition, maxEndPosition);
      while(commentIndex >= 0 && commentIndex < commentArray.length)
      {
        int commentStartPosition = commentArray[commentIndex].getStartPosition();
        int commentEndPosition = commentStartPosition + commentArray[commentIndex].getLength();
        if (commentEndPosition <= maxEndPosition && isWhitespace(nodeEndPosition, commentStartPosition))
        {
          nodeEndPosition = commentEndPosition;
          commentIndex++;
        }
        else
        {
          break;
        }
      }
    }
    return nodeEndPosition;
  }
  
  /**
   * Finds the node that follows the given node.
   * 
   * @param node
   * @return next node, or <code>null</code> if not found
   */
  protected ASTNode getNextNode(ASTNode node)
  {
    if (node == null)
    {
      return null;
    }
    StructuralPropertyDescriptor locationInParent = node.getLocationInParent();
    if (locationInParent != null && locationInParent.isChildListProperty())
    {
      ASTNode parent = node.getParent();
      List<?> siblings = (List<?>)parent.getStructuralProperty(locationInParent);
      int index = siblings.indexOf(node);
      if (index >= 0 && index < siblings.size() - 1)
      {
        return (ASTNode)siblings.get(index + 1);
      }
    }
    return null;
  }

  /**
   * Finds the node that precedes the given node.
   * 
   * @param node
   * @return previous node, or <code>null</code> if not found
   */
  protected ASTNode getPreviousNode(ASTNode node)
  {
    if (node == null)
    {
      return null;
    }
    StructuralPropertyDescriptor locationInParent = node.getLocationInParent();
    if (locationInParent != null && locationInParent.isChildListProperty())
    {
      ASTNode parent = node.getParent();
      List<?> siblings = (List<?>)parent.getStructuralProperty(locationInParent);
      int index = siblings.indexOf(node);
      if (index > 0 && index < siblings.size())
      {
        return (ASTNode)siblings.get(index - 1);
      }
    }
    return null;
  }

  /**
   * Mark this node as the node to be deleted.
   * <p>
   * The node marked for removal will use the default range.
   * <p>
   * The leading hanging comments will be added to the range of the previous node.
   * 
   * @param node
   * 
   * @see #computeDefaultSourceRange(ASTNode)
   * @see #unmarkNodeForRemoval(ASTNode)
   */
  public void markNodeForRemoval(ASTNode node)
  {
    nodesWithDefaultRange.add(node);

    // add leading hanging comments to the previous node,
    // hence, only comments immediately preceding the node will be in the range for removal
    ASTNode keyNode = getPreviousNode(node);
    if (keyNode != null)
    {
      Comment trailingComment = findTrailingComments(keyNode, node, true);
      if (trailingComment != null)
      {
        includeTrailingCommentMapper.put(keyNode, trailingComment);
      }
    }
  }

  /**
   * Unmark the node as the node for removal.
   * @param node
   * 
   * @see #markNodeForRemoval(ASTNode)
   */
  public void unmarkNodeForRemoval(ASTNode node)
  {
    nodesWithDefaultRange.remove(node);
    includeTrailingCommentMapper.remove(getPreviousNode(node));
  }

  /**
   * Calculate the end of the line comment that is at the same line as <code>position</code>,
   * and with only whitespace between <code>position</code> and the start of the line comment.
   * <p>
   * Uses source contents and comment arrays.
   * <p>
   * 
   * @param position 
   * @return original position if no such comment exist
   */
  protected int determineEndPositionOfLineComment(int position)
  {
    if (position >= 0)
    {
      // extend to include the comment at the same line as the position 
      // if there is nothing between the position and the comment
      int firstTrailingCommentIndex = findFirstCommentInRangeIndex(position, Integer.MAX_VALUE);
      if (firstTrailingCommentIndex >= 0 && commentArray[firstTrailingCommentIndex].isLineComment())
      {
        int commentStartPos = commentArray[firstTrailingCommentIndex].getStartPosition();
        int commentEndPos = commentStartPos + commentArray[firstTrailingCommentIndex].getLength();
        if (compilationUnit.getLineNumber(commentStartPos) == compilationUnit.getLineNumber(position))
        {
          // check if there is just whitespace between position and start of the comment
          if (isWhitespace(position, commentStartPos))
          {
            return commentEndPos;
          }
        }
      }
    }
    return position;
  }
  
  /**
   * Calculate the end of the line comment that is at the same line as <code>nodeEnd</code>,
   * and ends before <code>nodeExtendedEnd</code>.
   * <p>
   * Does not use source contents.
   * <p>
   * 
   * @param nodeEnd
   * @param nodeExtendedEnd line comment must end before this position
   * @return original position if no such comment exist
   */
  protected int determineEndPositionOfLineComment(int nodeEnd, int nodeExtendedEnd)
  {
    if (nodeEnd < 0)
    {
      return nodeEnd;
    }
    if (nodeExtendedEnd < 0)
    {
      return nodeExtendedEnd;
    }
    
    // extend to include the comment at the same line as the position 
    int firstTrailingCommentIndex = findFirstCommentInRangeIndex(nodeEnd, nodeExtendedEnd);
    if (firstTrailingCommentIndex >= 0 && commentArray[firstTrailingCommentIndex].isLineComment())
    {
      int commentStartPos = commentArray[firstTrailingCommentIndex].getStartPosition();
      int commentEndPos = commentStartPos + commentArray[firstTrailingCommentIndex].getLength();
      if (compilationUnit.getLineNumber(commentStartPos) == compilationUnit.getLineNumber(nodeEnd))
      {
        return commentEndPos;
      }
    }
    return nodeEnd;
  }  
  
  /**
   * @param startPosition
   * @param endPosition
   * @return <code>true</code> if there is only whitespace between start and end position
   */
  protected boolean isWhitespace(int startPosition, int endPosition)
  {
    return source != null && startPosition >= 0 && endPosition < source.length()
      && source.substring(startPosition, endPosition).matches("\\s*");
  }
  
  /**
   * Specific method for enum constants. Returns
   * range that includes preceding comments, extended range,
   * trailing comments, and whitespace following the comments.
   * <p>
   * Using extended range as defined above is important to keep all the 
   * comments when enum constants are moved (e.g. removed and then inserted). 
   * <p>
   * Such extended range also allows keeping right separating new line characters between constants,
   * e.g. including trailing whitespace prevents putting constants at the end of line comments
   * on the same line (https://bugs.eclipse.org/bugs/show_bug.cgi?id=165703).
   * 
   * @param node
   * @return new extended range
   */
  protected SourceRange getEnumConstantSourceRange(ASTNode node)
  {
    // extend range backward
    int extendedStartPosition = computeStartOfPrecedingComments(node);
    
    // extend range forward
    int extendedEndPosition = computeEndOfTrailingComments(node);

    // add trailing whitespace
    extendedEndPosition = addWhitespaceAfterPosition(extendedEndPosition);

    return new SourceRange(extendedStartPosition, extendedEndPosition - extendedStartPosition);
  }
  
  /**
   * If possible, extends given position to include any whitespace following the position.
   * @param position
   */
  protected int addWhitespaceAfterPosition(int position)
  {
    if (source != null && position >= 0)
    {
      while(position < source.length() && Character.isWhitespace(source.charAt(position++)))
      {
        // increments the position to add the white spaces
      }
      return position - 1;
    }
    else
    {
      return position;
    }
  }  
  
  /**
   * Calculates the default range for the node.
   * <p>
   * The default range starts at an extended start position and ends at the non-extended end of the node.
   * If the node has a line comment on the last line of the node, this comment is included in the default range.
   * 
   * @param node
   * @return range for the node
   * 
   * @see CompilationUnit#getExtendedStartPosition(ASTNode)
   * @see ASTNode#getStartPosition() 
   */
  public SourceRange computeDefaultSourceRange(ASTNode node)
  {
    // for all nodes but some use default extended range
    if (!NODE_TYPES_WITH_SPECIAL_RANGE.contains(node.getNodeType()))
    {
      return super.computeSourceRange(node);
    }

    // for enum constants use special range
    if (node.getNodeType() == ASTNode.ENUM_CONSTANT_DECLARATION)
    {
      return getEnumConstantSourceRange(node);
    }
    
    int nodeStartPos = node.getStartPosition();
    int nodeEndPos = nodeStartPos + node.getLength();

    int extendedStartPos = compilationUnit.getExtendedStartPosition(node);
    int extendedEndPos = extendedStartPos + compilationUnit.getExtendedLength(node);

    // we don't want to include the line comments immediately before an import in the 
    // returned range
    if (node.getNodeType() == ASTNode.IMPORT_DECLARATION)
    {
      extendedStartPos = nodeStartPos;
    }
    
    // line comments at the end of annotations are not a part of the range of annotation
    // TODO is not including line comments at the end of annotations a bug?
    extendedEndPos = node instanceof Annotation ?
      determineEndPositionOfLineComment(nodeEndPos) :
      determineEndPositionOfLineComment(nodeEndPos, extendedEndPos);
    
    // include line comments following end of the nodes into the range
    int extendedLength = extendedEndPos - extendedStartPos;
    
    return new SourceRange(extendedStartPos, extendedLength);
  }

  /**
   * Checks if the range should be extended for the given node.
   * <p>
   * Range should be extended for all nodes in {@link #nodesWithDefaultRange} except
   * for enum constants and for nodes that should use the default range (i.e. removed nodes). 
   * 
   * @param node
   * @return <code>true</code> if range should be extended, <code>false</code> otherwise
   */
  protected boolean shouldHaveExtendedRange(ASTNode node)
  {
    // since enum constants are separated by commas, they should always have the default range
    // we can not extend their range or reduce it
    return node.getNodeType() != ASTNode.ENUM_CONSTANT_DECLARATION && !nodesWithDefaultRange.contains(node);
  }
  
  /**
   * Calculates the range of a node as follows:
   * <p>
   * For any node the range is at least the range returned by {@link #computeDefaultSourceRange(ASTNode)}.
   * <p>
   * If the node should have an extended range (as defined by {@link #shouldHaveExtendedRange(ASTNode)}),
   * the range is extended to include all leading comments up to the previous node, and all hanging trailing comments
   * if the next node has been removed.
   * 
   * @see #computeDefaultSourceRange(ASTNode)
   * @see #shouldHaveExtendedRange(ASTNode)
   * @see org.eclipse.jdt.core.dom.rewrite.TargetSourceRangeComputer#computeSourceRange(org.eclipse.jdt.core.dom.ASTNode)
   */
  @Override
  public SourceRange computeSourceRange(ASTNode node)
  {
    // for all nodes but some use default extended range
    if (!NODE_TYPES_WITH_SPECIAL_RANGE.contains(node.getNodeType()))
    {
      return super.computeSourceRange(node);
    }    
    
    SourceRange sourceRange = computeDefaultSourceRange(node);

    // check if given node should use the default range (i.e. node removed),
    // or extended range (i.e. surrounding nodes removed)
    if (shouldHaveExtendedRange(node))
    {
      // add leading comments
      ASTNode prevNode = getPreviousNode(node);
      if (prevNode != null)
      {
        sourceRange = extendRangeBackward(findLeadingComment(node, prevNode, false), sourceRange);
      }

      // add trailing comments if necessary
      sourceRange = extendRangeForward(includeTrailingCommentMapper.get(node), sourceRange);
    }
    return sourceRange;
  }

  //  /**
  //   * Simpler implementation that sets the range of any node to be the node and all preceding comments
  //   */
  //  public SourceRange computeSourceRange(ASTNode node)
  //  {
  //    SourceRange range = new SourceRange(node.getStartPosition(), node.getLength());
  //
  //    range = extendRangeBackward(findLeadingComment(node, getPreviousNode(node), false), range);
  //
  //    return range;
  //  }
}
