/**
 * Copyright (c) 2006-2008 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;


import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.eclipse.emf.codegen.merge.java.facade.FacadeHelper;
import org.eclipse.emf.codegen.merge.java.facade.FacadeVisitor;
import org.eclipse.emf.codegen.merge.java.facade.JAbstractType;
import org.eclipse.emf.codegen.merge.java.facade.JAnnotation;
import org.eclipse.emf.codegen.merge.java.facade.JCompilationUnit;
import org.eclipse.emf.codegen.merge.java.facade.JImport;
import org.eclipse.emf.codegen.merge.java.facade.JMember;
import org.eclipse.emf.codegen.merge.java.facade.JMethod;
import org.eclipse.emf.codegen.merge.java.facade.JNode;
import org.eclipse.emf.codegen.merge.java.facade.JPackage;
import org.eclipse.emf.codegen.merge.java.facade.NodeConverter;
import org.eclipse.emf.codegen.merge.java.facade.ast.ASTFacadeHelper;
import org.eclipse.emf.codegen.util.CodeGenUtil;
import org.eclipse.emf.common.util.BasicMonitor;
import org.eclipse.emf.common.util.Monitor;


/**
 * This implements the method {@link #run}, 
 * which is called just like main during headless workbench invocation.
 */
public class JMerger
{
  public static final boolean DEBUG = false;
  
  public static final String DEFAULT_FACADE_HELPER_CLASS = ASTFacadeHelper.class.getName();  
  protected static final Object[] NO_ARGUMENTS = new Object[0];

  protected static Pattern interfaceBracePattern = null;
  
  public class PullTargetVisitor extends FacadeVisitor
  {
    protected void doPull(JNode source, JNode target)
    {
      map(source, target);
      if (source != null)
      {
        applyPullRules(source, target);
      }
    }
    
    @Override
    protected void visit(JNode target)
    {
      // retrieve source node corresponding to target
      //
      if (target instanceof JAbstractType)
      {
        JAbstractType targetAbstractType = (JAbstractType)target;
        String comment = targetAbstractType.getComment();
        
        isBlocked =
          targetAbstractType.getParent() instanceof JCompilationUnit &&
          getControlModel().getBlockPattern() != null && 
          comment != null &&
          getControlModel().getBlockPattern().matcher(comment).find();    
        
        String nodeIdentifier = targetPatternDictionary.getNodeIdentifier(targetAbstractType);
        JAbstractType sourceAbstractType = sourcePatternDictionary.getAbstractTypeMap().get(nodeIdentifier);
        
        // convert the target node to a compatible node
        //
        if (sourceAbstractType != null && !areCompatible(sourceAbstractType, targetAbstractType))
        {
          JNode newTarget = convertTarget(targetAbstractType, sourceAbstractType.getClass());
          if (newTarget != null)
          {
            // use new node from now on
            target = newTarget;
            
            // redo the markup since nodes changed now
            targetPatternDictionary.start(target);
          }
          else if (!isBlocked)
          {
            map(sourceAbstractType, target);
          }
        }
      }
      
      if (!isBlocked)
      {
        // super is called on converted node
        super.visit(target);
      }
    }
    
    @Override
    protected boolean basicVisit(JNode node)
    {
      String nodeIdentifier = targetPatternDictionary.getNodeIdentifier(node);
      JNode sourceNode = sourcePatternDictionary.getNodeMap(node).get(nodeIdentifier);
      if (noAbstractTypeConversion && sourceNode == null)
      {
        sourceNode = sourcePatternDictionary.getNode(nodeIdentifier);
      }      
      
      doPull(sourceNode, node);
      return true;
    }
    
    @Override
    protected boolean visit(JCompilationUnit compilationUnit)
    {
      return true;
    }
    
    @Override
    protected boolean visit(JMethod method)
    {
      String nodeIdentifier = targetPatternDictionary.getNodeIdentifier(method);
      JNode sourceNode = sourcePatternDictionary.getMethodMap().get(nodeIdentifier);

      if (sourceNode == null && 
            getControlModel().getRedirect() != null && 
            method.getName() != null &&
            method.getName().endsWith(getControlModel().getRedirect()))
      {
        String qualifiedTargetMethodName = method.getQualifiedName();
        int index = qualifiedTargetMethodName.indexOf("("); //)
        qualifiedTargetMethodName =
          qualifiedTargetMethodName.substring(0, index - getControlModel().getRedirect().length()) + 
            qualifiedTargetMethodName.substring(index);
        sourceNode = sourcePatternDictionary.getMethodMap().get(qualifiedTargetMethodName);
      }
      
      if (noAbstractTypeConversion && sourceNode == null)
      {
        sourceNode = sourcePatternDictionary.getNode(nodeIdentifier);
      }            
      doPull(sourceNode, method);
      return true;
    }
    
