package soot.dava.toolkits.base.renamer;

/*-
 * #%L
 * Soot - a J*va Optimization Framework
 * %%
 * Copyright (C) 2006 Nomair A. Naeem
 * %%
 * This program 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 program 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 General Lesser Public License for more details.
 * 
 * You should have received a copy of the GNU General Lesser Public
 * License along with this program.  If not, see
 * <http://www.gnu.org/licenses/lgpl-2.1.html>.
 * #L%
 */

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;

import soot.ArrayType;
import soot.Local;
import soot.RefLikeType;
import soot.SootClass;
import soot.SootField;
import soot.Type;
import soot.dava.internal.AST.ASTMethodNode;
import soot.util.Chain;

public class Renamer {
  public final boolean DEBUG = false;
  heuristicSet heuristics;

  List locals; // a list of locals in scope

  Chain fields; // a list of fields in scope

  ASTMethodNode methodNode;
  List<String> forLoopNames;

  HashMap<Local, Boolean> changedOrNot;// keeps track of which local was changed previously

  public Renamer(heuristicSet info, ASTMethodNode node) {
    heuristics = info;
    locals = null;
    methodNode = node;

    changedOrNot = new HashMap<Local, Boolean>();
    Iterator<Local> localIt = info.getLocalsIterator();
    while (localIt.hasNext()) {
      changedOrNot.put(localIt.next(), new Boolean(false));
    }

    forLoopNames = new ArrayList<String>();
    forLoopNames.add("i");
    forLoopNames.add("j");
    forLoopNames.add("k");
    forLoopNames.add("l");
  }

  /*
   * Add any naming heuristic as a separate method and invoke the method from this method.
   *
   * HOWEVER, NOTE that the order of naming really really matters
   */
  public void rename() {
    debug("rename", "Renaming started");

    // String args
    mainMethodArgument();

    // for(i=0;i<bla;i++)
    forLoopIndexing();

    // exceptions are named using first letter of each capital char in the class name
    exceptionNaming();

    // arrays get <type>Array
    arraysGetTypeArray();

    // if a local is assigned a field that name can be used since fields are conserved
    assignedFromAField();

    // check if a local is assigned the result of a new invocation
    newClassName();

    // check if a local is assigned after casting
    castedObject();

    // if nothing else give a reference the name of the class
    objectsGetClassName();

    // atleast remove the ugly dollar signs
    removeDollarSigns();
  }

  /*
   * if there is an array int[] x. then if no other heuristic matches give it the name intArray
   */
  private void arraysGetTypeArray() {
    Iterator<Local> it = heuristics.getLocalsIterator();
    while (it.hasNext()) {
      Local tempLocal = it.next();
      if (alreadyChanged(tempLocal)) {
        continue;
      }

      debug("arraysGetTypeArray", "checking " + tempLocal);

      Type type = tempLocal.getType();
      if (type instanceof ArrayType) {
        debug("arraysGetTypeArray", "Local:" + tempLocal + " is an Array Type: " + type.toString());
        String tempClassName = type.toString();
        // remember that a toString of an array gives you the square brackets
        if (tempClassName.indexOf('[') >= 0) {
          tempClassName = tempClassName.substring(0, tempClassName.indexOf('['));
        }

        // debug("arraysGetTypeArray","type of object is"+tempClassName);
        if (tempClassName.indexOf('.') != -1) {
          // contains a dot have to remove that
          tempClassName = tempClassName.substring(tempClassName.lastIndexOf('.') + 1);
        }

        String newName = tempClassName.toLowerCase();
        newName = newName + "Array";
        int count = 0;
        newName += count;
        count++;

        while (!isUniqueName(newName)) {
          newName = newName.substring(0, newName.length() - 1) + count;
          count++;
        }
        setName(tempLocal, newName);

      }
    }

  }

