package scala.tools.nsc.doc
package model
package diagram

import model._
import java.util.regex.{Pattern, Matcher}
import scala.util.matching.Regex

/**
 *  This trait takes care of parsing @{inheritance, content}Diagram annotations
 *
 *  @author Damien Obrist
 *  @author Vlad Ureche
 */
trait DiagramDirectiveParser {
  this: ModelFactory with DiagramFactory with CommentFactory with TreeFactory =>

  import this.global.definitions.AnyRefClass

  ///// DIAGRAM FILTERS //////////////////////////////////////////////////////////////////////////////////////////////

  /**
   *  The DiagramFilter trait directs the diagram engine about the way the diagram should be displayed
   *
   *  Vlad: There's an explanation I owe to people using diagrams and not finding a way to hide a specific class from
   *  all diagrams at once. So why did I choose to allow you to only control the diagrams at class level? So, the
   *  reason is you would break the separate scaladoc compilation:
   *  If you have an "@diagram hideMyClass" annotation in class A and you run scaladoc on it along with its subclass B
   *  A will not appear in B's diagram. But if you scaladoc only on B, A's comment will not be parsed and the
   *  instructions to hide class A from all diagrams will not be available. Thus I prefer to force you to control the
   *  diagrams of each class locally. The problem does not appear with scalac, as scalac stores all its necessary
   *  information (like scala signatures) serialized in the .class file. But we couldn't store doc comments in the class
   *  file, could we? (Turns out we could, but that's another story)
   *
   *  Any flaming for this decision should go to scala-internals@googlegroups.com
   */
  trait DiagramFilter {
    /** A flag to hide the diagram completely */
    def hideDiagram: Boolean
    /** Hide incoming implicit conversions (for type hierarchy diagrams) */
    def hideIncomingImplicits: Boolean
    /** Hide outgoing implicit conversions (for type hierarchy diagrams) */
    def hideOutgoingImplicits: Boolean
    /** Hide superclasses (for type hierarchy diagrams) */
    def hideSuperclasses: Boolean
    /** Hide subclasses (for type hierarchy diagrams) */
    def hideSubclasses: Boolean
    /** Show related classes from other objects/traits/packages (for content diagrams) */
    def hideInheritedNodes: Boolean
    /** Hide a node from the diagram */
    def hideNode(clazz: Node): Boolean
    /** Hide an edge from the diagram */
    def hideEdge(clazz1: Node, clazz2: Node): Boolean
  }

  /** Main entry point into this trait: generate the filter for inheritance diagrams */
  def makeInheritanceDiagramFilter(template: DocTemplateImpl): DiagramFilter = {

    val defaultFilter =
      if (template.isClass || template.isTrait || template.sym == AnyRefClass)
        FullDiagram
      else
        NoDiagramAtAll

    if (template.comment.isDefined)
      makeDiagramFilter(template, template.comment.get.inheritDiagram, defaultFilter, isInheritanceDiagram = true)
    else
      defaultFilter
  }

  /** Main entry point into this trait: generate the filter for content diagrams */
  def makeContentDiagramFilter(template: DocTemplateImpl): DiagramFilter = {
    val defaultFilter = if (template.isPackage || template.isObject) FullDiagram else NoDiagramAtAll
    if (template.comment.isDefined)
      makeDiagramFilter(template, template.comment.get.contentDiagram, defaultFilter, isInheritanceDiagram = false)
    else
      defaultFilter
  }

  protected var tFilter = 0l
  protected var tModel = 0l

  /** Show the entire diagram, no filtering */
  case object FullDiagram extends DiagramFilter {
    val hideDiagram: Boolean = false
    val hideIncomingImplicits: Boolean = false
    val hideOutgoingImplicits: Boolean = false
    val hideSuperclasses: Boolean = false
    val hideSubclasses: Boolean = false
    val hideInheritedNodes: Boolean = false
    def hideNode(clazz: Node): Boolean = false
    def hideEdge(clazz1: Node, clazz2: Node): Boolean = false
  }

