/*
  * JBoss, Home of Professional Open Source
  * Copyright 2005, JBoss Inc., and individual contributors as indicated
  * by the @authors tag. See the copyright.txt in the distribution for a
  * full listing of individual contributors.
  *
  * This is free software; you can redistribute it and/or modify it
  * under the terms of the GNU Lesser General Public License as
  * published by the Free Software Foundation; either version 2.1 of
  * the License, or (at your option) any later version.
  *
  * This software is distributed in the hope that it will be useful,
  * but WITHOUT ANY WARRANTY; without even the implied warranty of
  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  * Lesser General Public License for more details.
  *
  * You should have received a copy of the GNU Lesser General Public
  * License along with this software; if not, write to the Free
  * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
  * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
  */
package org.jboss.aop.instrument;

import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import org.jboss.aop.AOPClassPool;
import org.jboss.aop.AspectManager;
import org.jboss.aop.ClassAdvisor;

import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CodeConverter;
import javassist.CtClass;
import javassist.CtField;
import javassist.CtMethod;
import javassist.CtNewMethod;
import javassist.Modifier;
import javassist.NotFoundException;
import javassist.expr.ExprEditor;
import javassist.expr.FieldAccess;

/**
 * @author <a href="mailto:kabirkhan@bigfoot.com">Kabir Khan</a>
 */
public abstract class FieldAccessTransformer implements CodeConversionObserver
{
   // Constants -----------------------------------------------------
   
   final static String FIELD_INFO_CLASS_NAME = "org.jboss.aop.FieldInfo";
   
   // Attributes ----------------------------------------------------
   Instrumentor instrumentor;
   boolean optimize;
   private Codifier codifier;
   private JoinpointClassifier classifier;
   
   // Static --------------------------------------------------------
   // there are two transformations types available to a field: get and set
   protected static final String[] transformations = new String[] {"get", "set"};
   protected static final int GET_INDEX = 0;
   protected static final int SET_INDEX = 1;
   protected static final WrapperTransformer wrapper = new WrapperTransformer(transformations);
   
   // Constructors --------------------------------------------------
   protected FieldAccessTransformer(Instrumentor instrumentor)
   {
      this.instrumentor = instrumentor;
      this.optimize = AspectManager.optimize;
      this.codifier = new Codifier();
      this.classifier = instrumentor.joinpointClassifier;
   }

   // Public --------------------------------------------------------

   protected void buildFieldWrappers(CtClass clazz, ClassAdvisor advisor) throws NotFoundException, CannotCompileException
   {
      List fields = Instrumentor.getAdvisableFields(clazz);

      int fieldIndex = fieldOffset(clazz.getSuperclass());
      JoinpointClassification[] classificationGet = classifyFieldGet(clazz, advisor);
      JoinpointClassification[] classificationSet = classifyFieldSet(clazz, advisor);
      Iterator it = fields.iterator();
      boolean skipFieldInterception = true;
      for (int index = 0; it.hasNext(); index++, fieldIndex++)
      {
         CtField field = (CtField) it.next();

         if (!isPrepared(classificationGet[index]) && !isPrepared(classificationSet[index]))
         {
            continue;
         }
         
         if (!javassist.Modifier.isPrivate(field.getModifiers()))
         {
            skipFieldInterception = false;
         }
         
         doBuildFieldWrappers(clazz, field, fieldIndex, classificationGet[index], classificationSet[index]);
      }
      
      if (skipFieldInterception)
      {
         advisor.getManager().skipFieldAccess(clazz.getName());
      }
      else
      {
         advisor.getManager().addFieldInterceptionMarker(clazz.getName());
      }
         
   }
   
   protected boolean isPrepared(JoinpointClassification classification)
   {
      return classification != JoinpointClassification.NOT_INSTRUMENTED;
   }
   
