/*
 * (C) Copyright 2010-2015 SAP SE.
 *
 * 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
 *
 */

package eu.aniketos.dasca.crosslanguage.util

import com.ibm.wala.cast.ir.ssa.AstIRFactory
import com.ibm.wala.classLoader.Language
import com.ibm.wala.ipa.callgraph.CGNode
import com.ibm.wala.ssa.SSAInstruction
import com.ibm.wala.classLoader.CallSiteReference
import scala.collection.mutable.ListBuffer
import java.io.File
import com.ibm.wala.cast.js.html.IncludedPosition
import com.typesafe.scalalogging.Logger
import java.util.concurrent.TimeUnit
import com.ibm.wala.classLoader.IClass
import scala.collection.mutable.LinkedHashSet
import eu.aniketos.dasca.crosslanguage.builder.CrossBuilderOption
import eu.aniketos.dasca.crosslanguage.builder.FilterJavaCallSites
import eu.aniketos.dasca.crosslanguage.builder.MockCordovaExec
import eu.aniketos.dasca.crosslanguage.builder.ReplacePluginDefinesAndRequires
import eu.aniketos.dasca.crosslanguage.builder.FilterJSFrameworks
import eu.aniketos.dasca.crosslanguage.builder.PreciseJS
import eu.aniketos.dasca.crosslanguage.builder.RunBuildersInParallel
import com.ibm.wala.classLoader.IMethod

object Util {
  val cachedDalvikLines = Map[(CGNode, SSAInstruction), Int]()

  def derivesFromCordovaPlugin(cls: IClass): Boolean = cls != null &&
    (cls.getName.toString == "Lorg/apache/cordova/CordovaPlugin" || derivesFromCordovaPlugin(cls.getSuperclass))

  def getJavaSourceInfo(node: CGNode, inst: SSAInstruction) = {
    val path = node.getMethod.getReference.getDeclaringClass.getName.toString().substring(1)
    val className = path.substring(path.lastIndexOf('/') + 1)
    val method = node.getMethod.getName.toString()
    val line = node.getMethod.getLineNumber(inst.iindex)
    (line, path, className, method)
  }

  def hasSomeParentWithName(name: String, file: File): Boolean = {
    val parent = file.getParentFile
    parent != null && (parent.getName == name || hasSomeParentWithName(name, parent))
  }

  val JavaScriptPathRegex = """.+/assets/(.+)""".r
  def getJavaScriptSourceInfo(ir: AstIRFactory[IMethod]#AstIR, inst: SSAInstruction) = {
    val sourcePos = ir.getMethod.getSourcePosition(inst.iindex) match {
      case iPos: IncludedPosition => iPos.getIncludePosition
      case sp => sp
    }
    val relPath = sourcePos.getURL.getFile match {
      case JavaScriptPathRegex(p) => p
      case _ => sourcePos.getURL.toString()
    }
    val (line, col, start, end) = FileMappingStore.map.find(_._1.toURI().toURL() == sourcePos.getURL).map(_._2) match {
      case Some(rec) => rec.translate(sourcePos)
      case None => (sourcePos.getFirstLine, sourcePos.getFirstCol, sourcePos.getFirstOffset, sourcePos.getLastOffset)
    }
    (line, col, start, end, relPath)
  }

  def prettyPrintInstruction(node: CGNode, inst: SSAInstruction): String = {
    if (isJavaNode(node)) {
      val (line, path, className, method) = getJavaSourceInfo(node, inst)
      s"$className:$line, Method: $method, Path: $path"
    } else {
      node.getIR match {
        case ir:AstIRFactory[IMethod]#AstIR => {
          val (line, col, start, end, relPath:String) = getJavaScriptSourceInfo(ir, inst)
          val fileName = relPath.substring(relPath.lastIndexOf('/') + 1)
          s"$fileName:$line:$col ($start -> $end), Path: $relPath"
        }
        case _ => s"$inst, $node"
      }
    }
  }

  def argsToOptions(arg: String) = if (arg.charAt(0) == '-') {
      val lb = new ListBuffer[CrossBuilderOption]
      if (arg.contains('j')) lb += FilterJavaCallSites
      if (arg.contains('m')) lb += MockCordovaExec
      if (arg.contains('r')) lb += ReplacePluginDefinesAndRequires
      if (arg.contains('f')) lb += FilterJSFrameworks
      if (arg.contains('p')) lb += PreciseJS
      if (arg.contains('x')) lb += RunBuildersInParallel
      lb.toList
    } else List()

  def prettyPrintCrossTargets(targets: Map[(CGNode, CallSiteReference), LinkedHashSet[CGNode]]): List[String] = {
    val (javaJsCalls, jsJavaCalls) = targets.partition(p => Util.isJavaNode(p._1._1))

    val lb = new ListBuffer[String]()
    lb += s"Java -> JavaScript (${javaJsCalls.values.map({ set => set.size }).sum} calls)"
    lb ++= prettyPrintCrossCalls(javaJsCalls)
    lb += s"JavaScript -> Java (${jsJavaCalls.values.map({ set => set.size }).sum} calls)"
    lb ++= prettyPrintCrossCalls(jsJavaCalls)
    lb.toList
  }

  private def prettyPrintCrossCalls(targets: Map[(CGNode, CallSiteReference), LinkedHashSet[CGNode]]): List[String] = {
    val lb = new ListBuffer[String]()
    for ((node, csr) <- targets.keys) {
      lb += "\t" + prettyPrintInstruction(node, node.getIR.getCalls(csr)(0))
      for (target <- targets.get((node, csr)).get) {
        lb += "\t\t-> " + prettyPrintInstruction(target, target.getIR.getInstructions.find({ i => i != null }).get)
      }
    }
    lb.toList
  }

  def walkTree(file: File): Iterable[File] = {
    val children = new Iterable[File] {
      def iterator = if (file.isDirectory) file.listFiles.iterator else Iterator.empty
    }
    Seq(file) ++: children.flatMap(walkTree(_))
  }

  def isJavaNode(node: CGNode): Boolean = node.getMethod.getDeclaringClass.getClassLoader.getLanguage == Language.JAVA

  def time[R](label: String, block: => R)(implicit logger: Logger): R = {
    val start = System.nanoTime()
    val result = block
    logger.info(s"$label took %.3fs".format(TimeUnit.MILLISECONDS.convert(System.nanoTime() - start, TimeUnit.NANOSECONDS) / 1000.0))
    result
  }
}
