/**
 * This file is part of Eclipse Steady.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * SPDX-License-Identifier: Apache-2.0
 * SPDX-FileCopyrightText: Copyright (c) 2018-2020 SAP SE or an SAP affiliate company and Eclipse Steady contributors
 */
package org.eclipse.steady.cg;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.logging.log4j.Logger;
import org.eclipse.steady.shared.util.StopWatch;

import com.ibm.wala.util.graph.Graph;

/**
 * <p>CallgraphReachableSearch class.</p>
 */
public class CallgraphReachableSearch implements Runnable {

  private static final Logger log = org.apache.logging.log4j.LogManager.getLogger();

  private Callgraph graph = null;

  private boolean findTouchPoints = true;

  private Set<org.eclipse.steady.shared.json.model.ConstructId> appConstructs = null;

  private int min = -1;

  private int max = -1;

  /**
   * Library touch points (stored as a set of linked lists with two elements each) per library (SHA1).
   */
  private Map<String, Set<List<NodeMetaInformation>>> touchPoints =
      new HashMap<String, Set<List<NodeMetaInformation>>>();

  /**
   * Reachable constructs per library (SHA1).
   */
  private Map<String, Set<NodeMetaInformation>> reachableConstructs =
      new HashMap<String, Set<NodeMetaInformation>>();

  /**
   * <p>setCallgraph.</p>
   *
   * @param _g a {@link org.eclipse.steady.cg.Callgraph} object.
   * @return a {@link org.eclipse.steady.cg.CallgraphReachableSearch} object.
   */
  public CallgraphReachableSearch setCallgraph(Callgraph _g) {
    this.graph = _g;
    return this;
  }

  /**
   * <p>Setter for the field <code>findTouchPoints</code>.</p>
   *
   * @param _ft a boolean.
   * @return a {@link org.eclipse.steady.cg.CallgraphReachableSearch} object.
   */
  public CallgraphReachableSearch setFindTouchPoints(boolean _ft) {
    this.findTouchPoints = _ft;
    return this;
  }

  /**
   * <p>setMinMax.</p>
   *
   * @param _min a int.
   * @param _max a int.
   * @return a {@link org.eclipse.steady.cg.CallgraphReachableSearch} object.
   */
  public CallgraphReachableSearch setMinMax(int _min, int _max) {
    this.min = _min;
    this.max = _max;
    return this;
  }

  /**
   * <p>Setter for the field <code>appConstructs</code>.</p>
   *
   * @param _app_constructs a {@link java.util.Set} object.
   * @return a {@link org.eclipse.steady.cg.CallgraphReachableSearch} object.
   */
  public CallgraphReachableSearch setAppConstructs(
      Set<org.eclipse.steady.shared.json.model.ConstructId> _app_constructs) {
    this.appConstructs = _app_constructs;
    return this;
  }

  /**
   * Adds a given node to the set of reachable constructs for a given library (dependency).
   * @param _node
   */
  private void addReachableNode(NodeMetaInformation _node) {
    final String sha1 = _node.getArchiveId();
    if (sha1 != null) {
      if (!this.reachableConstructs.containsKey(sha1)) {
        this.reachableConstructs.put(sha1, new HashSet<NodeMetaInformation>());
      }
      final Set<NodeMetaInformation> nodes = this.reachableConstructs.get(sha1);
      nodes.add(_node);
    }
  }

  /**
   * Adds a given node to the set of touch points for a given library (dependency).
   * @param _node
   */
  private void addTouchPoint(NodeMetaInformation _from, NodeMetaInformation _to) {
    final String sha1 = _to.getArchiveId();
    if (sha1 != null) {
      if (!this.touchPoints.containsKey(sha1)) {
        this.touchPoints.put(sha1, new HashSet<List<NodeMetaInformation>>());
      }
      final Set<List<NodeMetaInformation>> touch_points = this.touchPoints.get(sha1);
      final List<NodeMetaInformation> touch_point = new ArrayList<NodeMetaInformation>();
      touch_point.add(_from);
      touch_point.add(_to);
      touch_points.add(touch_point);
    }
  }

  /** {@inheritDoc} */
  @Override
  public void run() {
    if (this.graph != null && this.graph.getGraph() != null) {
      final StopWatch sw =
          new StopWatch(
                  "Collect touch points and reachable constructs per library, nodes ["
                      + this.min
                      + " - "
                      + this.max
                      + "]")
              .setTotal(max - min)
              .start();
      try {
        // Loop over all nodes
        final Graph<Integer> wala_graph = this.graph.getGraph();
        final Iterator<Integer> wala_graph_iterator = wala_graph.iterator();

        if (wala_graph_iterator == null) {
          log.error("No iterator for callgraph [" + wala_graph + "]");
        } else {
          // Loop variables
          Integer successor_node = null;
          Iterator<Integer> successor_nodes_iterator = null;
          NodeMetaInformation current_node_metainf = null, successor_node_metainf = null;
          String current_node_qname = null;

          for (int current_node = this.min; current_node < this.max; current_node++) {
            try {
              // Current node and its meta information
              current_node_metainf = this.graph.getInformationForId(current_node);
              if (current_node_metainf != null) {
                current_node_qname = current_node_metainf.getConstructId().getQname();

                // Current node is part of library --> reachable constructs
                if (MethodNameFilter.getInstance()
                    .isLibraryMethod(this.appConstructs, current_node_qname)) {
                  if (!MethodNameFilter.getInstance().isBlackListed(current_node_qname))
                    this.addReachableNode(current_node_metainf);
                }
                // Current node is part of app --> touch points
                else {
                  // Collection enabled?
                  if (this.findTouchPoints) {
                    // Loop all successors of the current node
                    successor_nodes_iterator = wala_graph.getSuccNodes(current_node);
                    while (successor_nodes_iterator.hasNext()) {

                      // Successor node of the current node
                      successor_node = successor_nodes_iterator.next();
                      successor_node_metainf = this.graph.getInformationForId(successor_node);

                      final String succ_node_qname =
                          successor_node_metainf.getConstructId().getQname();
                      if (MethodNameFilter.getInstance()
                              .isLibraryMethod(this.appConstructs, succ_node_qname)
                          && !MethodNameFilter.getInstance().isBlackListed(succ_node_qname)) {
                        // current_node_metainf.addToList(successor_node_metainf);
                        this.addTouchPoint(current_node_metainf, successor_node_metainf);
                      }
                    }
                  }
                }
              }
              sw.progress();

              // TODO Davide: Also collect touchpoints from Library to Application (L2A)
            } catch (Exception e) {
              log.error(
                  e.getClass().getSimpleName()
                      + " occured when looping callgraph node "
                      + current_node_metainf
                      + ": "
                      + e.getMessage());
            }
          }
          sw.stop();
        }
      } catch (NullPointerException npe) {
        sw.stop(npe);
        log.error(npe.getClass().getSimpleName() + " occured when looping callgraph", npe);
      } catch (Exception e) {
        sw.stop(e);
        log.error(
            e.getClass().getSimpleName() + " occured when looping callgraph: " + e.getMessage());
      }
    }
  }

  /**
   * <p>Getter for the field <code>touchPoints</code>.</p>
   *
   * @return a {@link java.util.Map} object.
   */
  public Map<String, Set<List<NodeMetaInformation>>> getTouchPoints() {
    return touchPoints;
  }

  /**
   * <p>Getter for the field <code>reachableConstructs</code>.</p>
   *
   * @return a {@link java.util.Map} object.
   */
  public Map<String, Set<NodeMetaInformation>> getReachableConstructs() {
    return reachableConstructs;
  }
}