   protected abstract void doBuildFieldWrappers(CtClass clazz, CtField field, int index, JoinpointClassification classificationGet, JoinpointClassification classificationSet) throws NotFoundException, CannotCompileException;
   /**
    * replace field access for possible public/protected fields that are intercepted
    * don't need to replace access for private fields.
    *
    * @param clazz
    * @param fieldsAdvisor
    * @return
    * @throws NotFoundException
    */
   public boolean replaceFieldAccess(List fields, CtClass clazz, ClassAdvisor fieldsAdvisor) throws NotFoundException
   {
      CodeConverter converter = instrumentor.getCodeConverter();
      boolean converted = false;
      Iterator it = fields.iterator();
      while (it.hasNext())
      {
         CtField field = (CtField) it.next();
         if (!Modifier.isPrivate(field.getModifiers()) && Instrumentor.isAdvisable(field))
         {
            JoinpointClassification fieldGetClassification = classifier.classifyFieldGet(field, fieldsAdvisor);

            if (fieldGetClassification.equals(JoinpointClassification.WRAPPED))
            {
               converted = true;
               converter.replaceFieldRead(field, clazz, fieldRead(field.getName()));
            }
            JoinpointClassification fieldSetClassification = classifier.classifyFieldSet(field, fieldsAdvisor);
            if (fieldSetClassification.equals(JoinpointClassification.WRAPPED))
            {
               converted = true;
               converter.replaceFieldWrite(field, clazz, fieldWrite(field.getName()));
            }
         }
      }
      return converted;
   }

   /**
    * Wraps the field joinpoints contained in <code>fieldsGet</code> and <code>fieldsSet</code>.
    * @param clazz the class being instrumented.
    * @param fieldsGet a collection of <code>java.lang.Integer</code> indentifying
    * the field reads to be wrapped.
    * @param fieldsSet a collection of <code>java.lang.Integer</code> indentifying
    * the field writes to be wrapped.
    */
   public void wrap(CtClass clazz, Collection fieldsGet, Collection fieldsSet) throws CannotCompileException, NotFoundException
   {
      List advisableFields = Instrumentor.getAdvisableFields(clazz);
      CtField[] fields = new CtField[advisableFields.size()];
      fields = (CtField[] ) advisableFields.toArray(fields);
      for (Iterator iterator = fieldsGet.iterator(); iterator.hasNext(); )
      {
         int fieldIndex = ((Integer) iterator.next()).intValue();
         CtField field = fields[fieldIndex];
         if (wrapper.isNotPrepared(field, GET_INDEX))
         {
            continue;
         }
         // wrap
         wrapper.wrap(field, GET_INDEX);
         // execute wrapping
         String code = "{" + field.getType().getName() + " var; return var;}";
         CtMethod method = getWrapperReadMethod(field, clazz);
         method.setBody(code);
         code = getWrapperBody(clazz, field, true, fieldIndex);
         if (!Modifier.isPrivate(field.getModifiers()))
         {
            instrumentor.converter.replaceFieldRead(field, clazz, fieldRead(field.getName()));
            codifier.addPendingCode(method, code);
         }
         else
         {
            replaceFieldAccessInternally(clazz, field, true, false, fieldIndex);
            method.setBody(code);
         }
      }
      for (Iterator iterator = fieldsSet.iterator(); iterator.hasNext(); )
      {
         int fieldIndex = ((Integer) iterator.next()).intValue();
         CtField field = fields[fieldIndex];
         if (wrapper.isNotPrepared(field, SET_INDEX))
         {
            continue;
         }
         // wrap
         wrapper.wrap(field, SET_INDEX);
         // executeWrapping
         String code = "{  }";
         CtMethod method = getWrapperWriteMethod(field, clazz);
         method.setBody(code);
         code = getWrapperBody(clazz, field, false, fieldIndex);
         if (!Modifier.isPrivate(field.getModifiers()))
         {
            instrumentor.converter.replaceFieldWrite(field, clazz, fieldWrite(field.getName()));
            codifier.addPendingCode(method, code);
         }
         else
         {
            replaceFieldAccessInternally(clazz, field, false, true, fieldIndex);
            method.setBody(code);
         }
      }
   }

   
   protected CtMethod getWrapperReadMethod(CtField field, CtClass clazz)throws NotFoundException
   {
      return clazz.getDeclaredMethod(fieldRead(field.getName()));
   }
   
   protected CtMethod getWrapperWriteMethod(CtField field, CtClass clazz)throws NotFoundException
   {
      return clazz.getDeclaredMethod(fieldWrite(field.getName()));
   }
   
