package com.croctech.values.helper;

import java.lang.reflect.Array;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.StringTokenizer;

import org.apache.log4j.Category;

/**
 * <p>
 * This class is used to assemble trees of Value objects.
 * </p>
 * 
 * <p>
 * The tree builder takes a definition of the tree to be built, and uses
 * the local interface of the entity to build the tree.
 * </p>
 * 
 * <p>
 * <b>Usage :</b>
 * </p>
 * 
 * <p>
 * 1. Get the local interface of the entity that the tree is to be built for <br/>
 * 2. Create an include tree for the tree builder to use <br/>
 * 3. Call ValueTreeBuilder.getValueTree(...) with the entity and the 
 *    include tree. <br/>
 * </p>
 * 
 * <p>
 * The include tree specifies the tree of entities you want included within
 * the value object.  It is specified using a sort of package notation.
 * </p>
 * 
 * <p>
 * For example, if a Membership bean is the root entity, you could specify an include
 * tree such as : <br/>
 * "Member.*, Subscription, Subscription.vehicle"
 * </p>
 * 
 * <p>
 * This will retrieve a MembershipValue object with all non-CMR
 * fields present, and the CMR fields Member and Subscription present. 
 * </p>
 * 
 * <p>
 * Subscription will only have its non-CMR fields and it's vehicle
 * CMR field present, but Member will have the full tree of value
 * objects below it.
 * </p>
 * 
 * <p>
 * <b>Include tree wildcards : </b>
 * </p>
 *
 * <p> 
 * You can just pass '*' to get everything reachable from an entity.
 * </p>
 * 
 * <p>
 * For example :
 * </p>
 *
 * <p>
 *   thing = thingHome.findByPrimaryKey(thingId); <br/>
 *   value = (ThingValue) ValueTreeBuilder.getValueTree(  <br/>
 *               thing,  <br/>
 *               ValueTreeBuilder.createIncludeTree("*") ); <br/>
 * </p>
 * 
 * <p>
 * <b>Cyclic relationships :</b>
 * </p>
 * 
 * <p>
 * The builder should handle cyclic relationships safely, and builds a
 * value tree that is self referencing.
 * </p>
 * 
 * <p>
 * <b>Design patterns :</b>
 * </p>
 * 
 * <p>
 * For the builder to work all entities must have a 'getValueObject()'
 * method declared which returns a value object containing all non-CMR
 * fields for that entity.
 * </p>
 * 
 * @author Gavin Hughes (based on work by Tim Lee)
 */
public class ValueTreeBuilder {

  private static Category category =
    Category.getInstance(ValueTreeBuilder.class.getName());

  private static Class LOCAL_TAG_CLASS = javax.ejb.EJBLocalObject.class;

  public static class IncludeTree {
    private IncludeTreeNode rootNode;

    protected IncludeTree(IncludeTreeNode root) {
      rootNode = root;
    }

    protected IncludeTreeNode getRootNode() {
      return rootNode;
    }
  }

  protected static class IncludeTreeNode {
    private Map children;
    private String name;
    private boolean copyAll;

    public IncludeTreeNode(String newName) {
      name = newName;
      children = new HashMap();
    }

    public String getName() {
      return name;
    }

    public static IncludeTreeNode createTree(String path) {
      IncludeTreeNode root = new IncludeTreeNode(".");
      StringTokenizer paths = new StringTokenizer(path, ",");

      while (paths.hasMoreTokens()) {
        String currentPath = paths.nextToken();
        StringTokenizer pathElements = new StringTokenizer(currentPath, ".");
        IncludeTreeNode currentNode = root;
        
        while (pathElements.hasMoreTokens()) {
          String pathElement = convertToAccessor(pathElements.nextToken());

          IncludeTreeNode nextNode = currentNode.getPathNode(pathElement);

          if (nextNode == null) {
            IncludeTreeNode newChild = new IncludeTreeNode(pathElement);
            currentNode.addChild(newChild);
            currentNode = newChild;
          } else {
            currentNode = nextNode;
          }
        }
      }

      return root;
    }

    protected static String convertToAccessor(String element) {
      StringBuffer newElement = new StringBuffer("get");
      newElement.append(element);
      newElement.setCharAt(3, Character.toUpperCase(element.charAt(0)));
      return newElement.toString();
    }