  /** Hide the diagram completely, no need for special filtering */
  case object NoDiagramAtAll extends DiagramFilter {
    val hideDiagram: Boolean = true
    val hideIncomingImplicits: Boolean = true
    val hideOutgoingImplicits: Boolean = true
    val hideSuperclasses: Boolean = true
    val hideSubclasses: Boolean = true
    val hideInheritedNodes: Boolean = true
    def hideNode(clazz: Node): Boolean = true
    def hideEdge(clazz1: Node, clazz2: Node): Boolean = true
  }

  /** The AnnotationDiagramFilter trait directs the diagram engine according to an annotation
   *  TODO: Should document the annotation, for now see parseDiagramAnnotation in ModelFactory.scala */
  case class AnnotationDiagramFilter(hideDiagram: Boolean,
                                             hideIncomingImplicits: Boolean,
                                             hideOutgoingImplicits: Boolean,
                                             hideSuperclasses: Boolean,
                                             hideSubclasses: Boolean,
                                             hideInheritedNodes: Boolean,
                                             hideNodesFilter: List[Pattern],
                                             hideEdgesFilter: List[(Pattern, Pattern)]) extends DiagramFilter {

    private[this] def getName(n: Node): String =
      if (n.tpl.isDefined)
        n.tpl.get.qualifiedName
      else
        n.name

    def hideNode(clazz: Node): Boolean = {
      val qualifiedName = getName(clazz)
      for (hideFilter <- hideNodesFilter)
        if (hideFilter.matcher(qualifiedName).matches) {
          // println(hideFilter + ".matcher(" + qualifiedName + ").matches = " + hideFilter.matcher(qualifiedName).matches)
          return true
        }
      false
    }

    def hideEdge(clazz1: Node, clazz2: Node): Boolean = {
      val clazz1Name = getName(clazz1)
      val clazz2Name = getName(clazz2)
      for ((clazz1Filter, clazz2Filter) <- hideEdgesFilter) {
        if (clazz1Filter.matcher(clazz1Name).matches &&
            clazz2Filter.matcher(clazz2Name).matches) {
          // println(clazz1Filter + ".matcher(" + clazz1Name + ").matches = " + clazz1Filter.matcher(clazz1Name).matches)
          // println(clazz2Filter + ".matcher(" + clazz2Name + ").matches = " + clazz2Filter.matcher(clazz2Name).matches)
          return true
        }
      }
      false
    }
  }

  // TODO: This could certainly be improved -- right now the only regex is *, but there's no way to match a single identifier
  private val NodeSpecRegex = "\\\"[A-Za-z\\*][A-Za-z\\.\\*]*\\\""
  private val NodeSpecPattern = Pattern.compile(NodeSpecRegex)
  private val EdgeSpecRegex = "\\(" + NodeSpecRegex + "\\s*\\->\\s*" + NodeSpecRegex + "\\)"
  // And the composed regexes:
  private val HideNodesRegex = new Regex("^hideNodes(\\s*" + NodeSpecRegex + ")+$")
  private val HideEdgesRegex = new Regex("^hideEdges(\\s*" + EdgeSpecRegex + ")+$")