   /**
    * Unwraps the field joinpoints contained in <code>fieldsGet</code> and <code>fieldsSet</code>.
    * @param clazz the class being instrumented.
    * @param fieldsGet a collection of <code>java.lang.Integer</code> indentifying
    * the field reads to be unwrapped.
    * @param fieldsSet a collection of <code>java.lang.Integer</code> indentifying
    * the field writes to be unwrapped.
    */
   public void unwrap(CtClass clazz, Collection fieldsGet, Collection fieldsSet) throws CannotCompileException, NotFoundException
   {
      ClassPool classPool = instrumentor.getClassPool();
      List advisableFields = instrumentor.getAdvisableFields(clazz);
      CtField[] fields = new CtField[advisableFields.size()];
      fields = (CtField[] ) advisableFields.toArray(fields);
      // unwrapping field gets
      for (Iterator iterator = fieldsGet.iterator(); iterator.hasNext(); )
      {
         int fieldIndex = ((Integer) iterator.next()).intValue();
         CtField field = fields[fieldIndex];
         if (wrapper.isNotPrepared(field, GET_INDEX))
         {
            continue;
         }
         // unwrap
         wrapper.unwrap(field, GET_INDEX);
         // execute unwrapping
         CtMethod method = clazz.getDeclaredMethod(fieldRead(field.getName()));
         String target = Modifier.isStatic(field.getModifiers())? clazz.getName(): "((" + clazz.getName() + ")$1)";
         method.setBody("return " + target + "." + field.getName() + ";");
      }
      
      for (Iterator iterator = fieldsSet.iterator(); iterator.hasNext(); )
      {
         int fieldIndex = ((Integer) iterator.next()).intValue();
         CtField field = fields[fieldIndex];
         if (wrapper.isNotPrepared(field, SET_INDEX))
         {
            continue;
         }
         // unwrap
         wrapper.unwrap(field, SET_INDEX);
         // execute unwrapping
         // create field trapWrite memthod
         CtMethod method = clazz.getDeclaredMethod(fieldWrite(field.getName()));
         String target = Modifier.isStatic(field.getModifiers())? clazz.getName(): "((" + clazz.getName() + ")$1)";
         method.setBody(target + "." + field.getName() + "=$2" + ";");
      }
   }
   
   /**
    * Notifies this transformer that the code conversion is done.
    */
   public void codeConverted() throws NotFoundException, CannotCompileException {
      codifier.codifyPending();
   }
   
   protected int fieldOffset(CtClass clazz) throws NotFoundException
   {
      if (clazz == null)
         return 0;
      if (clazz.getName().equals("java.lang.Object"))
         return 0;
      int offset = fieldOffset(clazz.getSuperclass());

      CtField[] fields = clazz.getDeclaredFields();
      for (int i = 0; i < fields.length; i++)
      {
         if (Instrumentor.isAdvisable(fields[i]))
         {
            offset++;
         }
      }
      return offset;
   }

   /**
    * Classifies the field read joinpoints.
    * @param clazz the clazz whose field reads will be classified.
    * @param advisor the advisor associated to <code>clazz</code>.
    * @return a classification array.
    */
   private JoinpointClassification[] classifyFieldGet(CtClass clazz, ClassAdvisor advisor) throws NotFoundException
   {
      List fields = instrumentor.getAdvisableFields(clazz);
      JoinpointClassification[] classification = new JoinpointClassification[fields.size()];
      int index = 0;
      for (Iterator iterator = fields.iterator(); iterator.hasNext(); index++)
      {
         CtField field = (CtField) iterator.next();
         classification[index] = instrumentor.joinpointClassifier.classifyFieldGet(field, advisor);
      }
      return classification;
   }

   /**
    * Classifies the field write joinpoints.
    * @param clazz the clazz whose field writes will be classified.
    * @param advisor the advisor associated to <code>clazz</code>.
    * @return a classification array.
    */
   private JoinpointClassification[] classifyFieldSet(CtClass clazz, ClassAdvisor advisor) throws NotFoundException
   {
      List fields = instrumentor.getAdvisableFields(clazz);
      JoinpointClassification[] classification = new JoinpointClassification[fields.size()];
      int index = 0;
      for (Iterator iterator = fields.iterator(); iterator.hasNext(); index++)
      {
         CtField field = (CtField) iterator.next();
         classification[index] = instrumentor.joinpointClassifier.classifyFieldSet(field, advisor);
      }
      return classification;
   }

   protected String addFieldReadInfoFieldWithAccessors(int modifiers, CtClass addTo, CtField field) throws NotFoundException, CannotCompileException
   {
      return addFieldReadInfoFieldWithAccessors(modifiers, addTo, field, null);
   }
   
   /**
    * Adds a FieldInfo field to the passed in class
    */
   protected String addFieldReadInfoFieldWithAccessors(int modifiers, CtClass addTo, CtField field, CtField.Initializer init) throws NotFoundException, CannotCompileException
   {
      String name = getFieldReadInfoFieldName(field.getName());
      TransformerCommon.addInfoField(instrumentor, FIELD_INFO_CLASS_NAME, name, modifiers, addTo, addInfoAsWeakReference(), init);
      return name;
   }
   
