/**
 * Copyright (c) 2006-2007 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.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.compiler.IProblem;
import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.ASTParser;
import org.eclipse.jdt.core.dom.Annotation;
import org.eclipse.jdt.core.dom.AnnotationTypeDeclaration;
import org.eclipse.jdt.core.dom.AnnotationTypeMemberDeclaration;
import org.eclipse.jdt.core.dom.ArrayType;
import org.eclipse.jdt.core.dom.ChildListPropertyDescriptor;
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.FieldDeclaration;
import org.eclipse.jdt.core.dom.ImportDeclaration;
import org.eclipse.jdt.core.dom.Initializer;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.Name;
import org.eclipse.jdt.core.dom.PackageDeclaration;
import org.eclipse.jdt.core.dom.ParameterizedType;
import org.eclipse.jdt.core.dom.PrimitiveType;
import org.eclipse.jdt.core.dom.QualifiedType;
import org.eclipse.jdt.core.dom.SimpleType;
import org.eclipse.jdt.core.dom.Type;
import org.eclipse.jdt.core.dom.TypeDeclaration;
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
import org.eclipse.jdt.core.dom.rewrite.ASTRewrite;
import org.eclipse.jdt.core.dom.rewrite.ListRewrite;
import org.eclipse.jdt.core.dom.rewrite.TargetSourceRangeComputer;
import org.eclipse.jdt.core.dom.rewrite.TargetSourceRangeComputer.SourceRange;
import org.eclipse.jdt.core.formatter.DefaultCodeFormatterConstants;

import org.eclipse.emf.codegen.CodeGenPlugin;
import org.eclipse.emf.codegen.merge.java.JControlModel;
import org.eclipse.emf.codegen.merge.java.JMerger;
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.util.CodeGenUtil;
import org.eclipse.emf.common.util.BasicDiagnostic;
import org.eclipse.emf.common.util.Diagnostic;
import org.eclipse.emf.common.util.DiagnosticException;
import org.eclipse.emf.common.util.WrappedException;


/**
 * @since 2.2.0
 */
public class ASTFacadeHelper extends FacadeHelper
{
  /**
   * @deprecated Just use ASTRewrite directly.
   */
  @Deprecated
  public static class ASTRewriteWithRemove extends ASTRewrite
  {
    protected ASTRewriteWithRemove(AST ast)
    {
      super(ast);
    }
    
    /**
     * Disposes this ASTRewriteWithRemove
     */
    @SuppressWarnings("restriction")
    public void dispose()
    {
      getRewriteEventStore().clear();
      getNodeStore().clear();
      setTargetSourceRangeComputer(null);
    }
    
    /**
     * Workaround method that removes nodes similar to {@link #remove(ASTNode, org.eclipse.text.edits.TextEditGroup)}, but
     * it allows removal of newly created and inserted nodes that were not a part of original tree.
     * <p>
     * Note that {@link #remove(ASTNode, org.eclipse.text.edits.TextEditGroup)} does not remove newly created nodes that
     * have been inserted with {@link ListRewrite#insertFirst(ASTNode, org.eclipse.text.edits.TextEditGroup)} or
     * similar methods.
     * <p>
     * Workaround for <a href="https://bugs.eclipse.org/bugs/show_bug.cgi?id=164862">https://bugs.eclipse.org/bugs/show_bug.cgi?id=164862</a>
     * 
     * @param parent
     * @param childProperty
     * @param node
     */
    @SuppressWarnings("restriction")
    public void remove(ASTNode parent, ChildListPropertyDescriptor childProperty, ASTNode node)
    {
      ListRewrite lrw = getListRewrite(parent, childProperty);
      
      if (lrw.getRewrittenList().contains(node) && !lrw.getOriginalList().contains(node))
      {
        org.eclipse.jdt.internal.core.dom.rewrite.ListRewriteEvent listEvent = super.getRewriteEventStore().getListEvent(parent, childProperty, true);
        int index = listEvent.getIndex(node, org.eclipse.jdt.internal.core.dom.rewrite.ListRewriteEvent.NEW);
        if (index >= 0)
        {
          listEvent.revertChange((org.eclipse.jdt.internal.core.dom.rewrite.NodeRewriteEvent)listEvent.getChildren()[index]);
        }
        else
        {
          lrw.remove(node, null);
        }
      }
      else
      {
        lrw.remove(node, null);
      }
    }
  }
  
  
  /**
   * Debug output setting
   */
  protected final static boolean DEBUG = JMerger.DEBUG;

