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

import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.ClassInstanceCreation;
import org.eclipse.jdt.core.dom.EnumConstantDeclaration;
import org.eclipse.jdt.core.dom.EnumDeclaration;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.TypeDeclaration;
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;

import org.eclipse.emf.codegen.merge.java.facade.FacadeFlags;
import org.eclipse.emf.codegen.merge.java.facade.JAbstractType;
import org.eclipse.emf.codegen.merge.java.facade.JNode;
import org.eclipse.emf.codegen.merge.java.facade.NodeConverter;


public class ASTNodeConverter implements NodeConverter
{
  protected ASTFacadeHelper facadeHelper;

  /**
   * @param facadeHelper must be not <code>null</code>
   */
  public ASTNodeConverter(ASTFacadeHelper facadeHelper)
  {
    assert facadeHelper != null;

    this.facadeHelper = facadeHelper;
  }

  public ASTFacadeHelper getFacadeHelper()
  {
    return facadeHelper;
  }
  
  public JAbstractType convert(JAbstractType abstractType, Class<? extends JAbstractType> cls)
  {
    try
    {
      Converter converter = null; 
      
      // if the given class is subclass of enum and node is of subclass of type
      if (ASTJEnum.class.isAssignableFrom(cls) && abstractType instanceof ASTJType)
      {
        converter = new TypeToEnumConverter((ASTJType)abstractType);
      }
      else if (ASTJType.class.isAssignableFrom(cls) && abstractType instanceof ASTJEnum)
      {
        converter = new EnumToTypeConverter((ASTJEnum)abstractType);
      }
      
      return converter != null ? (JAbstractType)converter.convert() : null;
    }
    catch (Exception e)
    {
      if (ASTFacadeHelper.DEBUG)
      {
        getFacadeHelper().logError("Error converting " + abstractType.getClass().getSimpleName() + " to " + cls.getSimpleName(), e);
      }
    }
    return null;
  }  

  /**
   * Base class for all converters.
   * 
   * @see #convert()
   */
  protected abstract class Converter
  {
    /**
     * @return converted node
     */
    public abstract JNode convert();

    /**
     * Removes children from the source node and adds them to the target node.
     * 
     * @param source
     * @param target
     */
    protected void moveChildren(JNode source, JNode target)
    {
      for (JNode child : source.getChildren())
      {
        getFacadeHelper().remove(child);
        getFacadeHelper().addChild(target, child);
      }
    }
    
    /**
     * Replaces existing node by new node.
     * 
     * @param existingNode
     * @param newNode
     */
    protected void replaceNode(JNode existingNode, JNode newNode)
    {
      getFacadeHelper().insertSibling(existingNode, newNode, false);
      getFacadeHelper().remove(existingNode);      
    }
  }

  /**
   * Converter of type to enum.
   *
   * @see #convert()
   */  
  protected class TypeToEnumConverter extends Converter
  {
    /**
     * Type that will be converted
     */
    protected ASTJType type;
    
    /**
     * AST of the source and converted node
     */
    protected AST ast;
    
    /**
     * List of children of the type
     */
    protected List<JNode> typeChildren;
    
    /**
     * Map of field names to fields
     */
    protected Map<String, ASTJField> fieldNamesMap = new HashMap<String, ASTJField>();

    /**
     * @param type to convert to enum
     */
    public TypeToEnumConverter(ASTJType type)
    {
      this.type = type;
      
      ast = type.getASTNode().getAST();
      
      typeChildren = type.getChildren();

      // create map of field names to fields
      for (JNode child : typeChildren)
      {
        if (child instanceof ASTJField)
        {
          fieldNamesMap.put(child.getName(), (ASTJField)child);
        }
      }
    }