   protected String addFieldWriteInfoField(int modifiers, CtClass addTo, CtField field) throws NotFoundException, CannotCompileException
   {
      return addFieldWriteInfoField(modifiers, addTo, field, null);
   }
   
   /**
    * Adds a FieldInfo field to the passed in class
    */
   protected String addFieldWriteInfoField(int modifiers, CtClass addTo, CtField field, CtField.Initializer init) throws NotFoundException, CannotCompileException
   {
      String name = getFieldWriteInfoFieldName(field.getName());
      TransformerCommon.addInfoField(instrumentor, FIELD_INFO_CLASS_NAME, name, modifiers, addTo, addInfoAsWeakReference(), init);
      return name;
   }
   
   protected boolean addInfoAsWeakReference()
   {
      return true;
   }

   public static String getFieldReadInfoFieldName(String fieldName)
   {
      return "aop$FieldInfo_r_" + fieldName;
   }

   public static String getFieldWriteInfoFieldName(String fieldName)
   {
      return "aop$FieldInfo_w_" + fieldName;
   }

   public static String fieldRead(String fieldName)
   {
      return fieldName + "_r_" + ClassAdvisor.NOT_TRANSFORMABLE_SUFFIX;
   }

   public static String fieldWrite(String fieldName)
   {
      return fieldName + "_w_" + ClassAdvisor.NOT_TRANSFORMABLE_SUFFIX;
   }

   protected static String fieldInfoFromWeakReference(String localName, String fieldInfoName)
   {
      return TransformerCommon.infoFromWeakReference(FIELD_INFO_CLASS_NAME, localName, fieldInfoName);
   }
   
   protected int getStaticModifiers(CtField field)
   {
      int mod = Modifier.STATIC;
      if ((field.getModifiers() & Modifier.PUBLIC) != 0)
      {
         mod |= Modifier.PUBLIC;
      }
      else if ((field.getModifiers() & Modifier.PROTECTED) != 0)
      {
         mod |= Modifier.PROTECTED;
      }
      else if ((field.getModifiers() & Modifier.PRIVATE) != 0)
      {
         mod |= Modifier.PRIVATE;
      }
      else
      {
         mod |= Modifier.PUBLIC;
      }

      return mod;
   }

   /**
    * This function replaces internal field accesses with bytecode hooks into framework
    * todo this must do it for inherited protected/public fields as well
    *
    * @param clazz
    * @param field
    * @param doGet
    * @param doSet
    * @param index
    * @throws javassist.CannotCompileException
    *
    */
   protected abstract void replaceFieldAccessInternally(CtClass clazz, CtField field, boolean doGet, boolean doSet, int index) throws CannotCompileException;
   
   /**
    * 
    * Generate the wrapper place holders.
    * <p> PS: Removed from inside inner classes to avoid code repetition.</p>
    */
   protected void buildWrapperPlaceHolders(CtClass clazz, CtField field, boolean doGet, boolean doSet, int mod)
   throws NotFoundException, CannotCompileException
   {
      if (doGet)
      {
         buildReadWrapperPlaceHolder(clazz, field, fieldRead(field.getName()), mod);
      }
      if (doSet)
      {
         buildWriteWrapperPlaceHolder(clazz, field, fieldWrite(field.getName()), mod);
      }
   }

   /**
    * Builds the read wrapper place holder for preparation of <code>field</code> read
    * joinpoint.
    * @param clazz the <code>clazz</code> to add wrapper to
    * @param field the <code>field</code> whose read joinpoint wrapper will be generated.
    * @param wrapperName The name of the field wrapper to be generated
    * @param mod the modifiers of the generated wrapper.
    */
   protected CtMethod buildReadWrapperPlaceHolder(CtClass clazz, CtField field, String wrapperName, int mod)
   throws NotFoundException, CannotCompileException
   {
      AOPClassPool classPool = (AOPClassPool) instrumentor.getClassPool();

      String name = field.getName();
      CtClass ftype = field.getType();
      CtClass[] readParam = new CtClass[]{classPool.get("java.lang.Object")};
      
      //Doesn't matter what we put in as long as it compiles, 
      //body will be replaced when the field read gets wrapped
      String code = "{" + ftype.getName() +  " var = ";
      if (ftype.isPrimitive())
      {
         if (ftype == CtClass.booleanType)
         {
            code += false;
         }
         else if (ftype == CtClass.byteType)
         {
            code += "(byte)0";
         }
         else if (ftype == CtClass.charType)
         {
            code += "(char)0";
         }
         else if (ftype == CtClass.doubleType)
         {
            code += "0.0";
         }
         else if (ftype == CtClass.floatType)
         {
            code += "(float)0.0";
         }
         else if (ftype == CtClass.intType)
         {
            code += "0";
         }
         else if (ftype == CtClass.longType)
         {
            code += "(long)0";
         }
         else if (ftype == CtClass.shortType)
         {
            code += "(short)0";
         }  
      }
      else
      {
         code += "null";
      }
      code += "; return var;}";
      CtMethod rmethod = CtNewMethod.make(ftype, wrapperName, readParam, null, null, clazz);
      rmethod.setModifiers(mod);
      rmethod.setBody(code);
      clazz.addMethod(rmethod);
      
      return rmethod;
   }