  /**
   * Converts {@link Name} to string representation.
   * 
   * @param name
   * @return fully qualified name or <code>null</code> if name parameter is <code>null</code>
   */
  public static String toString(Name name)
  {
    return name == null ? null : name.getFullyQualifiedName();
  }

  /**
   * Converts {@link SimpleType} to a string representation.
   * @param simpleType the simple type.
   * @return fully qualified name or <code>null</code> if simple type parameter is <code>null</code>
   * @since 2.15
   */
  public static String toString(SimpleType simpleType)
  {
    return simpleType == null ? null : simpleType.getName().getFullyQualifiedName();
  }

  public static String getTypeErasure(ArrayType arrayType)
  {
    StringBuilder sb = new StringBuilder(ASTFacadeHelper.getTypeErasure(arrayType.getElementType()));
    for (int i = arrayType.getDimensions(); i > 0; i--)
    {
      sb.append("[]");
    }
    return sb.toString();
  }

  public static String getTypeErasure(ParameterizedType parameterizedType)
  {
    return getTypeErasure(parameterizedType.getType());
  }

  public static String getTypeErasure(PrimitiveType primitiveType)
  {
    return primitiveType.getPrimitiveTypeCode().toString();
  }

  public static String getTypeErasure(SimpleType simpleType)
  {
    return ASTFacadeHelper.toString(simpleType.getName());
  }

  public static String getTypeErasure(QualifiedType qualifiedType)
  {
    StringBuilder sb = new StringBuilder(ASTFacadeHelper.getTypeErasure(qualifiedType.getQualifier()));
    sb.append(".");
    sb.append(ASTFacadeHelper.toString(qualifiedType.getName()));
    return sb.toString();
  }

  /**
   * Converts {@link Type} to string representation, erasing type parameters information.
   * <p>
   * This method is used to create a method signature, and match methods by signature.
   * 
   * @param type
   * @return string representing the type
   */
  public static String getTypeErasure(Type type)
  {
    if (type != null)
    {
      if (type.isArrayType())
      {
        return getTypeErasure((ArrayType)type);
      }
      else if (type.isParameterizedType())
      {
        return getTypeErasure((ParameterizedType)type);
      }
      else if (type.isPrimitiveType())
      {
        return getTypeErasure((PrimitiveType)type);
      }
      else if (type.isQualifiedType())
      {
        return getTypeErasure((QualifiedType)type);
      }
      else if (type.isSimpleType())
      {
        return getTypeErasure((SimpleType)type);
      }
      else if (type.isWildcardType())
      {
        return "";
      }
    }
    return "";
  }

  protected ASTNodeConverter nodeConverter = null;
  
  /**
   * Map of options set by default from <code>JavaCore.getOptions()</code>
   */
  protected Map<?, ?> javaCoreOptions = null;

  /**
   * Map of nodes to node contents. Used for caching only.
   */
  protected Map<ASTNode, String> nodeContents = new HashMap<ASTNode, String>();

  @Override
  public void reset()
  {
    nodeConverter = null;
    nodeContents.clear();
    super.reset();
  }
  
  /**
   * Creates and returns <code>ASTParser</code>.
   * 
   * @return new ASTParser object
   */
  protected ASTParser createASTParser()
  {
    // caching parser does not parse 2nd file in the same way (javadoc of package for example)
    // hence, new parser is created every time this method is called
    ASTParser astParser = CodeGenUtil.EclipseUtil.newASTParser(true);
    Map<String, String> javaCoreOptions = new HashMap<String, String>();
    for (Map.Entry<?, ?> entry : getJavaCoreOptions().entrySet())
    {
      Object key = entry.getKey();
      Object value = entry.getValue();
      if (key instanceof String && value instanceof String)
      {
        javaCoreOptions.put((String)key, (String)value);
      }
    }

    astParser.setCompilerOptions(javaCoreOptions);
    return astParser;
  }