    /**
     * Converts type to the enum.
     * <p>
     * Name, flags, comment, super interfaces of the enum are set from the type.
     * <p>
     * All children of the type is added to the enum.
     * <p>
     * Fields that are <code>public static final </code>and have the same type are converted to
     * enum constants. Arguments of the enum constants are set from arguments of the initializer.
     * Arguments that match the name of any other field are replaced by initializer value of that field.
     * <p>
     * Conversion must be done only once for the same type.
     * 
     * @return converted type, or <code>null</code> if conversion not possible
     */
    @Override
    public ASTJEnum convert()
    {
      EnumDeclaration enumDeclaration = ast.newEnumDeclaration();

      ASTJEnum astjEnum = (ASTJEnum)getFacadeHelper().convertToNode(enumDeclaration);
      astjEnum.setRewriter(type.getRewriter());

      astjEnum.setName(type.getName());      
      astjEnum.setFlags(type.getFlags());
      astjEnum.setComment(type.getComment());

      // move all children to the enum, converting some fields to constants
      for (JNode child : typeChildren)
      {
        ASTJNode<?> nodeToRemove = (ASTJNode<?>)child;
        ASTJNode<?> nodeToInsert = nodeToRemove;

        if (nodeToRemove instanceof ASTJField)
        {
          ASTJField field = (ASTJField)nodeToRemove;
          nodeToInsert = convertFieldToEnumConst(field);
        }

        type.remove(nodeToRemove);
        astjEnum.addChild(nodeToInsert);
      }

      // remove type, insert enum
      replaceNode(type, astjEnum);

      return astjEnum;
    }

    /**
     * Converts given field to enum constant if possible.
     * <p>
     * Field must be declared as <code>public static final</code> field, and field must be of the same type
     * as its parent (e.g. type).
     * <p>
     * Enum constant arguments and body is set from the original initializer of the field.
     * 
     * @param field to convert
     * @return original field if conversion not possible, or converted field otherwise
     * 
     * @see #setEnumConstantArgumentsAndBody(ASTJEnumConstant, ASTJField)
     */
    protected ASTJNode<?> convertFieldToEnumConst(ASTJField field)
    {
      // convert only public static final fields
      int flags = field.getFlags();
      if ((flags & (FacadeFlags.PUBLIC | FacadeFlags.STATIC | FacadeFlags.FINAL)) == 0)
      {
        return field;
      }

      // convert only fields of the same type as parent
      ASTJNode<?> parent = field.getParent();
      String parentType = (parent == null ? null : parent.getName());
      if (parentType == null || !parentType.equals(field.getType()))
      {
        return field;
      }

      EnumConstantDeclaration enumConstantDeclaration = ast.newEnumConstantDeclaration();

      ASTJEnumConstant enumConstant = (ASTJEnumConstant)getFacadeHelper().convertToNode(enumConstantDeclaration);
      enumConstant.setRewriter(field.getRewriter());

      enumConstant.setName(field.getName());
      enumConstant.setComment(field.getComment());

      // set arguments and body
      setEnumConstantArgumentsAndBody(enumConstant, field);

      // move annotations
      moveChildren(field, enumConstant);

      return enumConstant;
    }

    /**
     * Sets arguments and body of the enum constant from the initializer of the given field.
     * <p>
     * Note that the original field's initializer node structure is used (e.g. if {@link ASTJField#setInitializer(String)}
     * has been called, this method will use the original initializer of the field).
     * 
     * @param enumConstant
     * @param field
     */
    protected void setEnumConstantArgumentsAndBody(ASTJEnumConstant enumConstant, ASTJField field)
    {
      Expression initializer = field.getVariableDeclarationFragment().getInitializer();
      if (initializer == null || initializer.getNodeType() != ASTNode.CLASS_INSTANCE_CREATION)
      {
        // unable to set arguments or body
        return;
      }

      ClassInstanceCreation classInstanceCreation = (ClassInstanceCreation)initializer;

      // replace arguments that match the name of the fields with the initializer value of the field
      //
      @SuppressWarnings("unchecked")
      String[] arguments = field.convertASTNodeListToStringArray(classInstanceCreation.arguments());

      for (int i = 0; i < arguments.length; i++)
      {
        ASTJField existingField = fieldNamesMap.get(arguments[i]);
        if (existingField != null)
        {
          String fieldInitializer = existingField.getInitializer();
          if (fieldInitializer != null && fieldInitializer.length() > 0)
          {
            arguments[i] = fieldInitializer;
          }
        }
      }

      enumConstant.setArguments(arguments);
      enumConstant.setBody(getFacadeHelper().toString(classInstanceCreation.getAnonymousClassDeclaration()));
    }
  }