   /**
    * Builds the write wrapper place holder for preparation of <code>field</code> read
    * joinpoint.
    * @param clazz the class the wrapper will be added to 
    * @param field the <code>field</code> whose write joinpoint wrapper will be generated.
    * @param wrapperName The name of the field wrapper to be generated
    * @param mod the modifiers of the generated wrapper.
    */
   protected CtMethod buildWriteWrapperPlaceHolder(CtClass clazz, CtField field, String wrapperName, int mod)
   throws NotFoundException, CannotCompileException
   {
      AOPClassPool classPool = (AOPClassPool) instrumentor.getClassPool();

      String name = field.getName();
      CtClass ftype = field.getType();

      // create field trapWrite memthod
      CtClass[] writeParam = new CtClass[2];
      writeParam[0] = classPool.get("java.lang.Object");
      writeParam[1] = ftype;

      //Doesn't matter what we put in as long as it compiles, 
      //body will be replaced when we the field write gets wrapped

      CtMethod wmethod = CtNewMethod.make(CtClass.voidType, wrapperName, writeParam, null, null, clazz);
      wmethod.setModifiers(mod);
      wmethod.setBody("{}");
      clazz.addMethod(wmethod);
      
      return wmethod;
   }

   /**
    * Returns the wrapper body of the <code>field</code> joinpoint.
    * @param clazz the class declaring <code>field</code>.
    * @param field the field whose joinpoint wrapper code will be generated.
    * @param get indicates if the wrapper is a field read wrapper or a field
    * write wrapper.
    * @param fieldIndex the index of <code>field</code>.
    * @return teh wrapper body code.
    */
   protected abstract String getWrapperBody(CtClass clazz, CtField field, boolean get, int fieldIndex) throws NotFoundException, CannotCompileException;

   protected abstract class FieldAccessExprEditor extends ExprEditor
   {
      CtClass clazz;
      CtField field;
      boolean doGet;
      boolean doSet;
      int fieldIndex;

      public FieldAccessExprEditor(CtClass clazz, CtField field, boolean doGet, boolean doSet, int index)
      {
         this.clazz = clazz;
         this.field = field;
         this.doGet = doGet;
         this.doSet = doSet;
         this.fieldIndex = index;
      }

      public void edit(FieldAccess fieldAccess) throws CannotCompileException
      {
         if (!fieldAccess.getClassName().equals(clazz.getName())) return;
         if (!fieldAccess.getFieldName().equals(field.getName())) return;
         if (calledByInvocationClass(fieldAccess))return;

         if (fieldAccess.isReader() && doGet)
         {
            replaceRead(fieldAccess);
         }
         if (fieldAccess.isWriter() && doSet)
         {
            replaceWrite(fieldAccess);
         }
      }

      private boolean calledByInvocationClass(FieldAccess fieldAccess)
      {
         try
         {
            return isInvocationClass(fieldAccess.where().getDeclaringClass());
         }
         catch (RuntimeException e)
         {
            //This means it is one of the access$xxx methods, 
            //This occurs when the field access is from one of the invocation classes, which we want to skip 
            return true;
         }
      }
      
      private boolean isInvocationClass(CtClass superClazz)
      {
         try
         {
            if (superClazz == null)
            {
               return false;
            }
            if (superClazz.getName().equals("java.lang.Object"))
            {
               return false;
            }
            if (superClazz.getName().equals("org.jboss.aop.joinpoint.Invocation"))
            {
               return true;
            }
            return (isInvocationClass(superClazz.getSuperclass()));
         }
         catch (NotFoundException e)
         {
            throw new RuntimeException(e.getMessage(), e);
         }
      }
      
      protected abstract void replaceRead(FieldAccess fieldAccess) throws CannotCompileException;
      protected abstract void replaceWrite(FieldAccess fieldAccess) throws CannotCompileException;
   }//End Inner class FieldAccessExprEditor


}