  /* (non-Javadoc)
   * @see org.eclipse.emf.codegen.merge.java.facade.FacadeHelper#createCompilationUnit(java.lang.String, java.lang.String)
   */
  @Override
  public ASTJCompilationUnit createCompilationUnit(String name, String contents)
  {
    // set source
    char[] contentAsCharArray = contents.toCharArray();
    ASTParser astParser = createASTParser();
    astParser.setSource(contentAsCharArray);

    // parse
    CompilationUnit astCompilationUnit = (CompilationUnit)astParser.createAST(null);

    Diagnostic diagnostic = analyzeCompilationUnit(astCompilationUnit, contents);
    if (diagnostic != Diagnostic.OK_INSTANCE)
    { 
      StringBuilder message = new StringBuilder(diagnostic.getMessage());
      for (Diagnostic childDiagnostic : diagnostic.getChildren())
      {
        message.append("\n\t").append(childDiagnostic.getMessage());
      }
      message.append(contents);
      CodeGenPlugin.INSTANCE.log(message.toString());
      
      if (diagnostic.getSeverity() == Diagnostic.ERROR)
      {
        throw new WrappedException(new DiagnosticException(diagnostic));
      }
    }

    // create rewriter to record changes
    ASTRewrite rewriter = ASTRewrite.create(astCompilationUnit.getAST());

    // keep comments between nodes when removing or moving nodes
    rewriter.setTargetSourceRangeComputer(new CommentAwareSourceRangeComputer(astCompilationUnit, contents));

    // set properties
    astCompilationUnit.setProperty(ASTJCompilationUnit.NAME_PROPERTY, name);

    // create JNode and set properties
    ASTJCompilationUnit compilationUnit = (ASTJCompilationUnit)convertToNode(astCompilationUnit);
    compilationUnit.setOriginalContents(contentAsCharArray);
    compilationUnit.setRewriter(rewriter);

    return compilationUnit;
  }
  
  @Override
  public String getOriginalContents(JCompilationUnit compilationUnit)
  {
    return new String(((ASTJCompilationUnit)compilationUnit).getOriginalContents());
  }

  private Diagnostic analyzeCompilationUnit(CompilationUnit compilationUnit, String contents)
  {
    if (compilationUnit.getProblems().length == 0)
    {
      return Diagnostic.OK_INSTANCE;
    }
    else
    {
      BasicDiagnostic diagnostic = new BasicDiagnostic(
        CodeGenPlugin.ID,
        0,
        CodeGenPlugin.INSTANCE.getString("_UI_ParsingProblem_message"),
        contents == null ? null : new Object[] {new StringBuilder(contents)}
      );
      
      for (IProblem problem : compilationUnit.getProblems())
      {
        String message = problem.getMessage() != null ?
          CodeGenPlugin.INSTANCE.getString("_UI_LineNumberAndText_message", new Object[] {problem.getSourceLineNumber(), problem.getMessage()}) :
          CodeGenPlugin.INSTANCE.getString("_UI_LineNumber_message", new Object[] {problem.getSourceLineNumber()});
        
        BasicDiagnostic childDiagnostic = new BasicDiagnostic(
          problem.isWarning() ? Diagnostic.WARNING : problem.isError() ? Diagnostic.ERROR : Diagnostic.INFO,
          CodeGenPlugin.ID,
          0,
          message.toString(),
          contents == null ? new Object[]{problem} : new Object[]{problem, new StringBuilder(contents)}
        );
        diagnostic.add(childDiagnostic);
      }
      
      return diagnostic;
    }
  }

  /**
   * Accessor for options to be used during parsing and rewriting.
   * <p>
   * If options are <code>null</code>, uses options provided by
   * <code>getDefaultJavaCoreOptions()</code>.
   * 
   * @return map of options
   * @see #getDefaultJavaCoreOptions()
   */
  public Map<?, ?> getJavaCoreOptions()
  {
    if (javaCoreOptions == null)
    {
      javaCoreOptions = getDefaultJavaCoreOptions();
    }
    return javaCoreOptions;
  }

