/*
 * 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.js.rhino.test;

import com.ibm.wala.cast.js.html.DefaultSourceExtractor;
import com.ibm.wala.cast.js.html.JSSourceExtractor;
import com.ibm.wala.cast.js.ipa.callgraph.JSCFABuilder;
import com.ibm.wala.cast.js.ipa.callgraph.JavaScriptFunctionDotCallTargetSelector;
import com.ibm.wala.cast.js.ipa.callgraph.RecursionCheckContextSelector;
import com.ibm.wala.cast.js.translator.CAstRhinoTranslatorFactory;
import com.ibm.wala.cast.js.util.JSCallGraphBuilderUtil;
import com.ibm.wala.cast.js.util.JSCallGraphBuilderUtil.CGBuilderType;
import com.ibm.wala.core.util.ProgressMaster;
import com.ibm.wala.core.util.io.FileProvider;
import com.ibm.wala.ipa.callgraph.CGNode;
import com.ibm.wala.ipa.callgraph.CallGraph;
import com.ibm.wala.ipa.callgraph.CallGraphBuilderCancelException;
import com.ibm.wala.ipa.callgraph.propagation.InstanceKey;
import com.ibm.wala.ipa.callgraph.propagation.PointerAnalysis;
import com.ibm.wala.util.MonitorUtil.IProgressMonitor;
import com.ibm.wala.util.NullProgressMonitor;
import com.ibm.wala.util.io.CommandLine;
import com.ibm.wala.util.io.FileUtil;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Properties;
import java.util.function.Supplier;
import org.junit.Assert;

/**
 * Utility class for building call graphs of HTML pages.
 *
 * @author mschaefer
 */
public class HTMLCGBuilder {
  public static final int DEFAULT_TIMEOUT = 120;

  /**
   * Simple struct-like type to hold results of call graph construction.
   *
   * @author mschaefer
   */
  public static class CGBuilderResult {
    /** time it took to build the call graph; {@code -1} if timeout occurred */
    public long construction_time;

    /** builder responsible for building the call graph */
    public JSCFABuilder builder;

    /** pointer analysis results; partial if {@link #construction_time} is {@code -1} */
    public PointerAnalysis<InstanceKey> pa;

    /** call graph; partial if {@link #construction_time} is {@code -1} */
    public CallGraph cg;
  }

  /**
   * Build a call graph for an HTML page, optionally with a timeout.
   *
   * @param src the HTML page to analyse, can either be a path to a local file or a URL
   * @param timeout analysis timeout in seconds, -1 means no timeout
   */
  public static CGBuilderResult buildHTMLCG(
      String src, int timeout, CGBuilderType builderType, Supplier<JSSourceExtractor> fExtractor) {
    CGBuilderResult res = new CGBuilderResult();
    URL url = null;
    try {
      url = toUrl(src);
    } catch (MalformedURLException e1) {
      Assert.fail("Could not find page to analyse: " + src);
    }
    com.ibm.wala.cast.js.ipa.callgraph.JSCallGraphUtil.setTranslatorFactory(
        new CAstRhinoTranslatorFactory());
    JSCFABuilder builder = null;
    try {
      builder = JSCallGraphBuilderUtil.makeHTMLCGBuilder(url, builderType, fExtractor);
      // TODO we need to find a better way to do this ContextSelector delegation;
      // the code below belongs somewhere else!!!
      // the bound of 4 is what is needed to pass our current framework tests
      builder.setContextSelector(new RecursionCheckContextSelector(builder.getContextSelector()));
      IProgressMonitor master =
          ProgressMaster.make(new NullProgressMonitor(), timeout * 1000, false);
      if (timeout > 0) {
        master.beginTask("runSolver", 1);
      }
      long start = System.currentTimeMillis();
      CallGraph cg =
          timeout > 0
              ? builder.makeCallGraph(builder.getOptions(), master)
              : builder.makeCallGraph(builder.getOptions());
      long end = System.currentTimeMillis();
      master.done();
      res.construction_time = (end - start);
      res.cg = cg;
      res.pa = builder.getPointerAnalysis();
      res.builder = builder;
      return res;
    } catch (CallGraphBuilderCancelException e) {
      res.construction_time = -1;
      res.cg = e.getPartialCallGraph();
      res.pa = e.getPartialPointerAnalysis();
      res.builder = builder;
      return res;
    } catch (Exception e) {
      throw new Error(e);
    }
  }

  private static URL toUrl(String src) throws MalformedURLException {
    // first try interpreting as local file name, if that doesn't work just
    // assume it's a URL
    try {
      File f = new FileProvider().getFileFromClassLoader(src, HTMLCGBuilder.class.getClassLoader());
      URL url = f.toURI().toURL();
      return url;
    } catch (FileNotFoundException fnfe) {
      return new URL(src);
    }
  }

  /**
   * Usage: HTMLCGBuilder -src path_to_html_file -timeout timeout_in_seconds -reachable
   * function_name timeout argument is optional and defaults to {@link #DEFAULT_TIMEOUT}. reachable
   * argument is optional. if provided, and some reachable function name contains function_name,
   * will print "REACHABLE"
   */
  public static void main(String[] args) throws IOException {
    Properties parsedArgs = CommandLine.parse(args);

    String src = parsedArgs.getProperty("src");
    if (src == null) {
      throw new IllegalArgumentException("-src argument is required");
    }

    // if src is a JS file, build trivial wrapper HTML file
    if (src.endsWith(".js")) {
      File tmpFile = File.createTempFile("HTMLCGBuilder", ".html");
      tmpFile.deleteOnExit();
      FileUtil.writeFile(
          tmpFile,
          "<html>"
              + "  <head>"
              + "    <title></title>"
              + "    <script src=\""
              + src
              + "\" type='text/javascript'></script>"
              + "  </head>"
              + "<body>"
              + "</body>"
              + "</html>");
      src = tmpFile.getAbsolutePath();
    }

    int timeout;
    if (parsedArgs.containsKey("timeout")) {
      timeout = Integer.parseInt(parsedArgs.getProperty("timeout"));
    } else {
      timeout = DEFAULT_TIMEOUT;
    }

    String reachableName = null;
    if (parsedArgs.containsKey("reachable")) {
      reachableName = parsedArgs.getProperty("reachable");
    }

    // suppress debug output
    JavaScriptFunctionDotCallTargetSelector.WARN_ABOUT_IMPRECISE_CALLGRAPH = false;

    // build call graph
    CGBuilderResult res =
        buildHTMLCG(src, timeout, CGBuilderType.ONE_CFA, DefaultSourceExtractor.factory);

    if (res.construction_time == -1) System.out.println("TIMED OUT");
    else
      System.out.println(
          "Call graph construction took " + res.construction_time / 1000.0 + " seconds");

    if (reachableName != null) {
      for (CGNode node : res.cg) {
        if (node.getMethod().getDeclaringClass().getName().toString().contains(reachableName)) {
          System.out.println("REACHABLE");
          break;
        }
      }
    }
  }
}