    public IncludeTreeNode getPathNode(String element) {
      if (copyAll)
        return this;
      else
        return (IncludeTreeNode) children.get(element);
    }

    public void addChild(IncludeTreeNode child) {
      copyAll = child.getName().endsWith("*");
      if (!copyAll)
        children.put(child.getName(), child);

      debug(name + " adding " + child.getName());
    }

    public String toString() {
      StringBuffer result = new StringBuffer("(name = ) " + name);
  
      return result.toString();
    }

  }

  /**
   * This method creates an IncludeTree based on a path specifying the entities
   * and fields to be included in the tree.
   * 
   * @param includePath  List of entity tree branches to include in value tree.
   */
  public static IncludeTree createIncludeTree(String includePath) {
    return new IncludeTree(IncludeTreeNode.createTree(includePath));
  }

  /**
   * This method assembles the value tree for an entity.
   * 
   * @param local        Local interface of the entity
   * @param includeTree  IncludeTree that defines which related entities (and 
   *                      attributes thereof) are to be included in the Value 
   *                      tree.
   */
  public static Object getValueTree(Object local, IncludeTree includeTree) {
    HashMap visited = new HashMap();
    
    return populateValueObject(local, visited, includeTree.getRootNode());
  }
  
  /**
   * This method does the actual work - it iterates over the fields on a
   * bean's local interface and copies the data to the corresponding value
   * object.
   * 
   * For CMR fields the method recurses down the tree to copy all branches
   * specified by the include tree.
   * 
   * There is some ugliness involved in translating many relationships
   * (Collections on the local interface need to be translated to Value object
   * arrays on the Value object).
   * 
   * @param local        Local interface of the entity
   */
  protected static Object populateValueObject(Object local,
                                                HashMap visited,
                                                IncludeTreeNode pathNode) {
                                                  
    debug("Processing : " + local.getClass().getName());
    Object value = null;

    try {
      
      // recursive base case (value object already created for local object)
      value = visited.get(getLocalObjectKey(local));
      if (value != null) {
        debug("Object already processed, returning");
        return value;
      } else {
        debug("Getting value object (calling getValueObject on local interface)");
        Method getValueObjectMethod =
          local.getClass().getMethod("getValueObject", null);
        value = getValueObjectMethod.invoke(local, null);
        visited.put(getLocalObjectKey(local), value);
      }

      Method methods[] = local.getClass().getDeclaredMethods();

      // loop over methods in local object
      for (int i = 0; i < methods.length; i++) {
        String name = methods[i].getName();

        // if a getXyz() method is found...
        if (name.startsWith("get") && !name.equals("getValueObject")) {
          
          Class interfaces[];
          Class returnType = methods[i].getReturnType();
          
          if (returnType.equals(java.util.Collection.class)) {
            
            debug("Found valid get method returning a Collection " + name);
            
            // only copy collection if the specified copy tree says so
            IncludeTreeNode nextPathNode = pathNode.getPathNode(name);
            if (nextPathNode != null) {
              
              debug("Copying the collection for : " + nextPathNode);
              debug("Calling method : " + methods[i].getName());
              
              // copy collection of local interfaces to a value object array
              Collection localObjectCollection =
                (Collection) methods[i].invoke(local, null);
                
              // find the method on the value object that will be passed the value object array
              String methodName = "set" + name.substring(3);
              debug("Looking for " + methodName + " in " + value.getClass());
              
              Method[] detailMethods = value.getClass().getMethods();
              Method setMethod = null;
              for (int k=0; k < detailMethods.length; k++) {
                Method method = detailMethods[k];
                debug ("trying to match " + method.getName() + " with " + methodName);
                if (method.getName().equals(methodName)) {
                  setMethod = method;
                  break;
                }
              }    
              
              // get the type of the value object array to be passed
              Class valueObjectArrayClass = (setMethod.getParameterTypes())[0];

              Class valueObjectClass = valueObjectArrayClass.getComponentType();
              debug("creating Value object array");
              Object[] valueObjectArray = (Object[]) Array.newInstance(valueObjectClass, localObjectCollection.size());

              debug("iterating over local objects in collection.");
              
              if (localObjectCollection != null) {
                Iterator localObjects = localObjectCollection.iterator();
                
                // iterate over all local objects in collection
                int j = 0;
                while (localObjects.hasNext()) {
                  // copy each object
                  Object newLocalObject = localObjects.next();
                  debug("recursively calling populateValueObject");
                  Object newValueObject =
                    populateValueObject(newLocalObject, visited, nextPathNode);
                  debug("back from calling populateValueObject");
                  
                  // add value object to array
                  valueObjectArray[j] = newValueObject;
                  j++;
                }
                
                Object args[] = { valueObjectArray };
                debug("calling " + methodName);
                setMethod.invoke(value, args);
                
                debug("collection copied to value object");
              }
            } else {
              debug("Collection ignored (not in path)");
            }
          } else if (
            (interfaces = returnType.getInterfaces()) != null
              && interfaces.length > 0
              && interfaces[0].equals(LOCAL_TAG_CLASS)) {
            debug("Found valid get method : " + returnType + " " + name);
            
            // only copy local object if the specified copy tree says so
            IncludeTreeNode nextPathNode = pathNode.getPathNode(name);
            
            if (nextPathNode != null) {
              
              // copy local object
              debug("Copying local object");

              // get new local object
              Object newLocalObject = methods[i].invoke(local, null);

              if (newLocalObject != null) {
                // create a value object based on the local object class name + package
                debug("Recursively calling populateValueObject");
                
                Object newValueObject =
                  populateValueObject(newLocalObject, visited, nextPathNode);
                debug("Back from calling populateValueObject");

                String methodName = "set" + name.substring(3);
                debug("Looking for " + methodName + "(...) in " + value.getClass());
                Method setMethod = getValueObjectMethod(methodName, value);
                Object args[] = { newValueObject };
                setMethod.invoke(value, args);
                
                debug("Local object copied");
              } else {
                debug("Local object was null, not copied");
              }
            } else {
              debug("Local object ignored");
            }
          }
        }
      }
    } catch (Exception e) {
      debug("Exception : " + e);
    }
    debug("complete");
    
    return value;
  }