  /**
   * Gets JavaCore options from JavaCore and updates tab and indentation settings from ControlModel.
   * 
   * @return map of options
   * 
   * @see #getJavaCoreOptions()
   * @see JavaCore#getOptions()
   * @see JControlModel#getLeadingTabReplacement()
   */
  private Map<?, ?> getDefaultJavaCoreOptions()
  {
    Map<Object, String> javaCoreOptions = new HashMap<Object, String>(JavaCore.getOptions());

    // Set of options that we want to copy from the current definition or use defaults
    //
    if (compilerCompliance != null)
    {
      javaCoreOptions.put(JavaCore.COMPILER_COMPLIANCE, compilerCompliance);
      javaCoreOptions.put(JavaCore.COMPILER_SOURCE, compilerCompliance);
      javaCoreOptions.put(JavaCore.COMPILER_CODEGEN_TARGET_PLATFORM, compilerCompliance);
    }
    else
    {
      useCurrentOption(javaCoreOptions, JavaCore.COMPILER_COMPLIANCE, "1.5");
      useCurrentOption(javaCoreOptions, JavaCore.COMPILER_SOURCE, "1.5");
      useCurrentOption(javaCoreOptions, JavaCore.COMPILER_CODEGEN_TARGET_PLATFORM, "1.5");
    }
    useCurrentOption(javaCoreOptions, DefaultCodeFormatterConstants.FORMATTER_TAB_CHAR, JavaCore.SPACE);
    useCurrentOption(javaCoreOptions, DefaultCodeFormatterConstants.FORMATTER_TAB_SIZE, "2");
    useCurrentOption(javaCoreOptions, DefaultCodeFormatterConstants.FORMATTER_INDENTATION_SIZE, "2");

    javaCoreOptions.put(JavaCore.COMPILER_DOC_COMMENT_SUPPORT, "enabled");

    if (getControlModel() != null)
    {
      String indent = getControlModel().getLeadingTabReplacement();
      if (indent != null && indent.length() > 0)
      {
        String size = Integer.toString(indent.length());
        if (indent.charAt(0) == '\t') 
        {
          javaCoreOptions.put(DefaultCodeFormatterConstants.FORMATTER_TAB_CHAR, JavaCore.TAB);
          javaCoreOptions.put(DefaultCodeFormatterConstants.FORMATTER_TAB_SIZE, size);
          javaCoreOptions.put(DefaultCodeFormatterConstants.FORMATTER_INDENTATION_SIZE, size);                
        }
        else if (indent.charAt(0) == ' ')
        {
          javaCoreOptions.put(DefaultCodeFormatterConstants.FORMATTER_TAB_CHAR, JavaCore.SPACE);
          javaCoreOptions.put(DefaultCodeFormatterConstants.FORMATTER_TAB_SIZE, size);
          javaCoreOptions.put(DefaultCodeFormatterConstants.FORMATTER_INDENTATION_SIZE, size);                
        }
        else
        {
          if (DEBUG)
          {
            logInfo("Unable to recognize indent string, using java core options.");
          }
        }
      }
      else
      {
        if (DEBUG)
        {
          logInfo("Indent is not set, using java core options.");
        }
      }
    }

    if (DEBUG)
    {
      logInfo("Tab char: " + javaCoreOptions.get(DefaultCodeFormatterConstants.FORMATTER_TAB_CHAR) + ", Indent size: "
        + javaCoreOptions.get(DefaultCodeFormatterConstants.FORMATTER_TAB_SIZE) + ", Tab size: "
        + javaCoreOptions.get(DefaultCodeFormatterConstants.FORMATTER_INDENTATION_SIZE));
    }

    // Set of options that we want to control
    javaCoreOptions.put("org.eclipse.jdt.core.incompleteClasspath", "warning");
    javaCoreOptions.put("org.eclipse.jdt.core.circularClasspath", "warning");
    //
    javaCoreOptions.put(DefaultCodeFormatterConstants.FORMATTER_BRACE_POSITION_FOR_ENUM_DECLARATION, DefaultCodeFormatterConstants.NEXT_LINE);
    javaCoreOptions.put(DefaultCodeFormatterConstants.FORMATTER_BRACE_POSITION_FOR_TYPE_DECLARATION, DefaultCodeFormatterConstants.NEXT_LINE);
    
    // separate fields with an empty line
    javaCoreOptions.put(DefaultCodeFormatterConstants.FORMATTER_BLANK_LINES_BEFORE_FIELD, "1");
    
    // make all enum constants to be on separate lines
    javaCoreOptions.put(DefaultCodeFormatterConstants.FORMATTER_ALIGNMENT_FOR_ENUM_CONSTANTS, DefaultCodeFormatterConstants.createAlignmentValue(true, DefaultCodeFormatterConstants.WRAP_ONE_PER_LINE, DefaultCodeFormatterConstants.INDENT_DEFAULT));
    
    return javaCoreOptions;
  }
  
  
  protected void useCurrentOption(Map<Object, String> options, String option)
  {
    useCurrentOption(options, option, null); 
  }

