package org.specs.matcher
import scala.xml._
import scala.xml.NodeSeq._
import StringToElem._
import xpath._
import org.specs.xml.NodeFunctions._
/**
 * The <code>XmlMatchers</code> trait provides matchers which are applicable to xml nodes
 */
trait XmlMatchers {
  
  /**
   * Matches if <code>node</code> is contained anywhere inside the tested node
   */   
  def \\(node: Node): XmlMatcher = new XmlMatcher(List(new PathFunction(node, nodeSearch _)))

  /**
   * Alias for <code>\\(node)</code> with the node label only
   */   
  def \\(label: String): XmlMatcher = \\(label.toElem)
  
  /**
   * Matches if <code>node</code> is contained anywhere inside the tested node and has exactly the <code>attributes</code> names 
   * as names for its attributes
   */   
  def \\(node: Node, attributes: List[String]): XmlMatcher = new XmlMatcher(List(new PathFunction(node, attributes, nodeSearch _)))

  /**
   * Alias for <code>\\(node, attributes)</code> with the node label only
   */   
  def \\(label: String, attributes: List[String]): XmlMatcher = \\(label.toElem, attributes)

  /**
   * Matches if <code>node</code> is contained anywhere inside the tested node and has exactly the <code>attributeValues</code> 
   * as names and values for its attributes
   */   
  def \\(node: Node, attributeValues: Map[String, String]): XmlMatcher = new XmlMatcher(List(new PathFunction(node, attributeValues, nodeSearch _)))

  /**
   * Alias for <code>\\(node, attributeValues)</code> with the node label only
   */   
  def \\(label: String, attributeValues: Map[String, String]): XmlMatcher = \\(label.toElem, attributeValues)

  /**
   * Matches if <code>node</code> is a direct child of the tested node
   */   
  def \(node: Node): XmlMatcher = new XmlMatcher(List(new PathFunction(node, subNodeSearch _)))

  /**
   * Alias for <code>\(node)</code> with the node label only
   */   
  def \(label: String): XmlMatcher = \(label.toElem)

  /**
   * Matches if <code>node</code> is a direct child of the tested node and has exactly the <code>attributes</code> names 
   * as names for its attributes
   */   
  def \(node: Node, attributes: List[String]): XmlMatcher = new XmlMatcher(List(new PathFunction(node, attributes, subNodeSearch _)))

  /**
   * Alias for <code>\(node, attributes)</code> with the node label only
   */   
  def \(label: String, attributes: List[String]): XmlMatcher = \(label.toElem, attributes)

  /**
   * Matches if <code>node</code> is a direct child of the tested node and has exactly the <code>attributeValues</code> 
   * as names and values for its attributes
   */   
  def \(node: Node, attributeValues: Map[String, String]): XmlMatcher = new XmlMatcher(List(new PathFunction(node, attributeValues, subNodeSearch _)))

  /**
   * Alias for <code>\(node, attributeValues)</code> with the node label only
   */   
  def \(label: String, attributeValues: Map[String, String]): XmlMatcher = \(label.toElem, attributeValues)
  
  /**
   * Matches if <code>node</code> is equal to the tested node without testing empty text
   */   
  def equalIgnoreSpace(node: Iterable[Node]): Matcher[Iterable[Node]] = new Matcher[Iterable[Node]] { def apply(n: =>Iterable[Node]) = (isEqualIgnoreSpace(node.toList, n.toList), n + " is equal to " + node, n + " is not equal to " + node) }

  /**
   * Alias for equalIgnoreSpace
   */   
  def ==/(node: Iterable[Node]): Matcher[Iterable[Node]] = equalIgnoreSpace(node)
}