    @Override
    protected boolean visit(JPackage jPackage)
    {
      JPackage sourcePackage = sourcePatternDictionary.getJPackage();
      doPull(sourcePackage, jPackage);
      return false;
    }
  } 
  
  public class PushSourceVisitor extends FacadeVisitor
  {
    /**
     * Returns whether the children should be visited (i.e., when the source is not in the target).
     */
    protected boolean doPush(JNode sourceNode)
    {
      applySortRules(sourceNode);
      if (!sourceToTargetMap.containsKey(sourceNode))
      {
        if (isPushMarkedUp(sourceNode))
        {         
          insertClone(sourceNode);
        }
        return false;
      }
      return true;
    }
    
    @Override
    protected boolean basicVisit(JNode node)
    {
      return doPush(node);
    }
    
    @Override
    protected boolean visit(JCompilationUnit compilationUnit)
    {
      return true;
    }
    
    @Override
    protected boolean visit(JAbstractType abstractType)
    {
      return 
        super.visit(abstractType) &&
        areCompatible(abstractType, sourceToTargetMap.get(abstractType));
    }
    
    @Override
    protected boolean visit(JImport jImport)
    {
      return 
       !targetPatternDictionary.isNoImport(jImport) 
       && super.visit(jImport);
    }
  }  
  
  protected JControlModel controlModel;
  
  protected JCompilationUnit sourceCompilationUnit;
  protected JCompilationUnit targetCompilationUnit;
  protected JPatternDictionary sourcePatternDictionary;
  protected JPatternDictionary targetPatternDictionary;

  protected Map<JNode, JNode> sourceToTargetMap = new LinkedHashMap<JNode, JNode>();
  protected Map<JNode, JNode> targetToSourceMap = new LinkedHashMap<JNode, JNode>();
  protected Map<JNode, List<JNode>> orderedSourceChildrenMap = new HashMap<JNode, List<JNode>>();

  protected boolean fixInterfaceBrace;
  protected boolean isBlocked = false;
  protected boolean targetCompilationUnitExists;
  protected boolean targetCompilationChanged = false;
  
  protected boolean noAbstractTypeConversion = true;
  
  private boolean clean = "true".equals(System.getProperty("org.eclipse.emf.codegen.merge.java.JMerger.clean"));

  /**
   * This creates an empty instances, an when used as a runnable.
   */
  public JMerger()
  {
    super();
  }

  public JMerger(JControlModel controlModel)
  {
    this();
    this.controlModel = controlModel;
    setFixInterfaceBrace(getControlModel().getFacadeHelper().fixInterfaceBrace());
  }
  
  /**
   * Resets this JMerger.  After calling this method, it is necessary to 
   * set the source and target compilation unit to reuse this instance.
   */
  public void reset()
  {
    if (sourcePatternDictionary != null)
    {
      sourcePatternDictionary.reset();
      sourcePatternDictionary = null;
    }
    if (targetPatternDictionary != null)
    {
      targetPatternDictionary.reset();
      targetPatternDictionary = null;
    }
    sourceCompilationUnit = null;
    targetCompilationUnit = null;
    
    sourceToTargetMap.clear();
    targetToSourceMap.clear();
    orderedSourceChildrenMap.clear();
    
    isBlocked = false;
    targetCompilationChanged = false;
  }

  public void merge()
  {
    targetCompilationChanged = false;
    targetCompilationUnitExists = targetCompilationUnit != null;
    
    if (targetCompilationUnitExists && clean)
    {
      sourceCompilationUnit.setHeader(targetCompilationUnit.getHeader());
      targetCompilationUnit = sourceCompilationUnit;
      targetCompilationChanged = true;
    }
    else
    {
      pullTargetCompilationUnit();
      if (!isBlocked && targetCompilationUnitExists)
      {
        pushSourceCompilationUnit();
        sweepTargetCompilationUnit();
        sortTargetCompilationUnit();
      }
    }
  }

  public void remerge()
  {
    sourceToTargetMap.clear();
    targetToSourceMap.clear();
    orderedSourceChildrenMap.clear();
    isBlocked = false;
    merge();
  }

  public JCompilationUnit createCompilationUnitForInputStream(InputStream inputStream)
  {
    return createCompilationUnitForInputStream(inputStream, null);
  }