  /**
   * @since 2.8
   * @param options
   * @param option
   * @param defaultValue
   */
  protected void useCurrentOption(Map<Object, String> options, String option, String defaultValue)
  {
    String value = JavaCore.getOption(option);
    if (value != null)
    {
      options.put(option, value);
    }
    else if (!options.containsKey(option) && defaultValue != null)
    {
      options.put(option, defaultValue);
    }
  }
  
  @Override
  public ASTNodeConverter getNodeConverter()
  {
    if (nodeConverter == null)
    {
      nodeConverter = new ASTNodeConverter(this);
    }
    return nodeConverter;
  }
  
  /* (non-Javadoc)
   * @see org.eclipse.emf.codegen.merge.java.facade.FacadeHelper#getContext(org.eclipse.emf.codegen.merge.java.facade.JNode)
   */
  @Override
  public Object getContext(JNode node)
  {
    return node;
  }

  /**
   * Creates a copy of the node to be inserted in the tree that context node belongs to.
   * <p>
   * Note that in this implementation the original and returned cloned node can not be modified.
   * Calls to <code>get...()</code> methods on the cloned node will not return the original content. 
   * The returned node can only be inserted in the same tree that context node belongs to.
   * 
   * @param context ASTJNode that belongs to the same tree that cloned node will be inserted to
   * @param node node that needs to be cloned
   * @return new node
   * 
   * @see org.eclipse.emf.codegen.merge.java.facade.FacadeHelper#cloneNode(java.lang.Object, org.eclipse.emf.codegen.merge.java.facade.JNode)
   */
  @Override
  public ASTJNode<?> cloneNode(Object context, JNode node)
  {
    if (node instanceof JCompilationUnit)
    {
      String content = applyFormatRules(toString(node.getContents()));
      return createCompilationUnit(node.getName(), content);
    }
    else
    {
      ASTJNode<?> newASTJNode = null;
      ASTNode originalASTNode = ((ASTJNode<?>)node).getASTNode();

      ASTJNode<?> contextNode = (ASTJNode<?>)context;
      ASTRewrite rewriter = contextNode.getRewriter();

      // handle field declarations separately
      if (node instanceof ASTJField)
      {
        newASTJNode = cloneField((ASTJField)node, contextNode);
      }
      else
      // create new node and replace it all by original contents
      {
        String contents = applyFormatRules(node.getContents());
        // note that string place holder adjusts indentation
        // to correct this trackAndReplace method is used below
        ASTNode newASTNode = rewriter.createStringPlaceholder(contents, originalASTNode.getNodeType());
        newASTJNode = (ASTJNode<?>)convertToNode(newASTNode);
        newASTJNode.trackAndReplace(newASTNode, contents);
      }

      // set rewriter on the new node
      newASTJNode.setRewriter(contextNode.getRewriter());

      return newASTJNode;
    }
  }

