/*
 * Copyright (c) 2002 - 2006 IBM Corporation.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 */
package com.ibm.wala.cast.java.ipa.callgraph;

import com.ibm.wala.analysis.typeInference.TypeInference;
import com.ibm.wala.cast.ipa.callgraph.AstSSAPropagationCallGraphBuilder;
import com.ibm.wala.cast.ipa.callgraph.GlobalObjectKey;
import com.ibm.wala.cast.java.analysis.typeInference.AstJavaTypeInference;
import com.ibm.wala.cast.java.loader.JavaSourceLoaderImpl.JavaClass;
import com.ibm.wala.cast.java.ssa.AstJavaInstructionVisitor;
import com.ibm.wala.cast.java.ssa.AstJavaInvokeInstruction;
import com.ibm.wala.cast.java.ssa.AstJavaNewEnclosingInstruction;
import com.ibm.wala.cast.java.ssa.EnclosingObjectReference;
import com.ibm.wala.classLoader.IClass;
import com.ibm.wala.classLoader.IMethod;
import com.ibm.wala.fixpoint.IntSetVariable;
import com.ibm.wala.fixpoint.UnaryOperator;
import com.ibm.wala.ipa.callgraph.AnalysisOptions;
import com.ibm.wala.ipa.callgraph.CGNode;
import com.ibm.wala.ipa.callgraph.IAnalysisCacheView;
import com.ibm.wala.ipa.callgraph.propagation.AbstractFieldPointerKey;
import com.ibm.wala.ipa.callgraph.propagation.InstanceKey;
import com.ibm.wala.ipa.callgraph.propagation.LocalPointerKey;
import com.ibm.wala.ipa.callgraph.propagation.PointerKey;
import com.ibm.wala.ipa.callgraph.propagation.PointerKeyFactory;
import com.ibm.wala.ipa.callgraph.propagation.PointsToSetVariable;
import com.ibm.wala.ssa.IR;
import com.ibm.wala.ssa.SSANewInstruction;
import com.ibm.wala.ssa.SymbolTable;
import com.ibm.wala.types.MethodReference;
import com.ibm.wala.util.debug.Assertions;
import com.ibm.wala.util.strings.Atom;

public class AstJavaSSAPropagationCallGraphBuilder extends AstSSAPropagationCallGraphBuilder {

  protected AstJavaSSAPropagationCallGraphBuilder(IMethod fakeRootClass, AnalysisOptions options, IAnalysisCacheView cache,
      PointerKeyFactory pointerKeyFactory) {
    super(fakeRootClass, options, cache, pointerKeyFactory);
  }

  // ///////////////////////////////////////////////////////////////////////////
  //
  // language specialization interface
  //
  // ///////////////////////////////////////////////////////////////////////////

  @Override
  protected boolean useObjectCatalog() {
    return false;
  }

  @Override
  protected AbstractFieldPointerKey fieldKeyForUnknownWrites(AbstractFieldPointerKey fieldKey) {
    assert false;
    return null;
  }

  // ///////////////////////////////////////////////////////////////////////////
  //
  // enclosing object pointer flow support
  //
  // ///////////////////////////////////////////////////////////////////////////

  public static class EnclosingObjectReferenceKey extends AbstractFieldPointerKey {
    private final IClass outer;

    public EnclosingObjectReferenceKey(InstanceKey inner, IClass outer) {
      super(inner);
      this.outer = outer;
    }

    @Override
    public int hashCode() {
      return getInstanceKey().hashCode() * outer.hashCode();
    }

    @Override
    public boolean equals(Object o) {
      return (o instanceof EnclosingObjectReferenceKey) && ((EnclosingObjectReferenceKey) o).outer.equals(outer)
          && ((EnclosingObjectReferenceKey) o).getInstanceKey().equals(getInstanceKey());
    }
  }

  // ///////////////////////////////////////////////////////////////////////////
  //
  // top-level node constraint generation
  //
  // ///////////////////////////////////////////////////////////////////////////