  /*
   * The method assigns any local whose name hasnt been changed yet to the name of the class type it belongs to
   */
  private void objectsGetClassName() {
    Iterator<Local> it = heuristics.getLocalsIterator();
    while (it.hasNext()) {
      Local tempLocal = it.next();
      if (alreadyChanged(tempLocal)) {
        continue;
      }

      debug("objectsGetClassName", "checking " + tempLocal);

      Type type = tempLocal.getType();
      if (type instanceof ArrayType) {
        // should have been handled by arraysGetTypeArray heuristic
        continue;
      }

      if (type instanceof RefLikeType) {
        debug("objectsGetClassName", "Local:" + tempLocal + " Type: " + type.toString());
        // debug("objectsGetClassName","getting array type"+type.getArrayType());
        String tempClassName = type.toString();
        // debug("objectsGetClassName","type of object is"+tempClassName);
        if (tempClassName.indexOf('.') != -1) {
          // contains a dot have to remove that
          tempClassName = tempClassName.substring(tempClassName.lastIndexOf('.') + 1);
        }

        String newName = tempClassName.toLowerCase();
        int count = 0;
        newName += count;
        count++;

        while (!isUniqueName(newName)) {
          newName = newName.substring(0, newName.length() - 1) + count;
          count++;
        }
        setName(tempLocal, newName);

      }
    }

  }

  /*
   * If a local is assigned the resullt of a cast expression temp = (List) object; then u can use list as the name...however
   * only if its always casted to the same object
   */
  private void castedObject() {
    debug("castedObject", "");

    Iterator<Local> it = heuristics.getLocalsIterator();
    while (it.hasNext()) {
      Local tempLocal = it.next();
      if (!alreadyChanged(tempLocal)) {
        debug("castedObject", "checking " + tempLocal);
        List<String> classes = heuristics.getCastStrings(tempLocal);

        Iterator<String> itClass = classes.iterator();
        String classNameToUse = null;
        while (itClass.hasNext()) {
          String tempClassName = itClass.next();
          if (tempClassName.indexOf('.') != -1) {
            // contains a dot have to remove that
            tempClassName = tempClassName.substring(tempClassName.lastIndexOf('.') + 1);
          }
          if (classNameToUse == null) {
            classNameToUse = tempClassName;
          } else if (!classNameToUse.equals(tempClassName)) {
            // different new assignment
            // cant use these classNames
            classNameToUse = null;
            break;
          }
        } // going through class names stored
        if (classNameToUse != null) {
          debug("castedObject", "found a classNametoUse through cast expr");
          /*
           * We should use this classNAme to assign to the local name We are guaranteed that all cast expressions use this
           * type
           */
          String newName = classNameToUse.toLowerCase();
          int count = 0;
          newName += count;
          count++;

          while (!isUniqueName(newName)) {
            newName = newName.substring(0, newName.length() - 1) + count;
            count++;
          }
          setName(tempLocal, newName);
        }
      } // not already changed
    } // going through locals
  }

  /*
   * See if any local was initialized using the new operator That name might give us a hint to a name to use for the local
   */
  private void newClassName() {

    debug("newClassName", "");
    // check if CLASSNAME is set
    // that would mean there was new className invocation
    Iterator<Local> it = heuristics.getLocalsIterator();
    while (it.hasNext()) {
      Local tempLocal = it.next();
      if (!alreadyChanged(tempLocal)) {
        debug("newClassName", "checking " + tempLocal);
        List<String> classes = heuristics.getObjectClassName(tempLocal);
        Iterator<String> itClass = classes.iterator();
        String classNameToUse = null;
        while (itClass.hasNext()) {
          String tempClassName = itClass.next();
          if (tempClassName.indexOf('.') != -1) {
            // contains a dot have to remove that
            tempClassName = tempClassName.substring(tempClassName.lastIndexOf('.') + 1);
          }
          if (classNameToUse == null) {
            classNameToUse = tempClassName;
          } else if (!classNameToUse.equals(tempClassName)) {
            // different new assignment
            // cant use these classNames
            classNameToUse = null;
            break;
          }
        } // going through class names stored
        if (classNameToUse != null) {
          debug("newClassName", "found a classNametoUse");
          /*
           * We should use this classNAme to assign to the local name We are guaranteed that all new invocations use this
           * class name
           */
          String newName = classNameToUse.toLowerCase();
          int count = 0;
          newName += count;
          count++;

          while (!isUniqueName(newName)) {
            newName = newName.substring(0, newName.length() - 1) + count;
            count++;
          }
          setName(tempLocal, newName);
        }
      } // not already changed
    } // going through locals

  }