  /**
   * Copies the ASTJField node.
   * <p>
   * The copied field should <b>not</b> be modified (using set methods) nor read (using get methods), 
   * and can <b>only</b> be inserted into the same tree that context node belongs to.
   * <p>
   * If the source field has only 1 variable, returned field is replaced by contents of original field declaration. 
   * The returned field will have no internal structure.
   * <p>
   * If the source field has multiple variables declared in it, the returned field will
   * contain only 1 variable. Annotations, javadoc and initializer are replaced by original contents
   * with formatting preserved. Other parts of the declaration is not guaranteed to have formatting preserved.
   * The copied field will have source ranges for all nodes relative to the source file, hence, get methods might return
   * incorrect contents.
   * 
   * @param originalField
   * @param contextNode
   */
  protected ASTJField cloneField(ASTJField originalField, ASTJNode<?> contextNode)
  {
    ASTJField newField = null;
    FieldDeclaration originalFieldDeclaration = originalField.getOriginalFieldDeclaration();

    // if there are multiple variables in the same field declaration, create declaration with only 1 variable
    if (originalFieldDeclaration.fragments().size() > 1)
    {
      AST ast = contextNode.getWrappedObject().getAST();
      
      // note that the copied tree should not be modified by wrapped ASTJField
      //
      // the copied tree will have source ranges for all nodes in the source file,
      // hence, the get methods in the new ASTJField will not return the right contents
      FieldDeclaration newFieldDeclaration = 
        (FieldDeclaration)ASTNode.copySubtree(ast, originalFieldDeclaration);

      @SuppressWarnings("unchecked")
      List<Object> newFragmentsList = newFieldDeclaration.fragments();
      newFragmentsList.clear();
      ASTNode newASTNode = ASTNode.copySubtree(ast, originalField.getWrappedVariableDeclarationFragment());
      newFragmentsList.add(newASTNode);

      newField = (ASTJField)convertToNode(newASTNode);

      // rewriter is required for set methods
      newField.setRewriter(contextNode.getRewriter());

      // set comment and initializer as strings
      newField.setComment(originalField.getComment());
      newField.setInitializer(originalField.getInitializer());

      // set annotation contents
      Iterator<JNode> originalChildrenIterator = originalField.getChildren().iterator();
      Iterator<JNode> childrenIterator = newField.getChildren().iterator();
      for (; childrenIterator.hasNext() && originalChildrenIterator.hasNext();)
      {
        ASTJAnnotation originalAnnotation = (ASTJAnnotation)originalChildrenIterator.next();
        ASTJAnnotation annotation = (ASTJAnnotation)childrenIterator.next();
        annotation.setContents(applyFormatRules(originalAnnotation.getContents()));
      }
    }
    else
    // create new field and replace it all by original contents
    {
      String contents = applyFormatRules(originalField.getContents());
      FieldDeclaration fieldDeclaration = (FieldDeclaration)contextNode.getRewriter().createStringPlaceholder(
        contents,
        ASTNode.FIELD_DECLARATION);
      newField = (ASTJField)convertToNode(fieldDeclaration.fragments().get(0));
      newField.trackAndReplace(fieldDeclaration, contents);
    }
    return newField;
  }

  /**
   * Finds the parent node based on the parent of wrapped AST node.
   * 
   * @return parent {@link JNode}
   */
  public ASTJNode<?> findParent(ASTNode node)
  {
    JNode parent = null;
    // skip nodes in hierarchy that can not be converted to JNode (i.e. VariableDeclarationFragment)
    do
    {
      node = node.getParent();
      parent = convertToNode(node);
    }
    while (parent == null && node != null);
    return (ASTJNode<?>)parent;
  }