  protected TypeInference makeTypeInference(IR ir) {
    TypeInference ti = new AstJavaTypeInference(ir, false);

    if (DEBUG_TYPE_INFERENCE) {
      System.err.println(("IR of " + ir.getMethod()));
      System.err.println(ir);
      System.err.println(("TypeInference of " + ir.getMethod()));
      for (int i = 0; i <= ir.getSymbolTable().getMaxValueNumber(); i++) {
        if (ti.isUndefined(i)) {
          System.err.println(("  value " + i + " is undefined"));
        } else {
          System.err.println(("  value " + i + " has type " + ti.getType(i)));
        }
      }
    }

    return ti;
  }

  protected class AstJavaInterestingVisitor extends AstInterestingVisitor implements AstJavaInstructionVisitor {
    protected AstJavaInterestingVisitor(int vn) {
      super(vn);
    }

    @Override
    public void visitEnclosingObjectReference(EnclosingObjectReference inst) {
      Assertions.UNREACHABLE();
    }

    @Override
    public void visitJavaInvoke(AstJavaInvokeInstruction instruction) {
      bingo = true;
    }
  }

  @Override
  protected InterestingVisitor makeInterestingVisitor(CGNode node, int vn) {
    return new AstJavaInterestingVisitor(vn);
  }

  // ///////////////////////////////////////////////////////////////////////////
  //
  // specialized pointer analysis
  //
  // ///////////////////////////////////////////////////////////////////////////

  protected static class AstJavaConstraintVisitor extends AstConstraintVisitor implements AstJavaInstructionVisitor {

    public AstJavaConstraintVisitor(
			     AstSSAPropagationCallGraphBuilder builder,
			     CGNode node) 
    {
      super(builder, node);
    }

    /**
     * For each of objKey's instance keys ik, adds the constraint lvalKey = EORK(ik,cls),
     * where EORK(ik,cls) will be made equivalent to the actual enclosing class by
     * the handleNew() function below.
     */
    private void handleEnclosingObject(final PointerKey lvalKey, final IClass cls, final PointerKey objKey) {
      SymbolTable symtab = ir.getSymbolTable();
      int objVal;
      if (objKey instanceof LocalPointerKey) {
        objVal = ((LocalPointerKey) objKey).getValueNumber();
      } else {
        objVal = 0;
      }

      if (objVal > 0 && contentsAreInvariant(symtab, du, objVal)) {
        system.recordImplicitPointsToSet(objKey);

        InstanceKey[] objs = getInvariantContents(objVal);

        for (InstanceKey obj : objs) {
          PointerKey enclosing = new EnclosingObjectReferenceKey(obj, cls);
          system.newConstraint(lvalKey, assignOperator, enclosing);
        }

      } else {
        system.newSideEffect(new UnaryOperator<PointsToSetVariable>() {
          @Override
          public byte evaluate(PointsToSetVariable lhs, PointsToSetVariable rhs) {
            IntSetVariable<?> tv = rhs;
            if (tv.getValue() != null) {
              tv.getValue().foreach(ptr -> {
                InstanceKey iKey = system.getInstanceKey(ptr);
                PointerKey enclosing = new EnclosingObjectReferenceKey(iKey, cls);
                system.newConstraint(lvalKey, assignOperator, enclosing);
              });
            }
            return NOT_CHANGED;
          }

          @Override
          public int hashCode() {
            return System.identityHashCode(this);
          }

          @Override
          public boolean equals(Object o) {
            return o == this;
          }

          @Override
          public String toString() {
            return "enclosing objects of " + objKey;
          }
        }, objKey);
      }
    }

    @Override
    public void visitEnclosingObjectReference(EnclosingObjectReference inst) {
      PointerKey lvalKey = getPointerKeyForLocal(inst.getDef());
      PointerKey objKey = getPointerKeyForLocal(1);
      IClass cls = getClassHierarchy().lookupClass(inst.getEnclosingType());
      handleEnclosingObject(lvalKey, cls, objKey);
    }