  /*
   * If a local is assigned from a field (static or non staitc) we can use that name to assign a some what better name for
   * the local
   *
   * If multiple fields are assigned then it might be a better idea to not do anything since that will only confuse the user
   *
   */
  private void assignedFromAField() {
    Iterator<Local> it = heuristics.getLocalsIterator();
    while (it.hasNext()) {
      Local tempLocal = it.next();
      if (!alreadyChanged(tempLocal)) {
        debug("assignedFromField", "checking " + tempLocal);
        List<String> fieldNames = heuristics.getFieldName(tempLocal);
        if (fieldNames.size() > 1) {
          // more than one fields were assigned to this var
          continue;
        } else if (fieldNames.size() == 1) {
          // only one field was used
          String fieldName = fieldNames.get(0);

          // okkay to use the name of the field if its not in scope
          // eg it was some other classes field
          int count = 0;
          while (!isUniqueName(fieldName)) {
            if (count == 0) {
              fieldName = fieldName + count;
            } else {
              fieldName = fieldName.substring(0, fieldName.length() - 1) + count;
            }
            count++;
          }

          setName(tempLocal, fieldName);
        } // only one field assigned to this local
      } // not changed
    } // going through locals
  }

  /*
   * If we cant come up with any better name atleast we should remove the $ signs
   */
  private void removeDollarSigns() {
    Iterator<Local> it = heuristics.getLocalsIterator();
    while (it.hasNext()) {
      Local tempLocal = it.next();
      String currentName = tempLocal.getName();
      int dollarIndex = currentName.indexOf('$');
      if (dollarIndex == 0) {
        // meaning there is a $ sign in the first location
        String newName = currentName.substring(1, currentName.length());

        if (isUniqueName(newName)) {
          setName(tempLocal, newName);
          // System.out.println("Changed "+currentName+" to "+newName);
          // tempLocal.setName(newName);
        }
      }
    }
  }

  /*
   *
   */
  private void exceptionNaming() {
    Iterator<Local> it = heuristics.getLocalsIterator();
    while (it.hasNext()) {
      Local tempLocal = it.next();
      Type localType = tempLocal.getType();
      String typeString = localType.toString();
      if (typeString.indexOf("Exception") >= 0) {
        // the string xception occurs in this type
        debug("exceptionNaming", "Type is an exception" + tempLocal);

        // make a new name of all caps characters in typeString
        String newName = "";
        for (int i = 0; i < typeString.length(); i++) {
          char character = typeString.charAt(i);
          if (Character.isUpperCase(character)) {
            newName += Character.toLowerCase(character);
          }
        }
        int count = 0;
        if (!isUniqueName(newName)) {
          count++;
          while (!isUniqueName(newName + count)) {
            count++;
          }
        }
        if (count != 0) {
          newName = newName + count;
        }

        setName(tempLocal, newName);
      }
    }
  }

  /*
   * Probably one of the most common programming idioms for loop indexes are often i j k l
   */
  private void forLoopIndexing() {
    Iterator<Local> it = heuristics.getLocalsIterator();
    while (it.hasNext()) {
      Local tempLocal = it.next();
      debug("foeLoopIndexing", "Checking local" + tempLocal.getName());
      if (heuristics.getHeuristic(tempLocal, infoGatheringAnalysis.FORLOOPUPDATE)) {
        // this local variable is the main argument
        // will like to set it to args if no one has an objection
        int count = -1;

        String newName;

        do {
          count++;
          if (count >= forLoopNames.size()) {
            newName = null;
            break;
          }
          newName = (String) forLoopNames.get(count);
        } while (!isUniqueName(newName));

        if (newName != null) {
          setName(tempLocal, newName);
        }
      }
    }
  }