  /* (non-Javadoc)
   * @see org.eclipse.emf.codegen.merge.java.facade.FacadeHelper#doConvertToNode(java.lang.Object)
   */
  @Override
  protected ASTJNode<?> doConvertToNode(Object object)
  {
    ASTJNode<?> node = null;
    
    if (object instanceof ASTNode)
    {
      ASTNode astNode = (ASTNode)object;
      
      // get rewriter
      ASTRewrite rewriter = null;
      ASTJCompilationUnit compilationUnit = (ASTJCompilationUnit)getCompilationUnit(findParent((ASTNode)object));
      if (compilationUnit != null)
      {
        rewriter = compilationUnit.getRewriter();
      }
  
      boolean isCompilationUnit = false;
  
      switch (astNode.getNodeType())
      {
        case ASTNode.COMPILATION_UNIT:
          node = new ASTJCompilationUnit((CompilationUnit)object);
          isCompilationUnit = true;
          break;
        case ASTNode.VARIABLE_DECLARATION_FRAGMENT:
          node = new ASTJField((VariableDeclarationFragment)object, this, rewriter);
          break;
        case ASTNode.IMPORT_DECLARATION:
          node = new ASTJImport((ImportDeclaration)object);
          break;
        case ASTNode.INITIALIZER:
          node = new ASTJInitializer((Initializer)object);
          break;
        case ASTNode.METHOD_DECLARATION:
          node = new ASTJMethod((MethodDeclaration)object);
          break;
        case ASTNode.PACKAGE_DECLARATION:
          node = new ASTJPackage((PackageDeclaration)object);
          break;
        case ASTNode.TYPE_DECLARATION:
          node = new ASTJType((TypeDeclaration)object);
          break;
        case ASTNode.MARKER_ANNOTATION:
        case ASTNode.NORMAL_ANNOTATION:
        case ASTNode.SINGLE_MEMBER_ANNOTATION:
          node = new ASTJAnnotation((Annotation)object);
          break;
        case ASTNode.ANNOTATION_TYPE_DECLARATION:
          node = new ASTJAnnotationType((AnnotationTypeDeclaration)object);
          break;
        case ASTNode.ANNOTATION_TYPE_MEMBER_DECLARATION:
          node = new ASTJAnnotationTypeMember((AnnotationTypeMemberDeclaration)object);
          break;
        case ASTNode.ENUM_DECLARATION:
          node = new ASTJEnum((EnumDeclaration)object);
          break;
        case ASTNode.ENUM_CONSTANT_DECLARATION:
          node = new ASTJEnumConstant((EnumConstantDeclaration)object);
      }
      
      if (node != null)
      {
        node.setFacadeHelper(this);
        if (!isCompilationUnit)
        {
          node.setRewriter(rewriter);
        }
      }
    }
    return node;
  }
  
  /**
   * Updates wrapped objects to nodes map. This method must be called
   * when wrapped ASTNode of ASTJNode is changed.
   * 
   * @param node
   */
  public void updateObjectToNodeMap(ASTJNode<?> node)
  {
    objectToNodeMap.put(
      node instanceof ASTJField ? 
        ((ASTJField)node).getWrappedVariableDeclarationFragment() :
        node.getWrappedObject(),
      node);
  }

  /* (non-Javadoc)
   * @see org.eclipse.emf.codegen.merge.java.facade.FacadeHelper#addChild(org.eclipse.emf.codegen.merge.java.facade.JNode, org.eclipse.emf.codegen.merge.java.facade.JNode)
   */
  @Override
  public boolean addChild(JNode node, JNode child)
  {
    if (node == null)
    {
      return false;
    }
    return ((ASTJNode<?>)node).addChild((ASTJNode<?>)child);
  }

  /**
   * Removes the given node.
   * <p>
   * Most <code>get...()</code> operations on the removed node will not give the correct results
   * even after insertion of the node.
   * 
   * @see org.eclipse.emf.codegen.merge.java.facade.FacadeHelper#remove(org.eclipse.emf.codegen.merge.java.facade.JNode)
   */
  @Override
  public boolean remove(JNode node)
  {
    if (node != null)
    {
      ASTJNode<?> astjNode = (ASTJNode<?>)node;
      ASTJNode<?> parent = astjNode.getParent();
      if (parent != null)
      {
        // update the wrapped object map
        if (parent.remove(astjNode))
        {
          updateObjectToNodeMap(astjNode);
        }
        return true;
      }
    }
    return false;
  }
  
  @Override
  public void commentOut(JNode node)
  {
    ASTJNode<?> astjNode = (ASTJNode<?>)node;
    astjNode.commentOut();
  }