  private def makeDiagramFilter(template: DocTemplateImpl,
                                directives: List[String],
                                defaultFilter: DiagramFilter,
                                isInheritanceDiagram: Boolean): DiagramFilter = directives match {

    // if there are no specific diagram directives, return the default filter (either FullDiagram or NoDiagramAtAll)
    case Nil =>
      defaultFilter

    // compute the exact filters. By including the annotation, the diagram is autmatically added
    case _ =>
      tFilter -= System.currentTimeMillis
      var hideDiagram0: Boolean = false
      var hideIncomingImplicits0: Boolean = false
      var hideOutgoingImplicits0: Boolean = false
      var hideSuperclasses0: Boolean = false
      var hideSubclasses0: Boolean = false
      var hideInheritedNodes0: Boolean = false
      var hideNodesFilter0: List[Pattern] = Nil
      var hideEdgesFilter0: List[(Pattern, Pattern)] = Nil

      def warning(message: String) = {
        // we need the position from the package object (well, ideally its comment, but yeah ...)
        val sym = if (template.sym.isPackage) template.sym.info.member(global.nme.PACKAGE) else template.sym
        assert((sym != global.NoSymbol) || (sym == global.rootMirror.RootPackage))
        global.reporter.warning(sym.pos, message)
      }

      def preparePattern(className: String) =
        "^" + className.stripPrefix("\"").stripSuffix("\"").replaceAll("\\.", "\\\\.").replaceAll("\\*", ".*") + "$"

      // separate entries:
      val entries = directives.foldRight("")(_ + " " + _).split(",").map(_.trim)
      for (entry <- entries)
        entry match {
          case "hideDiagram" =>
              hideDiagram0 = true
          case "hideIncomingImplicits" if isInheritanceDiagram =>
              hideIncomingImplicits0 = true
          case "hideOutgoingImplicits" if isInheritanceDiagram  =>
              hideOutgoingImplicits0 = true
          case "hideSuperclasses" if isInheritanceDiagram =>
              hideSuperclasses0 = true
          case "hideSubclasses" if isInheritanceDiagram =>
              hideSubclasses0 = true
          case "hideInheritedNodes" if !isInheritanceDiagram =>
              hideInheritedNodes0 = true
          case HideNodesRegex(last) =>
            val matcher = NodeSpecPattern.matcher(entry)
            while (matcher.find()) {
              val classPattern = Pattern.compile(preparePattern(matcher.group()))
              hideNodesFilter0 ::= classPattern
            }
          case HideEdgesRegex(last) =>
            val matcher = NodeSpecPattern.matcher(entry)
            while (matcher.find()) {
              val class1Pattern = Pattern.compile(preparePattern(matcher.group()))
              assert(matcher.find()) // it's got to be there, just matched it!
              val class2Pattern = Pattern.compile(preparePattern(matcher.group()))
              hideEdgesFilter0 ::= ((class1Pattern, class2Pattern))
            }
          case "" =>
            // don't need to do anything about it
          case _ =>
            warning("Could not understand diagram annotation in " + template.kind + " " + template.qualifiedName +
              ": unmatched entry \"" + entry + "\".\n" +
              "  This could be because:\n" +
              "   - you forgot to separate entries by commas\n" +
              "   - you used a tag that is not allowed in the current context (like @contentDiagram hideSuperclasses)\n"+
              "   - you did not use one of the allowed tags (see docs.scala-lang.org for scaladoc annotations)")
        }
      val result =
        if  (hideDiagram0)
          NoDiagramAtAll
        else if ((hideNodesFilter0.isEmpty) &&
                 (hideEdgesFilter0.isEmpty) &&
                 (hideIncomingImplicits0 == false) &&
                 (hideOutgoingImplicits0 == false) &&
                 (hideSuperclasses0 == false) &&
                 (hideSubclasses0 == false) &&
                 (hideInheritedNodes0 == false) &&
                 (hideDiagram0 == false))
          FullDiagram
        else
          AnnotationDiagramFilter(
            hideDiagram = hideDiagram0,
            hideIncomingImplicits = hideIncomingImplicits0,
            hideOutgoingImplicits = hideOutgoingImplicits0,
            hideSuperclasses = hideSuperclasses0,
            hideSubclasses = hideSubclasses0,
            hideInheritedNodes = hideInheritedNodes0,
            hideNodesFilter = hideNodesFilter0,
            hideEdgesFilter = hideEdgesFilter0)

      if (settings.docDiagramsDebug && result != NoDiagramAtAll && result != FullDiagram)
        settings.printMsg(template.kind + " " + template.qualifiedName + " filter: " + result)
      tFilter += System.currentTimeMillis

      result
  }
}