  /*
   * A simple heuristic which sets the mainMethodArgument's name to args
   */
  private void mainMethodArgument() {
    Iterator<Local> it = heuristics.getLocalsIterator();
    while (it.hasNext()) {
      Local tempLocal = it.next();
      if (heuristics.getHeuristic(tempLocal, infoGatheringAnalysis.MAINARG)) {

        // this local variable is the main argument
        // will like to set it to args if no one has an objection
        String newName = "args";
        int count = 0;
        while (!isUniqueName(newName)) {
          if (count == 0) {
            newName = newName + count;
          } else {
            newName = newName.substring(0, newName.length() - 1) + count;
          }

          count++;
        }
        setName(tempLocal, newName);
        // there cant be a same local with this heuristic set so just return
        return;
      }
    }

  }

  /*
   * In order to make sure that some previous heuristic which is usually a STRONGER heuristic has not already changed the
   * name we use this method which checks for past name changes and only changes the name if the name hasnt been changed
   * previously
   */
  private void setName(Local var, String newName) {

    Object truthValue = changedOrNot.get(var);

    // if it wasnt in there add it
    if (truthValue == null) {
      changedOrNot.put(var, new Boolean(false));
    } else {
      if (((Boolean) truthValue).booleanValue()) {
        // already changed just return
        debug("setName", "Var: " + var + " had already been renamed");
        return;
      }
    }
    // will only get here if the var had not been changed

    debug("setName", "Changed " + var.getName() + " to " + newName);
    var.setName(newName);
    changedOrNot.put(var, new Boolean(true));
  }

  /*
   * Check if a local has already been changed
   *
   * @param local to check
   *
   * @return true if already changed otherwise false
   */
  private boolean alreadyChanged(Local var) {
    Object truthValue = changedOrNot.get(var);

    // if it wasnt in there add it
    if (truthValue == null) {
      changedOrNot.put(var, new Boolean(false));
      return false;
    } else {
      if (((Boolean) truthValue).booleanValue()) {
        // already changed just return
        debug("alreadyChanged", "Var: " + var + " had already been renamed");
        return true;
      } else {
        return false;
      }
    }
  }

  /*
   * Should return true if the name is unique
   */
  private boolean isUniqueName(String name) {
    Iterator it = getScopedLocals();
    // check that none of the locals uses this name
    while (it.hasNext()) {
      Local tempLocal = (Local) it.next();
      if (tempLocal.getName().equals(name)) {
        debug("isUniqueName", "New Name " + name + " is not unique (matches some local)..changing");
        return false;
      } else {
        debug("isUniqueName", "New Name " + name + " is different from local " + tempLocal.getName());
      }
    }

    it = getScopedFields();
    // check that none of the fields uses this name
    while (it.hasNext()) {
      SootField tempField = (SootField) it.next();
      if (tempField.getName().equals(name)) {
        debug("isUniqueName", "New Name " + name + " is not unique (matches field)..changing");
        return false;
      } else {
        debug("isUniqueName", "New Name " + name + " is different from field " + tempField.getName());
      }
    }
    return true;
  }

  /*
   * Method is responsible to find all names with which there could be a potential clash The variables are: all the fields of
   * this class and all the locals defined in this method
   */
  private Iterator getScopedFields() {
    // get the fields for this class and store them
    SootClass sootClass = methodNode.getDavaBody().getMethod().getDeclaringClass();
    fields = sootClass.getFields();

    return fields.iterator();
  }

  /*
   * Method is responsible to find all variable names with which there could be a potential clash The variables are: all the
   * fields of this class and all the locals defined in this method
   */
  private Iterator getScopedLocals() {
    Iterator<Local> it = heuristics.getLocalsIterator();

    locals = new ArrayList();
    while (it.hasNext()) {
      locals.add(it.next());
    }

    return locals.iterator();

  }

  public void debug(String methodName, String debug) {

    if (DEBUG) {
      System.out.println(methodName + "    DEBUG: " + debug);
    }
  }

}
