package soot.jimple.toolkits.thread.synchronization;

/*-
 * #%L
 * Soot - a J*va Optimization Framework
 * %%
 * Copyright (C) 1997 - 2018 Raja Vallée-Rai and others
 * %%
 * 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.Iterator;
import java.util.List;

import soot.Hierarchy;
import soot.Local;
import soot.PointsToAnalysis;
import soot.RefLikeType;
import soot.RefType;
import soot.Scene;
import soot.SootClass;
import soot.jimple.toolkits.callgraph.ReachableMethods;
import soot.jimple.toolkits.pointer.CodeBlockRWSet;
import soot.jimple.toolkits.thread.mhp.MhpTester;

public class CriticalSectionInterferenceGraph {

  int nextGroup;
  List<CriticalSectionGroup> groups;

  List<CriticalSection> criticalSections;
  MhpTester mhp;
  PointsToAnalysis pta;
  boolean optionOneGlobalLock = false;
  boolean optionLeaveOriginalLocks = false;
  boolean optionIncludeEmptyPossibleEdges = false;

  public CriticalSectionInterferenceGraph(List<CriticalSection> criticalSections, MhpTester mhp, boolean optionOneGlobalLock,
      boolean optionLeaveOriginalLocks, boolean optionIncludeEmptyPossibleEdges) {
    this.criticalSections = criticalSections;
    this.mhp = mhp;
    this.pta = Scene.v().getPointsToAnalysis();
    this.optionOneGlobalLock = optionOneGlobalLock;
    this.optionLeaveOriginalLocks = optionLeaveOriginalLocks;
    this.optionIncludeEmptyPossibleEdges = optionIncludeEmptyPossibleEdges;

    calculateGroups();
  }

  public int groupCount() {
    return nextGroup;
  }

  public List<CriticalSectionGroup> groups() {
    return groups;
  }

  public void calculateGroups() {
    nextGroup = 1;
    groups = new ArrayList<CriticalSectionGroup>();
    groups.add(new CriticalSectionGroup(0)); // dummy group

    if (optionOneGlobalLock) // use one group for all transactions
    {
      CriticalSectionGroup onlyGroup = new CriticalSectionGroup(nextGroup);
      Iterator<CriticalSection> tnIt1 = criticalSections.iterator();
      while (tnIt1.hasNext()) {
        CriticalSection tn1 = tnIt1.next();
        onlyGroup.add(tn1);
      }
      nextGroup++;
      groups.add(onlyGroup);
    } else // calculate separate groups for transactions
    {
      Iterator<CriticalSection> tnIt1 = criticalSections.iterator();
      while (tnIt1.hasNext()) {
        CriticalSection tn1 = tnIt1.next();

        // if this transaction has somehow already been marked for deletion
        if (tn1.setNumber == -1) {
          continue;
        }

        // if this transaction is empty
        if (tn1.read.size() == 0 && tn1.write.size() == 0 && !optionLeaveOriginalLocks) {
          // this transaction has no effect except on locals... we don't need it!
          tn1.setNumber = -1; // AKA delete the transactional region (but don't really so long as we are using
          // the synchronized keyword in our language... because java guarantees memory
          // barriers at certain points in synchronized blocks)
        } else {
          Iterator<CriticalSection> tnIt2 = criticalSections.iterator();
          while (tnIt2.hasNext()) {
            CriticalSection tn2 = tnIt2.next();

            // check if this transactional region is going to be deleted
            if (tn2.setNumber == -1) {
              continue;
            }

            // check if they're already marked as having an interference
            // NOTE: this results in a sound grouping, but a badly
            // incomplete dependency graph. If the dependency
            // graph is to be analyzed, we cannot do this
            // if(tn1.setNumber > 0 && tn1.setNumber == tn2.setNumber)
            // continue;

            // check if these two transactions can't ever be in parallel
            if (!mayHappenInParallel(tn1, tn2)) {
              continue;
            }

            // check for RW or WW data dependencies.
            // or, for optionLeaveOriginalLocks, check type compatibility
            SootClass classOne = null;
            SootClass classTwo = null;
            boolean typeCompatible = false;
            boolean emptyEdge = false;
            if (tn1.origLock != null && tn2.origLock != null) {
              // Check if edge is empty
              if (tn1.origLock == null || tn2.origLock == null) {
                emptyEdge = true;
              } else if (!(tn1.origLock instanceof Local) || !(tn2.origLock instanceof Local)) {
                emptyEdge = !tn1.origLock.equals(tn2.origLock);
              } else {
                emptyEdge = !pta.reachingObjects((Local) tn1.origLock)
                    .hasNonEmptyIntersection(pta.reachingObjects((Local) tn2.origLock));
              }

              // Check if types are compatible
              RefLikeType typeOne = (RefLikeType) tn1.origLock.getType();
              RefLikeType typeTwo = (RefLikeType) tn2.origLock.getType();
              classOne = (typeOne instanceof RefType) ? ((RefType) typeOne).getSootClass() : null;
              classTwo = (typeTwo instanceof RefType) ? ((RefType) typeTwo).getSootClass() : null;
              if (classOne != null && classTwo != null) {
                Hierarchy h = Scene.v().getActiveHierarchy();
                if (classOne.isInterface()) {
                  if (classTwo.isInterface()) {
                    typeCompatible = h.getSubinterfacesOfIncluding(classOne).contains(classTwo)
                        || h.getSubinterfacesOfIncluding(classTwo).contains(classOne);
                  } else {
                    typeCompatible = h.getImplementersOf(classOne).contains(classTwo);
                  }
                } else {
                  if (classTwo.isInterface()) {
                    typeCompatible = h.getImplementersOf(classTwo).contains(classOne);
                  } else {
                    typeCompatible = (classOne != null
                        && Scene.v().getActiveHierarchy().getSubclassesOfIncluding(classOne).contains(classTwo)
                        || classTwo != null
                            && Scene.v().getActiveHierarchy().getSubclassesOfIncluding(classTwo).contains(classOne));
                  }
                }
              }
            }
            if ((!optionLeaveOriginalLocks && (tn1.write.hasNonEmptyIntersection(tn2.write)
                || tn1.write.hasNonEmptyIntersection(tn2.read) || tn1.read.hasNonEmptyIntersection(tn2.write)))
                || (optionLeaveOriginalLocks && typeCompatible && (optionIncludeEmptyPossibleEdges || !emptyEdge))) {
              // Determine the size of the intersection for GraphViz output
              CodeBlockRWSet rw = null;
              int size;
              if (optionLeaveOriginalLocks) {
                rw = new CodeBlockRWSet();
                size = emptyEdge ? 0 : 1;
              } else {
                rw = tn1.write.intersection(tn2.write);
                rw.union(tn1.write.intersection(tn2.read));
                rw.union(tn1.read.intersection(tn2.write));
                size = rw.size();
              }

              // Record this
              tn1.edges.add(new CriticalSectionDataDependency(tn2, size, rw));
              // Don't add opposite... all n^2 pairs will be visited separately

              if (size > 0) {
                // if tn1 already is in a group
                if (tn1.setNumber > 0) {
                  // if tn2 is NOT already in a group
                  if (tn2.setNumber == 0) {
                    tn1.group.add(tn2);
                  }
                  // if tn2 is already in a group
                  else if (tn2.setNumber > 0) {
                    if (tn1.setNumber != tn2.setNumber) // if they are equal, then they are already in the same group!
                    {
                      tn1.group.mergeGroups(tn2.group);
                    }
                  }
                }
                // if tn1 is NOT already in a group
                else if (tn1.setNumber == 0) {
                  // if tn2 is NOT already in a group
                  if (tn2.setNumber == 0) {
                    CriticalSectionGroup newGroup = new CriticalSectionGroup(nextGroup);
                    newGroup.add(tn1);
                    newGroup.add(tn2);
                    groups.add(newGroup);
                    nextGroup++;
                  }
                  // if tn2 is already in a group
                  else if (tn2.setNumber > 0) {
                    tn2.group.add(tn1);
                  }
                }
              }
            }
          }
          // If, after comparing to all other transactions, we have no group:
          if (tn1.setNumber == 0) {
            tn1.setNumber = -1; // delete transactional region
          }
        }
      }
    }
  }

  public boolean mayHappenInParallel(CriticalSection tn1, CriticalSection tn2) {
    if (mhp == null) {
      if (optionLeaveOriginalLocks) {
        return true;
      }
      ReachableMethods rm = Scene.v().getReachableMethods();
      if (!rm.contains(tn1.method) || !rm.contains(tn2.method)) {
        return false;
      }
      return true;
    }
    return mhp.mayHappenInParallel(tn1.method, tn2.method);
  }
}