  public JCompilationUnit createCompilationUnitForInputStream(InputStream inputStream, String encoding)
  {
    try
    {
      BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream);
      byte [] input = new byte [bufferedInputStream.available()];
      bufferedInputStream.read(input);
      bufferedInputStream.close();
      return getControlModel().getFacadeHelper().createCompilationUnit("NAME", encoding == null ? new String(input) : new String(input, encoding));
    }
    catch (IOException exception)
    {
      // exception.printStackTrace();
    }
    return null;
  }

  /**
   * Creates a JCompilationUnit from a URI.
   */
  public JCompilationUnit createCompilationUnitForURI(String uri)
  {
    return createCompilationUnitForURI(uri, null);
  }

  /**
   * Creates a JCompilationUnit from a URI.
   */
  public JCompilationUnit createCompilationUnitForURI(String uri, String encoding)
  {
    try
    {
      URL url = null;
      try
      {
        url = new URL(uri);
      }
      catch (MalformedURLException exception)
      {
        url = new URL("file:" + uri);
      }
  
      BufferedInputStream bufferedInputStream = new BufferedInputStream(url.openStream());
      byte [] input = new byte [bufferedInputStream.available()];
      bufferedInputStream.read(input);
      bufferedInputStream.close();
      
      return getControlModel().getFacadeHelper().createCompilationUnit(
        url.toString(), 
        encoding == null ? new String(input) : new String(input, encoding));
    }
    catch (IOException exception)
    {
      // exception.printStackTrace();
    }
  
    return null;
  }

  public JCompilationUnit createCompilationUnitForContents(String contents)
  {
    return getControlModel().getFacadeHelper().createCompilationUnit("NAME", contents);
  }

  public JControlModel getControlModel()
  {
    return controlModel;
  }

  public JCompilationUnit getSourceCompilationUnit()
  {
    return sourceCompilationUnit;
  }

  /**
   * @since 2.15
   */
  public void setSourceCompilationUnit(String sourceCompilationUnitContents)
  {
    setSourceCompilationUnit(createCompilationUnitForContents(sourceCompilationUnitContents));
  }

  public void setSourceCompilationUnit(JCompilationUnit sourceCompilationUnit)
  {
    this.sourceCompilationUnit =  sourceCompilationUnit;
    sourcePatternDictionary = new JPatternDictionary(sourceCompilationUnit, getControlModel());
  }

  public String getSourceCompilationUnitContents()
  {
    return sourceCompilationUnit.getContents();
  }

  public JCompilationUnit getTargetCompilationUnit()
  {
    return targetCompilationUnit;
  }

  public void setTargetCompilationUnit(JCompilationUnit targetCompilationUnit)
  {
    this.targetCompilationUnit = targetCompilationUnit;
    targetPatternDictionary = new JPatternDictionary(targetCompilationUnit, getControlModel());
  }

  public String getTargetCompilationUnitContents()
  {
    String result = null;
    if (getControlModel().getFacadeHelper() != null && (!targetCompilationUnitExists || !targetCompilationChanged))
    {
      result = getControlModel().getFacadeHelper().getOriginalContents(targetCompilationUnit);
    }
    if (result == null)
    {
      result = targetCompilationUnit.getContents();
      if (clean)
      {
        result = CodeGenUtil.convertFormat(controlModel.getLeadingTabReplacement(), controlModel.convertToStandardBraceStyle(), result);
      }
    }
    
    if (fixInterfaceBrace)
    {
      if (interfaceBracePattern == null)
      {
        interfaceBracePattern = Pattern.compile
          ("(?:\\n\\r|\\r\\n|\\n|\\r)(\\s*)(?:public|private|protected|static|\\s)*(?:interface|class)\\s*[^\\{\\n\\r]*(\\{)(\\n\\r|\\r\\n|\\n|\\r)", 
          Pattern.MULTILINE);
      }
      Matcher matcher = interfaceBracePattern.matcher(result);
      int offset = 0;
      while (matcher.find())
      {
        if (getControlModel().standardBraceStyle)
        {
          if (result.charAt(matcher.start(2) - 1) != ' ')
          {
            result = 
              result.substring(0, offset + matcher.start(2)) + 
                " {" + result.substring(offset + matcher.end(2), result.length());
            offset += 1;
          }
        }
        else
        {
          result = 
            result.substring(0, offset + matcher.start(2)) + matcher.group(3) + 
              matcher.group(1) + "{" + result.substring(offset + matcher.end(2), result.length());
          offset += matcher.group(1).length() + matcher.group(3).length();
        }
      }
    }
    return result;
  }

  public JPatternDictionary getSourcePatternDictionary()
  {
    return sourcePatternDictionary;
  }

  public void setSourcePatternDictionary(JPatternDictionary sourcePatternDictionary)
  {
    this.sourcePatternDictionary = sourcePatternDictionary;
  }

  public JPatternDictionary getTargetPatternDictionary()
  {
    return targetPatternDictionary;
  }

  public void setTargetPatternDictionary(JPatternDictionary targetPatternDictionary)
  {
    this.targetPatternDictionary = targetPatternDictionary;
  }

  public boolean isFixInterfaceBrace()
  {
    return fixInterfaceBrace;
  }

  public void setFixInterfaceBrace(boolean fixInterfaceBrace)
  {
    this.fixInterfaceBrace = fixInterfaceBrace;
  }

  protected void pullTargetCompilationUnit()
  {
    if (!targetCompilationUnitExists)
    {
      setTargetCompilationUnit((JCompilationUnit)insertClone(sourceCompilationUnit));
    }
    else
    {
      map(sourceCompilationUnit, targetCompilationUnit);
      applyPullRules(sourceCompilationUnit, targetCompilationUnit);

      createPullTargetVisitor().start(targetCompilationUnit);
    }
  }

  protected FacadeVisitor createPullTargetVisitor()
  {
    return new PullTargetVisitor();
  }

  protected void pushSourceCompilationUnit()
  {
    createPushSourceVisitor().start(sourceCompilationUnit);
  }

  protected FacadeVisitor createPushSourceVisitor()
  {
    return new PushSourceVisitor();
  }

  protected void sortTargetCompilationUnit()
  {
    FacadeHelper facadeHelper = getControlModel().getFacadeHelper();
    JNode parent = null;
    List<JNode> children = null;
    
    for (List<JNode> nodes : orderedSourceChildrenMap.values())
    {
      if (nodes.size() >= 2)
      {
        Iterator<JNode> i = nodes.iterator();
        JNode sourceNode = i.next();
        JNode previousTargetNode = sourceToTargetMap.get(sourceNode);
        do
        {
          sourceNode = i.next();
          JNode nextTargetNode = sourceToTargetMap.get(sourceNode);
  
          boolean reorder = true;

          JNode nextTargetNodeParent = nextTargetNode.getParent();
          if (facadeHelper.isSibilingTraversalExpensive() && parent != nextTargetNodeParent)
          {
            parent = nextTargetNodeParent;
            children = nextTargetNodeParent == null ?
              null
              : new ArrayList<JNode>(nextTargetNodeParent.getChildren());
          }

          int previousTargetNodeIndex = 0;
          int nextTargetNodeIndex = 0;
          if (children != null)
          {
            previousTargetNodeIndex = children.indexOf(previousTargetNode);
            nextTargetNodeIndex = children.indexOf(nextTargetNode);
            reorder = previousTargetNodeIndex > nextTargetNodeIndex;
          }
          else
          {
            for (JNode node = facadeHelper.getPrevious(nextTargetNode); 
                 node != null; 
                 node = facadeHelper.getPrevious(node))
            {
              if (node == previousTargetNode)
              {
                reorder = false;
                break;
              }
            }
          }
          
          if (reorder)
          {
            targetCompilationChanged = true;
            facadeHelper.remove(nextTargetNode);

            boolean appendNode = false;            
            if (children != null)
            {
              children.remove(nextTargetNode);
              appendNode = children.get(children.size()-1) == previousTargetNode; 
              if (appendNode)
              {
                children.add(nextTargetNode);
              }
              else
              {
                children.add(previousTargetNodeIndex, nextTargetNode);
              }
            }
            else
            {
              appendNode = facadeHelper.getNext(previousTargetNode) == null; 
            }
            
            if (appendNode)
            {
              facadeHelper.addChild(previousTargetNode.getParent(), nextTargetNode);
            }
            else
            {
              facadeHelper.insertSibling(previousTargetNode, nextTargetNode, false);
            }
          }
  
          previousTargetNode = nextTargetNode;
        }
        while (i.hasNext());
      }
    }
  }

  protected void sweepTargetCompilationUnit()
  {
    Set<JNode> sweptNodes = new HashSet<JNode>(targetToSourceMap.size());
    for (Map.Entry<JNode, JNode> entry : targetToSourceMap.entrySet())
    {
      if (entry.getValue() == null)
      {
        JNode node = entry.getKey();
        JNode parent = node.getParent();        
        if (parent != null && sweptNodes.contains(parent))
        {
          sweptNodes.add(node);
          continue;
        }
        
        if (applySweepRules(node))
        {
          targetCompilationChanged = true;
          sweptNodes.add(node);
        }
      }
    }
  }

  // Method created to increase the performance of regular expressions
  // by reducing the length of the string that is matched.
  //
  private int getStartIndex(String string)
  {
    int index = string.indexOf("<!--"); 
    if (index > 0)
    {
      while (Character.isWhitespace(string.charAt(--index)) && index > 0)
      {
        // Back up over the whitespace.
      }
      return index;
    }
    return 0;
  }

  protected void applyPullRules(JNode sourceNode, JNode targetNode)
  {
    try
    {
      for (JControlModel.PullRule pullRule : getControlModel().getPullRules())
      {
        if (pullRule.getSourceGetFeature().getFeatureMethod() != null &&
        	pullRule.getSourceGetFeature().getFeatureClass().isInstance(sourceNode) &&
        	pullRule.getTargetPutFeature().getFeatureMethod() != null &&
            pullRule.getTargetPutFeature().getFeatureClass().isInstance(targetNode) &&
            sourcePatternDictionary.isMarkedUp(pullRule.getSourceMarkup(), pullRule.getSourceParentMarkup(), sourceNode) && 
            targetPatternDictionary.isMarkedUp(pullRule.getTargetMarkup(), pullRule.getTargetParentMarkup(), targetNode))
        {
          // Skip if there's an equality filter and the values aren't equal.
          //
          if (pullRule.getEqualityFeature() != null)
          {
            Method equalityFeatureMethod = pullRule.getEqualityFeature().getFeatureMethod();
            Object value1 = equalityFeatureMethod.invoke(sourceNode, NO_ARGUMENTS);
            Object value2 = equalityFeatureMethod.invoke(targetNode, NO_ARGUMENTS);
            if (value1 == null ? value2 != null : !value1.equals(value2))
            {
              continue;
            }
          }
          Method sourceGetMethod = pullRule.getSourceGetFeature().getFeatureMethod();
          Object value = sourceGetMethod.invoke(sourceNode, NO_ARGUMENTS);
          Method targetPutMethod = pullRule.getTargetPutFeature().getFeatureMethod();
          if (!sourceGetMethod.getReturnType().isArray() || 
                targetPutMethod.getParameterTypes()[0].isAssignableFrom(sourceGetMethod.getReturnType()))
          {
            if (value instanceof String)
            {
              String stringValue = (String)value;
              stringValue = getControlModel().getFacadeHelper().applyFormatRules(stringValue);
              Pattern sourceTransfer = pullRule.getSourceTransfer();
              if (sourceTransfer != null)
              {
                String oldStringValue = (String)sourceGetMethod.invoke(targetNode, NO_ARGUMENTS);
                if (oldStringValue != null)
                {
                  Matcher sourceMatcher = sourceTransfer.matcher(stringValue);
                  Matcher targetMatcher = sourceTransfer.matcher(oldStringValue);
                  if (sourceMatcher.groupCount() >= 1 && targetMatcher.groupCount() >= 1)
                  {
                    StringBuilder result = new StringBuilder();
                    int index = 0;
                    int sourceStart = 0;
                    int targetStart = 0;
                    if (sourceTransfer.pattern().startsWith("(\\s*<!--"))
                    {
                      sourceStart = getStartIndex(stringValue);
                      targetStart = getStartIndex(oldStringValue);
                    }
                    for (boolean match = sourceMatcher.find(sourceStart) && targetMatcher.find(targetStart); 
                         match; 
                         match = sourceMatcher.find() && targetMatcher.find())
                    {
                      result.append(stringValue.substring(index, sourceMatcher.start(1)));
                      result.append(targetMatcher.group(1));
                      index =  sourceMatcher.end(1);
                    }
                    // There must be at least one match.
                    //
                    if (result.length() == 0)
                    {
                      stringValue = null;
                    }
                    else
                    {
                      result.append(stringValue.substring(index));
                      stringValue = result.toString();
                    }
                  }
                  else
                  {
                    stringValue = null;
                  }
                }
              }
              value = stringValue;
            }
            
            if (value != null || 
                targetPutMethod.getName().equals("setInitializer") ||
                targetPutMethod.getName().equals("setSuperclass") ||
                targetPutMethod.getName().equals("setExceptions"))
            {
              // Ignore if there is not substantial change.
              //
              Object oldValue = sourceGetMethod.invoke(targetNode, NO_ARGUMENTS);
              if (value == null ? oldValue == null : value.equals(oldValue))
              {
                continue;
              }
              else if (value instanceof Object[] && oldValue instanceof Object[] && Arrays.equals((Object[])value, (Object[])oldValue))
              {
                continue;
              }
              else if (targetPutMethod.getName().equals("setSuperclass"))
              {
                if (oldValue != null && value != null && ((String)oldValue).trim().equals(((String)value).trim()))
                {
                  continue;
                }
              }
  
              // The block pattern needs to prevent merging of the return type, to allow changing from the modeled data
              // type (Bugzilla 102209). 
              //
              if (sourceGetMethod.getName().equals("getReturnType") &&
                  getControlModel().getBlockPattern() != null && 
                  ((JMethod)targetNode).getComment() != null &&
                  getControlModel().getBlockPattern().matcher(((JMethod)targetNode).getComment()).find())
              {
                continue;
              }
  
              targetPutMethod.invoke(targetNode, new Object [] { value });
              targetCompilationChanged = true;
              if (targetPutMethod.getName().equals("setBody") && sourceNode instanceof JMethod)
              {
                JMethod sourceMethod = (JMethod)sourceNode;
                JMethod targetMethod = (JMethod)targetNode;

                String[] sourceParameterTypes = sourceMethod.getParameterTypes();
                String[] targetParameterTypes = targetMethod.getParameterTypes();
                if (Arrays.equals(sourceParameterTypes, targetParameterTypes))
                {
                  targetMethod.setParameterNames(sourceMethod.getParameterNames());
                }
                else
                {
                  // The methods were matched by a process that doesn't consider the
                  // parameters
                  targetMethod.setParameters(sourceMethod.getParameters());
                }
              }
            }
          }
          // source method return type is array (getExceptions), target is not array (i.e. addException)
          else
          {
            ArrayList<String> additionalStrings = new ArrayList<String>();
            String[] sourceStrings = (String[])value;
            if (sourceStrings != null)
            {
              additionalStrings.addAll(Arrays.asList(sourceStrings));
            }
  
            if (targetPutMethod.getName().equals("addSuperInterface"))
            {
              Pattern sourceTransfer = pullRule.getSourceTransfer();
              String comment = ((JMember)targetNode).getComment();
              if (sourceTransfer != null && comment != null)
              {
                Matcher matcher = sourceTransfer.matcher(comment);
                while (matcher.find() && matcher.groupCount() >= 1)
                {
                  String clientStrings =
                    comment.substring(matcher.start(matcher.groupCount()), matcher.end(matcher.groupCount()));
  
                  for (StringTokenizer stringTokenizer = new StringTokenizer(clientStrings, ", \t\n\r\f"); 
                       stringTokenizer.hasMoreTokens(); )
                  {
                    String token = stringTokenizer.nextToken();
                    if (!additionalStrings.contains(token))
                    {
                      additionalStrings.add(token);
                    }
                  }
                }
              }
  
              String[] oldSuperInterfaces = (String[])sourceGetMethod.invoke(targetNode);
              String[] superInterfaces = additionalStrings.toArray(new String[additionalStrings.size()]);
              if (oldSuperInterfaces == null ?
                   superInterfaces.length != 0 :
                   !Arrays.equals(oldSuperInterfaces, superInterfaces))
              {
                Method putMethod = targetNode.getClass().getMethod("setSuperInterfaces", String [].class);
                putMethod.invoke(targetNode, new Object []{ superInterfaces });
                targetCompilationChanged = true;
              }
            }
            // target method is NOT addSuperInterface
            else
            {
              String [] oldStringValues = (String [])sourceGetMethod.invoke(targetNode, NO_ARGUMENTS);
              List<String> old = oldStringValues == null ? Collections.<String>emptyList() : Arrays.<String>asList(oldStringValues);
              for (String string : additionalStrings)
              {
                if (!old.contains(string))
                {
                  targetPutMethod.invoke(targetNode, new Object [] { string });
                  targetCompilationChanged = true;
                }
              }
            }
          }
        }
      }
  
    }
    catch (InvocationTargetException exception)
    {
      if (DEBUG)
      {
        exception.printStackTrace();
      }
    }
    catch (IllegalAccessException exception)
    {
      if (DEBUG)
      {
        exception.printStackTrace();
      }
    }
    catch (SecurityException e)
    {
      if (DEBUG)
      {
        e.printStackTrace();
      }
    }
    catch (NoSuchMethodException e)
    {
      if (DEBUG)
      {
        e.printStackTrace();
      }
    }
  }

  protected void applySortRules(JNode sourceNode)
  {
    for (JControlModel.SortRule sortRule : getControlModel().getSortRules())
    {
      if (sourcePatternDictionary.isMarkedUp(sortRule.getMarkup(), sourceNode)  &&
          sortRule.getSelector().isInstance(sourceNode))
      {
        JNode parent = sourceNode.getParent();
        List<JNode> children = orderedSourceChildrenMap.get(parent);
        if (children == null)
        {
          children = new ArrayList<JNode>();
          orderedSourceChildrenMap.put(parent, children);
        }
        children.add(sourceNode);
        break;
      }
    }
  }

  protected boolean applySweepRules(JNode targetNode)
  {
    for (JControlModel.SweepRule sweepRule : getControlModel().getSweepRules())
    {
      boolean sweep = 
           (sweepRule.getSelector() == JImport.class 
              && targetNode instanceof JImport 
              && sweepRule.getMarkup().matcher(targetNode.getName()).find())
        || (sweepRule.getSelector().isInstance(targetNode)  
              && targetPatternDictionary.isMarkedUp(sweepRule.getMarkup(), sweepRule.getParentMarkup(), targetNode));
      
      if (sweep)
      {
        switch (sweepRule.getAction())
        {
          case REMOVE:
          {
            getControlModel().getFacadeHelper().remove(targetNode);
            return true;
          }
          case RENAME:
          {
            String newName = sweepRule.getNewName();
            int index = newName.indexOf("{0}");
            if (index >= 0)
            {
              String name = targetNode.getName();
              newName = newName.substring(0, index) + name + newName.substring(index + "{0}".length());
            }            
            targetNode.setName(newName);
            return true;
          }
          case COMMENT:
          {
            getControlModel().getFacadeHelper().commentOut(targetNode);
            return true;
          }
        }
        break;
      }
    }
    return false;
  }

  /**
   * Checks if two nodes are compatible and can be merged.
   * <p>
   * This method must always return <code>false</code> if target node
   * can have children that can not be added to source node. 
   * 
   * @param sourceNode
   * @param targetNode
   * @return <code>true</code> if nodes can be merged, <code>false</code> otherwise
   */
  protected boolean areCompatible(JNode sourceNode, JNode targetNode)
  {
    return targetNode != null && targetNode.getClass().isInstance(sourceNode);
  }

  /**
   * Converts the target abstract type to be compatible with the given source node class
   * 
   * @param targetAbstractType
   * @param sourceClass the class to which to convert the node.
   * @return <code>null</code> when conversion not possible, converted node otherwise
   */
  protected JNode convertTarget(JAbstractType targetAbstractType, Class<? extends JAbstractType> sourceClass)
  {
    NodeConverter converter = getControlModel().getFacadeHelper().getNodeConverter();
    if (converter != null)
    {
      noAbstractTypeConversion = false;
      return converter.convert(targetAbstractType, sourceClass);      
    }
    else
    {
      return null;
    }
  }

  protected JNode insertClone(JNode sourceNode)
  {
    FacadeHelper facadeHelper = getControlModel().getFacadeHelper();
    if (sourceNode == sourceCompilationUnit && !targetCompilationUnitExists)
    {
      String originalContents = facadeHelper.getOriginalContents(sourceCompilationUnit);
      if (originalContents != null)
      {
        return createCompilationUnitForContents(facadeHelper.applyFormatRules(originalContents));
      }
    }
    
    Object context = targetCompilationUnit != null ? facadeHelper.getContext(targetCompilationUnit) : null; 
    JNode targetNode = facadeHelper.cloneNode(context, sourceNode);
    if (targetNode != null)
    {
      mapChildren(sourceNode, targetNode);
    }
    
    JNode sourceParent = sourceNode.getParent();
    if (sourceParent != null)
    {
      JNode targetParent = sourceToTargetMap.get(sourceParent);
      if (targetParent != null)
      {
        targetCompilationChanged = true;
        JNode targetParentFirstChild = null;
        if (facadeHelper.isSibilingTraversalExpensive())
        { 
          List<JNode> sourceChildren = sourceParent.getChildren();
          List<JNode> targetChildren = targetParent.getChildren();
          
          for (int i = sourceChildren.indexOf(sourceNode); i >= 0; i--)
          {
            if (i > 0)
            {
              JNode previousNode = sourceChildren.get(i-1);
              JNode targetSibling = sourceToTargetMap.get(previousNode);
              if (targetSibling != null)
              {
                int targetSibilingIndex = targetChildren.indexOf(targetSibling);
                if (targetSibilingIndex == targetChildren.size()-1)
                {
                  facadeHelper.addChild(targetSibling.getParent(), targetNode);
                }
                else
                {
                  facadeHelper.insertSibling(targetChildren.get(targetSibilingIndex+1), targetNode, true);
                }
                return targetNode;
              }
            }            
          }
          targetParentFirstChild = targetChildren.isEmpty() ? null : targetChildren.get(0);
        }
        else
        {
          for (JNode previousNode = facadeHelper.getPrevious(sourceNode); previousNode != null; previousNode = facadeHelper.getPrevious(previousNode))
          {
            JNode targetSibling = sourceToTargetMap.get(previousNode);
            if (targetSibling != null)
            {
              JNode targetNextSibling = facadeHelper.getNext(targetSibling);
              if (targetNextSibling == null)
              {
                facadeHelper.addChild(targetSibling.getParent(), targetNode);
              }
              else
              {
                facadeHelper.insertSibling(targetNextSibling, targetNode, true);
              }
              return targetNode;
            }
          }
          targetParentFirstChild = facadeHelper.getFirstChild(targetParent);
        }
        
        if (targetParentFirstChild == null)
        {
          facadeHelper.addChild(targetParent, targetNode);
        }
        else
        {
          facadeHelper.insertSibling(targetParentFirstChild, targetNode, true);
        }
      }
    }
    return targetNode;
  }
  
  /**
   * Checks if the node is marked up for pushing.
   * <p>
   * Node is considered marked up by default if there are no push rules for this node class.
   * <p>
   * The first push rule that matches the node makes the node marked up, and no further checking is performed.
   * <p>
   * If the push rule does not match the node, but the push rule is defined for the same node class,
   * then the node will not be marked up, unless any of the following push rules will match the node.
   * 
   * @param node
   * @return <code>true</code> if node should be pushed, <code>false</code> otherwise
   */
  protected boolean isPushMarkedUp(JNode node)
  {
    JNode sourceParent = node.getParent();
    JNode targetParent = sourceToTargetMap.get(sourceParent);
    assert targetParent != null; // if the parent is not in target, there is no point on checking the child
    
    // Don't push method annotations into redirected methods.
    //
    String redirect = getControlModel().getRedirect();
    if (redirect != null &&
          node instanceof JAnnotation && 
          sourceParent instanceof JMethod)
    {
      JMethod sourceMethod = (JMethod)sourceParent;
      if (!sourceMethod.isConstructor() &&
            !sourceMethod.getName().endsWith(redirect) &&
            targetParent instanceof JMethod &&
            ((JMethod)targetParent).getName().endsWith(redirect))
      {
        return false;
      }
    }

    // by default nodes are marked up
    boolean markedUp = true;
    for (JControlModel.PushRule rule : getControlModel().getPushRules())
    {
      // ignore rules for nodes of different class
      if (rule.getSelector().isInstance(node))
      {
        Pattern targetParentMarkup = rule.getTargetParentMarkup();
        Pattern sourceMarkup = rule.getMarkup();

        // apply target parent pattern first
        if (getSourcePatternDictionary().isMarkedUp(sourceMarkup, node)
          && (getTargetPatternDictionary().isMarkedUp(targetParentMarkup, targetParent)))
        {
          return true;
        }
        else
        {
          // node is not marked up by now, but keep checking other rules 
          markedUp = false;
        }
      }
    }
    return markedUp;
  }
  
  /**
   * Maps the specified source and target nodes.
   * @param sourceNode
   * @param targetNode
   */
  protected void map(JNode sourceNode, JNode targetNode)
  {
    if (sourceNode != null)
    {
      sourceToTargetMap.put(sourceNode, targetNode);
    }
    targetToSourceMap.put(targetNode, sourceNode);
  }  
  
  protected void mapChildren(JNode sourceNode, JNode targetNode)
  {
    map(sourceNode, targetNode);
    
    FacadeHelper facadeHelper = getControlModel().getFacadeHelper();
    if (facadeHelper.isSibilingTraversalExpensive())
    {
      if (sourceNode != null)
      {
        List<JNode> sourceChildren = sourceNode.getChildren();
        if (targetNode == null)
        {
          for (int i = 0, size = sourceChildren.size(); i < size; i++)
          {
            mapChildren(sourceChildren.get(i), null);
          }           
        }
        else
        {
          List<JNode> targetChildren = targetNode.getChildren();
          int targetChildrenSize = targetChildren.size();
          for (int i = 0, size = sourceChildren.size(); i < size; i++)
          {
            mapChildren(sourceChildren.get(i), targetChildrenSize > i ? targetChildren.get(i) : null);
          }   
        }
      }      
    }
    else
    {
      for (JNode sourceChild = facadeHelper.getFirstChild(sourceNode), targetChild = facadeHelper.getFirstChild(targetNode); 
           sourceChild != null;
           sourceChild = facadeHelper.getNext(sourceChild), targetChild = facadeHelper.getNext(targetChild))
      {
        mapChildren(sourceChild, targetChild);
      }
    }
  }
  
  
  // Command line execution methods
    
  /**
   * This is called with the command line arguments of a headless workbench invocation.
   */
  public Object run(Object object) 
  {
    try
    {
      // Three arguments are expected: the .xml getControlModel() URI, the source java URI, and the target java URI.
      //
      String contents = execute(new BasicMonitor(), (String[])object);

      System.out.println("**********************************************");
      System.out.println(contents);

      return 0;
    }
    catch (Exception exception)
    {
      return 1;
    }
  }

  /**
   * Utility for headless operations.
   * @return the merged content.
   */
  public String execute(Monitor monitor, String[] arguments)
  {
    String mergeXML = arguments[0];
    String sourceURI = arguments[1];
    String targetURI = arguments[2];
    String facadeHelperClass = arguments.length > 3 ? arguments[3] : DEFAULT_FACADE_HELPER_CLASS;
    
    // Create the options model.
    //
    controlModel =  new JControlModel();
    controlModel.initialize(CodeGenUtil.instantiateFacadeHelper(facadeHelperClass), mergeXML);
  
    // Create the source and target JCompilationUnit.
    //
    sourceCompilationUnit = createCompilationUnitForURI(sourceURI);
    targetCompilationUnit = createCompilationUnitForURI(targetURI);
  
    // Create a pattern dictionary for each.
    //
    sourcePatternDictionary = new JPatternDictionary(sourceCompilationUnit, getControlModel());
    targetPatternDictionary = new JPatternDictionary(targetCompilationUnit, getControlModel());
    
    merge();
    
    String contents = getTargetCompilationUnitContents();
    if (controlModel.getFacadeHelper() != null)
    {
      controlModel.getFacadeHelper().reset();
    }
    return contents;
  }
}