/**
 * The XmlMatcher class matches an xml Node, or a list of Nodes against a list of search functions, which can either search for:<ul>
 * <li/>a given direct child, with its label and/or attributes and/or attributes names and values
 * <li/>a given child, direct or not (maybe deeply nested), with its label and/or attributes and/or attributes names and values
 * </ul>
 * 
 * XmlMatchers can be "chained" by using the \ or the \\ methods. In that case, the resulting matcher has a new
 * search function which tries to match the result of the preceding function. For example<pre>
 * <a><b><c><d></d></c></b></a> must \\("c").\("d")</pre> will be ok.
*/
case class XmlMatcher(functions: List[PathFunction]) extends Matcher[Iterable[Node]]() {
  
  /**
   * checks that the <code>nodes</code> satisfy the <code>functions</code>
   */
  def apply(n: =>Iterable[Node]) = {val nodes = n; checkFunctions(functions, nodes, (true, nodes.toString, nodes.toString))}
  
  /**
   * checks that the <code>nodes</code> satisfy the <code>functions</code>
   * @returns a MatcherResult (status, ok message, ko message)
   */
  def checkFunctions(pathFunctions: List[PathFunction], nodes: Iterable[Node], result: MatcherResult): MatcherResult = {
    // return the result if we have a failure or if there are no (or no more) functions to check
    if (!result.success || pathFunctions.isEmpty) 
      return result
    
    // check the rest of the functions, with the nodes returned by the current function
    // and build a MatcherResult being a success if the function retrieves some node
    pathFunctions match {
      case function::rest => {
         checkFunctions(rest, 
                        function(nodes), 
                        (result.success && !function(nodes).isEmpty, 
                        result.okMessage + (if (result.okMessage == nodes.toString) "" else " and") + " contains " + searchedElements(function), 
                        result.okMessage + (if (result.okMessage == nodes.toString) "" else " but") + " doesn't contain " + searchedElements(function)))
      }
      case _ => result
    }
  }
  
  /**
   * @returns a string representing the searched nodes, attributes, attribute values
   */
  private[this] def searchedElements(function: PathFunction) = {
      val node = if (function.node.child.isEmpty)
                   function.nodeLabel
                 else
                   function.node.toString
      val attributes = if (function.attributes.isEmpty && function.attributeValues.isEmpty) 
                         "" 
                       else 
                         " with attributes: " + function.searchedAttributes
      node + attributes
  }

  /**
   * @returns a new Matcher which will try to find <code>node</code> as a direct child after using
   * functions to find elements
   */
  def \(node: Node): XmlMatcher = new XmlMatcher(functions:::List(new PathFunction(node, Nil, subNodeSearch _)))

  /**
   * @returns a new Matcher which will try to find <code>node</code> as a child (possibly deeply nested) after 
   * using functions to find elements
   */
  def \\(node: Node): XmlMatcher = new XmlMatcher(functions:::List(new PathFunction(node, Nil, nodeSearch _)))

  /**
   * alias for \ using the node label only 
   */
  def \(label: String): XmlMatcher = \(label.toElem)

  /**
   * alias for \\ using the node label only 
   */
  def \\(label: String): XmlMatcher = \\(label.toElem)
}  

/**
 * This object provides XPath functions in order to use them as parameters
 */
object xpath extends XPathFunctions
trait XPathFunctions {
  type XPathFunction = Function2[Node, String, NodeSeq]
  /**
   * @returns the \ XPath function
   */
  def subNodeSearch(node: Node, label: String) = node \ label  

  /**
   * @returns the \\ XPath function
   */
  def nodeSearch(node: Node, label: String) = node \\ label  
}

/**
 * The PathFunction object encapsulate a search for a node and/or attributes or attributeValues with an XPath function
 * If <code>node</code> has some children, then they are searched using equality
 */
class PathFunction(val node: Node, val attributes: List[String], val attributeValues: Map[String, String], val function: XPathFunction) extends Function1[Iterable[Node], Iterable[Node]] with XPathFunctions {

  /**
   * @returns a PathFunction looking for a Node
   */
  def this(n: Node, function: XPathFunction) = this(n, Nil, Map.empty, function)

  /**
   * @returns a PathFunction looking for a Node and its attributes
   */
  def this(n: Node, attributes: List[String], function: XPathFunction) = this(n, attributes, Map.empty, function)

  /**
   * @returns a PathFunction looking for a Node and its attributes and attributes values
   */
  def this(n: Node, attributeValues: Map[String, String], function: XPathFunction) = this(n, Nil, attributeValues, function)

  /**
   * @returns the node if it is found and matching the searched attributes and/or attribute values when specified
   */
  def apply(nodes: Iterable[Node]): Iterable[Node] = for(n <- nodes;
                                                         found <- function(n, node.label) if (matchNode(found))) 
                                                       yield found 

  /**
   * @returns "subnode" or "node" depending on the type of search a direct child search or a general search
   */
  def nodeLabel: String = (if (!function(<a/>, "a").isEmpty) "node " else "subnode " )+ node.label
  
  /**
   * @returns true if the node found with a label also satisfies the attributes and/or values requirement
   */
  def matchNode(found: Node): Boolean = {
    // returns true if m matches the attribute names or attribute names + values
    def attributesMatch(m: MetaData) = if (!attributes.isEmpty)
                                         m.map((a: MetaData) => a.key).toList.intersect(attributes) == attributes
                                       else if (!attributeValues.isEmpty)
                                         Map(m.map((a: MetaData) => a.key -> a.value.toString).toList: _*) == attributeValues
                                       else
                                         true

    // returns true if the node matches the specified children
    def childrenMatch(n: Node) = {
      if (node.child.isEmpty) 
        true 
      else 
        isEqualIgnoreSpace(fromSeq(n.child), fromSeq(node.child))
    }

    attributesMatch(found.attributes) && childrenMatch(found) 
  }

  /**
   * @returns a string representation of attributes or attributeValues (one of them being empty by construction)
   */
  def searchedAttributes = attributes.mkString(", ") + attributeValues.map(a=> a._1 + "=\"" + a._2 + "\"").mkString(" ")
  
}
object StringToElem {
  implicit def toElement(s: String) = new ToElem(s)
  class ToElem(s: String) {def toElem: Elem = Elem(null, s, Null, TopScope)}
}