  /**
   * Converter of enum to class.
   *
   * @see #convert()
   */
  protected class EnumToTypeConverter extends Converter
  {
    /**
     * Enum to convert to type
     */
    protected ASTJEnum astjEnum;
    
    /**
     * AST of the source and converted node
     */
    protected AST ast;    
    
    /**
     * List of children of the enum
     */
    protected List<JNode> enumChildren;

    /**
     * Map of initializer values of the fields to fields
     */
    protected Map<String, ASTJField> fieldInitializersMap = new HashMap<String, ASTJField>();

    /**
     * Map of fields to their index in the children list
     */
    protected Map<ASTJField, Integer> fieldIndexesMap = new HashMap<ASTJField, Integer>();
    
    /**
     * Last field used in the initializer string of the fields converted from enum constants. 
     */
    protected ASTJNode<?> lastFinalFieldUsed = null;

    /**
     * @param astjEnum to convert
     */
    public EnumToTypeConverter(ASTJEnum astjEnum)
    {
      this.astjEnum = astjEnum;

      ast = astjEnum.getASTNode().getAST();
      
      enumChildren = astjEnum.getChildren();

      // create map of public static final field initializers to fields
      int i = 0;
      for (JNode child : enumChildren)
      {
        if (child instanceof ASTJField)
        {
          ASTJField field = (ASTJField)child;
          if ((field.getFlags() & (FacadeFlags.PUBLIC | FacadeFlags.STATIC | FacadeFlags.FINAL)) != 0)
          {
            fieldInitializersMap.put(field.getInitializer(), field);
            fieldIndexesMap.put(field, i++);
          }
        }
      }
    }

    /**
     * Converts enum to the class declaration.
     * <p>
     * Name, flags, comment, super interfaces of the class declaration are set from the enum.
     * <p>
     * All children of the enum are added to the class declaration. Enum constants are converted
     * to public static final fields with initializer value created from arguments and body of enum constant.
     * 
     * @return converted type, or <code>null</code> if conversion not possible
     * 
     * @see #setFieldInitializer(ASTJField, ASTJEnumConstant) 
     */
    @Override
    public ASTJType convert()
    {
      TypeDeclaration typeDeclaration = ast.newTypeDeclaration();

      ASTJType type = (ASTJType)getFacadeHelper().convertToNode(typeDeclaration);
      type.setRewriter(astjEnum.getRewriter());

      type.setName(astjEnum.getName());
      type.setFlags(astjEnum.getFlags());
      type.setComment(astjEnum.getComment());

      // move all children to the type, converting enum constants to fields
      //
      // fields that are created by conversion from enum constants must be inserted after
      // the last final field that is used in constructors for initializers
      List<ASTJNode<?>> convertedEnumConstants = new ArrayList<ASTJNode<?>>(enumChildren.size());

      for (JNode child : enumChildren)
      {
        ASTJNode<?> originalNode = (ASTJNode<?>)child;

        // convert enum constants and add to list of converted constants
        if (originalNode instanceof ASTJEnumConstant)
        {
          ASTJEnumConstant constant = (ASTJEnumConstant)originalNode;
          convertedEnumConstants.add(convertEnumConstToField(constant));
        }
        else
        {
          // move the node
          astjEnum.remove(originalNode);
          type.addChild(originalNode);
        }
      }

      // add all converted fields after the last final field 
      // that is used in initializers of the converted fields
      for (ASTJNode<?> nodeToInsert : convertedEnumConstants)
      {
        if (lastFinalFieldUsed == null)
        {
          type.addChild(nodeToInsert);
        }
        else
        {
          // insert nodeToInsert after targetNode
          type.insertSibling(lastFinalFieldUsed, nodeToInsert, false);
          lastFinalFieldUsed = nodeToInsert;
        }
      }

      // replace type by enum
      replaceNode(astjEnum, type);

      return type;
    }