  protected static Method getValueObjectMethod(String name, Object value) {
    Method methods[] = value.getClass().getMethods();
    int index = 0;

    while (index < methods.length && !methods[index].getName().equals(name)) {
      index++;
    }

    return methods[index];
  }

  protected static String getLocalObjectKey(Object local) throws Exception {
    Method getMethod = local.getClass().getDeclaredMethod("getId", null);
    
    return local.getClass().getName() + getMethod.invoke(local, null);
  }

  protected static Object createValueFromLocal(
    Class localClass,
    Object local,
    String valuesPackage)
    throws Exception {
    final Class parameterTypes[] = { java.lang.Integer.class };

    // work out value class name from local class name
    Class interfaces[] = local.getClass().getInterfaces();
    boolean localInterfaceFound = false;
    int currentInterface = 0;
    debug("Looking for Local entity interface...");
    while (interfaces[currentInterface].getName().indexOf("Local") == -1) {
      debug("Skipping : " + interfaces[currentInterface].getName());
      currentInterface++;
    }
    debug("Using : " + interfaces[currentInterface].getName());

    String localClassName = interfaces[currentInterface].getName();
    StringBuffer buffer = new StringBuffer(valuesPackage);
    int start = localClassName.indexOf("Local") + 5;
    buffer.append(".");
    buffer.append(localClassName.substring(start));
    buffer.append("Value");

    String valueClassName = buffer.toString();
    debug("Attempting to construct value object : " + valueClassName);

    // get the id to use for value object construction
    Method getMethod = local.getClass().getDeclaredMethod("getId", null);
    Integer id = (Integer) getMethod.invoke(local, null);
    Object parameters[] = { id };

    debug("Using ID = " + id);

    // find the class and instantiate an instance of it.
    Class valueObjectClass = Class.forName(valueClassName);
    Object newValueObject;
    newValueObject =
      valueObjectClass.getDeclaredConstructor(parameterTypes).newInstance(
        parameters);

    return newValueObject;
  }

  static private void debug(String s) {
    category.debug(s);
  }

}