    @Override
    public void visitNew(SSANewInstruction instruction) {
      super.visitNew(instruction);
      InstanceKey iKey = getInstanceKeyForAllocation(instruction.getNewSite());

      if (iKey != null) {
        IClass klass = iKey.getConcreteType();

        // in the case of a AstJavaNewEnclosingInstruction (a new instruction like outer.new Bla()),
        // we may need to record the instance keys if the pointer key outer is invariant (and thus implicit) 
        InstanceKey enclosingInvariantKeys[] = null;
       
        if (klass instanceof JavaClass) {
          IClass enclosingClass = ((JavaClass) klass).getEnclosingClass(); // the immediate enclosing class.
          if (enclosingClass != null) {
            IClass currentCls = node.getMethod().getDeclaringClass();
            PointerKey objKey;
            
            if ( instruction instanceof AstJavaNewEnclosingInstruction ) {
              

              int enclosingVal = ((AstJavaNewEnclosingInstruction) instruction).getEnclosing();
              SymbolTable symtab = ir.getSymbolTable();

              // pk 'outer' is invariant, which means it's implicit, so can't add a constraint with the pointer key.
              // we should just add constraints directly to the instance keys (below) 
              if ( contentsAreInvariant(symtab, du, enclosingVal) )
                enclosingInvariantKeys = getInvariantContents(enclosingVal);
              
              // what happens if objKey is implicit but the contents aren't invariant?! (it this possible?) big trouble!
              
              objKey = getPointerKeyForLocal(enclosingVal);
            }
            else
              objKey = getPointerKeyForLocal(1);

            System.err.println(("class is " + klass + ", enclosing is " + enclosingClass + ", method is " + node.getMethod()));

            if (node.getMethod().isWalaSynthetic()) {
              return;
            }

            currentCls = enclosingClass;
            
            PointerKey x = new EnclosingObjectReferenceKey(iKey, currentCls);
            if ( enclosingInvariantKeys != null )
              for ( InstanceKey obj: enclosingInvariantKeys )
                system.newConstraint(x, obj);
            else
              system.newConstraint(x, assignOperator, objKey);

            // If the immediate inclosing class is not a top-level class, we must make EORKs for all enclosing classes up to the top level.
            // for instance, if we have "D d = c.new D()", and c is of type A$B$C, methods in D may reference variables and functions from
            // A, B, and C. Therefore we must also make the links from EORK(allocsite of d,enc class B) and EORK(allocsite of d,en class A).
            // We do this by getting the enclosing class of C and making a link from EORK(d,B) -> EORK(c,B), etc.
            currentCls = ((JavaClass) currentCls).getEnclosingClass();
            while (currentCls != null) { 
              x = new EnclosingObjectReferenceKey(iKey, currentCls); // make EORK(d,B), EORK(d,A), etc.
              handleEnclosingObject(x, currentCls, objKey);
              // objKey is the pointer key representing the immediate inner class.
              // handleEnclosingObject finds x's instance keys and for each one "ik" links x to EORK(ik,currentCls)
              // thus, for currentCls=B, it will find the allocation site of c and make a link from EORK(d,B) to EORK(c,B)
              
              currentCls = ((JavaClass) currentCls).getEnclosingClass();
            }
            
          }
        }
      }
    }

    @Override
    public void visitJavaInvoke(AstJavaInvokeInstruction instruction) {
      visitInvokeInternal(instruction, new DefaultInvariantComputer());
    }
  }

  @Override
  public ConstraintVisitor makeVisitor(CGNode node) {
    return new AstJavaConstraintVisitor(this, node);
  }

  @Override
  protected boolean sameMethod(CGNode opNode, String definingMethod) {
    MethodReference reference = opNode.getMethod().getReference();
    String selector = reference.getSelector().toString();
    String containingClass = reference.getDeclaringClass().getName().toString();
    return definingMethod.equals(containingClass + '/' + selector);
  }

  @Override
  public GlobalObjectKey getGlobalObject(Atom language) {
    Assertions.UNREACHABLE();
    return null;
  }
}