    /**
     * Converts enum constant to field.
     * <p>
     * Resulting field is <code>public static final</code>. The type of the field is the name of the
     * enum. Comment of the field is copied from the enum constant.
     * Initializer value is a call to the constructor of the enum (or converted type) with arguments
     * taken from enum constant.
     * 
     * @see #setFieldInitializer(ASTJField, ASTJEnumConstant)
     * 
     * @param enumConstant to convert
     * @return converted field
     */
    protected ASTJNode<?> convertEnumConstToField(ASTJEnumConstant enumConstant)
    {
      // create field declaration with 1 variable declaration fragment
      VariableDeclarationFragment variableDeclarationFragment = ast.newVariableDeclarationFragment();
      ast.newFieldDeclaration(variableDeclarationFragment);

      ASTJField field = (ASTJField)getFacadeHelper().convertToNode(variableDeclarationFragment);
      field.setRewriter(enumConstant.getRewriter());

      field.setName(enumConstant.getName());
      field.setComment(enumConstant.getComment());
      field.setType(enumConstant.getParent().getName());
      field.setFlags(FacadeFlags.PUBLIC | FacadeFlags.STATIC | FacadeFlags.FINAL);

      // create field initializer from enum constant's arguments and body
      setFieldInitializer(field, enumConstant);

      // move annotations
      moveChildren(enumConstant, field);

      return field;
    }

    /**
     * Sets field initializer based on arguments and body of enum constant.
     * <p>
     * Initializer string is in the form <pre><code> <b>new</b> Type ( ArgumentsList ) AnonymousClassDeclaration</code></pre> where
     * <ul>
     * <li><code>Type</code> is the the type of the class
     * <li><code>ArgumentsList</code> is the arguments of the enum constant with some arguments replaced by the names of static final fields.
     * Arguments that match the initializer value of any public static final field are replaced by the name of that field.
     * <li><code>AnonymousClassDeclaration is the body of enum constant
     * </ul>
     * <p>
     * 
     * @param field
     * @param enumConstant
     */
    protected void setFieldInitializer(ASTJField field, ASTJEnumConstant enumConstant)
    {
      String[] arguments = enumConstant.getArguments();
      String body = enumConstant.getBody();

      ClassInstanceCreation classInstanceCreation = ast.newClassInstanceCreation();

      // set the type to create
      field.setNodeProperty(
        classInstanceCreation,
        enumConstant.getParent().getName(),
        ClassInstanceCreation.TYPE_PROPERTY,
        ASTNode.SIMPLE_TYPE);

      // set arguments
      if (arguments != null)
      {
        // replace some arguments by another field names (constants)
        for (int i = 0; i < arguments.length; i++)
        {
          if (arguments[i] != null && arguments[i].length() > 0)
          {
            ASTJField constantField = fieldInitializersMap.get(arguments[i]);
            if (constantField != null)
            {
              arguments[i] = constantField.getName();
              
              // update last field that is used
              if (lastFinalFieldUsed != null)
              {
                int constantFieldUsedIndex = fieldIndexesMap.get(constantField);
                int lastFinalFieldUsedIndex = fieldIndexesMap.get(lastFinalFieldUsed);
                if (constantFieldUsedIndex > lastFinalFieldUsedIndex)
                {
                  lastFinalFieldUsed = constantField;
                }
              }
              else
              {
                lastFinalFieldUsed = constantField;
              }
            }
          }
        }

        // set the arguments to the constructor
        field.setListNodeProperty(classInstanceCreation, arguments, ClassInstanceCreation.ARGUMENTS_PROPERTY, ASTNode.SIMPLE_NAME);
      }

      // set the body
      if (body != null && body.length() > 0)
      {
        field.setTrackedNodeProperty(
          classInstanceCreation,
          body,
          ClassInstanceCreation.ANONYMOUS_CLASS_DECLARATION_PROPERTY,
          ASTNode.ANONYMOUS_CLASS_DECLARATION);
      }

      // set the whole initializer
      field.setNodeProperty(field.getVariableDeclarationFragment(), classInstanceCreation, VariableDeclarationFragment.INITIALIZER_PROPERTY);
    }
  }
}