  /* (non-Javadoc)
   * @see org.eclipse.emf.codegen.merge.java.facade.FacadeHelper#insertSibling(org.eclipse.emf.codegen.merge.java.facade.JNode, org.eclipse.emf.codegen.merge.java.facade.JNode, boolean)
   */
  @Override
  public boolean insertSibling(JNode node, JNode newSibling, boolean before)
  {
    if (node != null && newSibling != null)
    {
      ASTJNode<?> astjNode = (ASTJNode<?>)node;
      ASTJNode<?> parent = astjNode.getParent();
      if (parent != null)
      {
        return parent.insertSibling(astjNode, (ASTJNode<?>)newSibling, before);
      }
    }
    return false;
  }  
  
  /* (non-Javadoc)
   * @see org.eclipse.emf.codegen.merge.java.facade.FacadeHelper#toString(java.lang.Object)
   */
  @Override
  public String toString(Object object)
  {
    if (object instanceof ASTJNode<?>)
    {
      return toString(((ASTJNode<?>)object).getASTNode());
    }
    if (object instanceof ASTNode)
    {
      return toString((ASTNode)object);
    }
    else
    {
      return super.toString(object);
    }
  }
  
  /**
   * Gets the original contents of the node.
   * <p> 
   * The contents of the node in AST implementation includes only the node itself without
   * the leading or trailing whitespace.
   * If the node has a Javadoc comment, it is included in the contents. No other leading or trailing
   * comments are included in the node contents.
   * <p>
   * The given node must be one of the nodes of the AST tree created by {@link #createCompilationUnit(String, String)}.
   * <p>
   * Note that contents is cached on the first access. Therefore, returned contents will not reflect any changes to the nodes.
   * 
   * @param node node to get contents for
   * @return original contents of the node
   */
  public String toString(ASTNode node)
  {
    if (node == null)
    {
      return null;
    }
    
    if (nodeContents.containsKey(node))
    {
      return nodeContents.get(node);
    }
    
    JNode root = convertToNode(node.getRoot());
    if (root == null || !(root instanceof ASTJCompilationUnit))
    {
      root = getCompilationUnit(convertToNode(node));
    }

    ASTJCompilationUnit compilationUnit = (ASTJCompilationUnit)root;
    if (compilationUnit != null)
    {
      char[] originalContents = compilationUnit.getOriginalContents();
      //String resultString = new String(originalContents, compilationUnit.getASTCompilationUnit().getExtendedStartPosition(node), compilationUnit.getASTCompilationUnit().getExtendedLength(node));

      int start = node.getStartPosition();
      int length = node.getLength();
      TargetSourceRangeComputer rangeComputer = compilationUnit.getRewriter().getExtendedSourceRangeComputer();
      if (rangeComputer instanceof CommentAwareSourceRangeComputer)
      {
        SourceRange sourceRange = ((CommentAwareSourceRangeComputer)rangeComputer).computeDefaultSourceRange(node);
        length = (sourceRange.getStartPosition() + sourceRange.getLength()) - start;
      }
      String resultString = null;
      
      if (start >= 0 && start + length <= originalContents.length)
      {
        resultString = new String(originalContents, start, length);
      }
      
      nodeContents.put(node, resultString);
      return resultString;      
    }

    // this is a fall-back, however, this should never be called
    return node.toString();
  }
  
  /**
   * Adds a new entry to the log using INFO level.
   * 
   * @param string to add to the log
   */
  public void logInfo(String string)
  {
    CodeGenPlugin.INSTANCE.log("INFO " + string);
  }    
  
  /**
   * Adds a new entry to the log using ERROR level.
   * 
   * @param string to add to the log
   */
  public void logError(String string)
  {
    CodeGenPlugin.INSTANCE.log("ERROR " + string);
  }  
  
  /**
   * Adds a new entry to the log using ERROR level.
   * 
   * @param string to add to the log
   * @param e exception to record
   */
  public void logError(String string, Exception e)
  {
    CodeGenPlugin.INSTANCE.log("ERROR " + string);
    CodeGenPlugin.INSTANCE.log(e);
  }
}
