package lawOfDemeter;
import java.util.*;

/**
 * The ImmediatePartBin aspect is part of a Law of Demeter Checker - a set of
 * classes and aspects that verify a given program does not violate
 * the Law of Demeter.  This aspect stores the immediate parts, or
 * instance variables, of objects as they are set for later
 * examination by the Checker aspect.
 *
 * @author David H. Lorenz
 * @author Modifications made by Paul Freeman
 */
public aspect ImmediatePartBin extends Supplier
    pertarget(Any.ConstructorExecution()){
  /**
   * What this supplier contains.
   */
  public String contains(){ return "immediate part objects"; }

  /**
   * Returns the object associated with the supplied argument thisObj.
   *
   * @param thisObj
   * @return Returns the correct supplier for the supplied Object thisObject
   * if there is one, else returns null.
   */
  public Supplier getSupplier(Object thisObj) {
      try{
      // this needs to be "tried" as there might NOT be an aspect associated with this obj
        return this.aspectOf(thisObj);
      }catch(org.aspectj.lang.NoAspectBoundException e){
        return null;
      }
  }

  /**
   * Advice that gathers the immediate part objects.
   */
  before():
    Any.Set() {
      // even though this aspect is declared perTarget, ensure we have the correct bin
      ImmediatePartBin partBin = ImmediatePartBin.aspectOf(thisJoinPoint.getTarget());

      // remove the old value based on this partName
      String partName = thisJoinPoint.getSignature().getName();
      partBin.removeOldValue(partName);
      // There is only one argument to a set join point.
      Object newValue = thisJoinPoint.getArgs()[0];
      partBin.addNewValue(partName, newValue);

      partBin.add(newValue, direct+at(thisJoinPoint));
//      System.out.println("Adding to ImmediatePartBin of " + thisJoinPoint.getTarget() + " --" + thisJoinPoint + at(thisJoinPoint) + " in " + thisJoinPoint.getTarget());
  }

  // HashMap to store immediate part values by immediate part name so that old
  //  immediate part values can be removed when new values are set
  // NOTE: must be a standard hashmap as the String representing the instance
  //  variable name (the key) will not necessarily be the same object between
  //  puts and removals. Counts of the instances of the target object in
  //  the partNameTargets hashmap are maintained in an IdentityHashMap declared below.
  private HashMap partNameTargets = new HashMap();

  // code to add the value to the partNameTargets HashMap
  protected void addNewValue(String partName, Object newValue){
    partNameTargets.put(partName, newValue);
    long count = increaseCount(newValue);
//    System.out.println("-- set: " + partName + " = " + newValue + " count = " + count);
  }

  // code to remove the value from the partNameTargets HashMap AND the targets HashMap
  protected void removeOldValue(String partName){
    // find the object that corresponds to the partName and remove this pair from partNameTargets
    Object oldVal = partNameTargets.remove(partName);
    long instanceCount = decreaseCount(oldVal);
//    System.out.println("-- removed partNameTarget: " + partName + " = " + oldVal + " count = " + instanceCount);
    // if removed the last oldVal instance, i.e. oldVal is not aliased to another
    // partName in this object, then remove it from targets.  This is required since
    // each object exists only once in targets
    if(oldVal != null && instanceCount == 0){
      targets.remove(oldVal);
//      System.out.println("-- removed target: " + partName + " = " + oldVal);
    }
  }

  //HashMap to store counts of target object instances in the partNameTargets HashMap
  // this count is used to determine whether or not it is safe to remove the object
  // instance from the targets IdentityHashMap.  We must keep a count of the number
  // of times the target object exists in the partNameTargets hashmap as a single
  // object might be aliased to more than one part name.
  private IdentityHashMap targetInstances = new IdentityHashMap();

  //helper code to increase the target instance count
  private long increaseCount(Object targetObject){
    long count = getCount(targetObject) + 1;
    targetInstances.put(targetObject, new Long(count));
    return count;
  }

  // helper code that returns the count for a particular target object
  private long getCount(Object targetObject){
    Long count = (Long)targetInstances.get(targetObject);
    if(count == null){
      return 0;
    }else{
      return count.longValue();
    }
  }

  // helper code that decreases the target instance count
  private long decreaseCount(Object targetObject){
    long count = getCount(targetObject) - 1;
    if(count == 0){
      targetInstances.remove(targetObject);
    }else if(count < 0){
      count = 0;
    }else{
      targetInstances.put(targetObject, new Long(count));
    }
    return count;
  }
}

