#
# **PredicateFactory** generates boolean-valued
# functions that implement tests of specific
# CSS selectors.
#
# Each generated function has the signature:
#
#     predicate(node,node_metadata,dom_metadata)
#
# and returns `true` iff the given `node` matches
# the associated CSS selection rule.
#
# (This is an internal class, primarily used by
# the class `Stew`. These methods are subject to
# change without notice.)
#
class PredicateFactory

  # **and_predicate** generates a function that returns `true` iff *all* of the given `predicates` evaluate to `true`.
  and_predicate:(predicates)->
    return (node,node_metadata,dom_metadata)->
      for predicate in predicates
        if not predicate(node,node_metadata,dom_metadata)
          return false
      return true

  # **or_predicate** generates a function that returns `true` iff *any* of the given `predicates` evaluate to `true`.
  or_predicate:(predicates)->
    return (node,node_metadata,dom_metadata)->
      for predicate in predicates
        if predicate(node,node_metadata,dom_metadata)
          return true
      return false

  # **by_attribute_predicate** creates a predicate
  # that returns `true` if the given `attrname`
  # matches the given `attrvalue`.
  #
  # * When `attrvalue` is `null`, then the predicate will
  #   return `true` if the tested node has an attribute
  #   named `attrname`.
  #
  # * When `attrvalue` is a String then the predicate will
  #   return true if the value of the `attrname` attribute
  #   *equals* the `attrvalue` *string*.
  #
  # * When `attrvalue` is a `RegExp` then the predicate will
  #   return `true` if the value of the `attrname` attribute
  #   *matches* the `attrvalue` *expression*.
  #
  # * When `valuedelim` is non-`null`, the specified value will
  #   be used as a delimiter by which to split the value of
  #   the `attrname` attribute, and the corresponding elements
  #   will be tested rather than the entire string.
  #
  # For example, the call:
  #
  #     by_attribute_predicate('class','foo',/\s+/)
  #
  # will return a function that tests if a given DOM node
  # has been assigned class `foo`.  E.g, `true` for these:
  #
  #     <span class="foo"></span>
  #
  #     <span class="bar foo"></span>
  #
  # and `false` for these:
  #
  #     <span></span>
  #
  #     <span class="food"></span>
  #
  by_attribute_predicate:(attrname,attrvalue=null,valuedelim=null)->
    if typeof(attrname) is 'string'
      np = (str)->str is attrname
    else
      np = (str)->attrname.test(str)

    if attrvalue is null
      vp = null
    else if typeof(attrvalue) is 'string'
      attrvalue = attrvalue.replace(/\\\"/g,'"')
      vp = (str)->str is attrvalue
    else if attrvalue?.test?
      vp = (str)->attrvalue.test(str)

    return (node)->
      for name,value of node?.attribs
        if np(name)
          if vp is null
            return true
          else
            if valuedelim?
              if value?
                for token in value.split(valuedelim)
                  if vp(token)
                    return true
            else
              if vp(value)
                return true
      return false

  # **by_class_predicate** creates a predicate
  # that returns `true` if the given DOM node has
  # the specified `klass` value.
  by_class_predicate:(klass)=>
    return @by_attribute_predicate('class',klass,/\s+/)

  # **by_id_predicate** creates a predicate
  # that returns `true` if the given DOM node has
  # the specified `id` value.
  by_id_predicate:(id)=>
    return @by_attribute_predicate('id',id)

  # **by_attr_exists_predicate** creates a
  # predicate that returns `true` if the given DOM
  # node has an attribute with the specified `attrname`,
  # regardless of the value for the atttribute.
  by_attr_exists_predicate:(attrname)=>
    return @by_attribute_predicate(attrname,null)

  # **by_attr_value_predicate** is an alias to `by_attribute_predicate`.
  by_attr_value_predicate:(attrname,attrvalue,valuedelim)=>
    return @by_attribute_predicate(attrname,attrvalue,valuedelim)

  # **_escape_for_regexp** is an internal utility function that escapes
  # reserved characters to create a string that can be embedded
  # in a regular expression.
  _escape_for_regexp:(str)->return str.replace(/([.?*+^$[\]\\(){}|-])/g, "\\$1")

  # **by_attr_value_pipe_equals** creates a predicate that
  # implements the `[name|=value]` CSS selector (matching tags
  # with a `name` attribute with a value matching `value`
  # (exactly) or a value that starts with `value` followed
  # by a `-` character..
  #
  # (Used for selectors such as `[lang|=en]`, for example,
  # which will match the values `en`, `en-US` and `en-CA`.)
  #
  # When `attrvalue` is a regular expression:
  #
  #  - If `attrvalue` doesn't already start with
  #    `^` (matching the beginning of a line),
  #    then `^` will be added.
  #
  #  - If `attrvalue` doesn't already end with
  #    `($|-)` (matching the end of a line, or `-`)
  #    then `($|-)` will be added.
  #
  # Hence the regular expression `/f[aeio]o?/`
  # would be converted to `/^f[aeio]o?($|-)/` but
  # the regular expression `/^en($|-)/` would be
  # left alone.
  by_attr_value_pipe_equals:(attrname,attrvalue)=>
    if typeof attrvalue is 'string'
      regexp_source = @_escape_for_regexp(attrvalue)
      attrvalue = new RegExp("^#{regexp_source}($|-)")
    else
      regexp_source = attrvalue.source
      modifier = ''
      modifier += 'i' if attrvalue.ignoreCase
      modifier += 'g' if attrvalue.global
      modifier += 'm' if attrvalue.multiline
      unless /^\^/.test attrvalue.source
        regexp_source = "^#{regexp_source}"
      unless /\(\$\|-\)$/.test regexp_source
        regexp_source = "#{regexp_source}($|-)"
      attrvalue = new RegExp(regexp_source,modifier)
    return @by_attribute_predicate(attrname,attrvalue)

  # **by_tag_predicate** creates a
  # predicate that returns `true` if the given DOM
  # node is a tag with the specified `name`.
  #
  # If `name` is a RegExp then the predicate will
  # return true if tag's name *matches* the
  # specified *expression*.
  #
  # If `name` is a String then the predicate will
  # return true if the tag's name *equals* the
  # specified *string*.
  by_tag_predicate:(name)->
    if typeof name is 'string'
      return (node)->(name is node.name)
    else
      return (node)->(name.test(node.name))

  # **first_child_predicate** returns a predicate that evaluates to `true`
  # iff the given `node` is the first child *tag* node among all of
  # its siblings.
  #
  #{ TODO FIXME should :first-child also consider elements like <script>?
  first_child_predicate:()->return @_first_child_impl
  _first_child_impl:(node,node_metadata,dom_metadata)->
    if node.type is 'tag' and node_metadata.siblings?
      for elt in node_metadata.siblings
        if elt.type is 'tag'
          return node._stew_node_id is elt._stew_node_id
    return false

  # **any_tag_predicate** returns a predicate that evaluates
  # to `true` iff the given `node` is a tag.
  any_tag_predicate:()->return @_any_tag_impl
  # (...and **_any_tag_impl** is the implementation of that predicate.)
  _any_tag_impl:(node)->(node?.type is 'tag')

  # **descendant_predicate**
  # returns a predicate that for the given array
  # *P* containing *n*, evaluates to `true` for
  # a given `node` if:
  #
  #  - `P[n-1](node)` is `true`, and
  #
  #  - `P[n-2](parent)` is `true` for some element
  #    `parent` that is an ancestor of `node`
  #
  #  - `P[n-3](parent2)` is `true` for some element
  #     `parent2` that is an ancestor of `parent`
  #
  #  - ...etc.
  #
  # In other words, the returned predicate will evalue to `true`
  # for the current `node` if each of the given `predicates`
  # evaluates to `true` for some ancestor of the node, *in sequence*.
  # (I.e., the node that matches `predicates[n]` must be an ancestor
  # of the node that mathces `predicates[n+1]`.)
  #
  descendant_predicate:(predicates)->
    if predicates.length is 1
      return predicates[0]
    else
      return (node,node_metadata,dom_metadata)->
        if predicates[predicates.length-1](node,node_metadata,dom_metadata)
          cloned_path = [].concat(node_metadata.path)
          cloned_predicates = [].concat(predicates)
          cloned_predicates.pop() # drop last predicate, we just tested it
          while cloned_path.length > 0
            node = cloned_path.pop()
            node_metadata = dom_metadata[node._stew_node_id]
            if cloned_predicates[cloned_predicates.length-1](node,node_metadata,dom_metadata)
              cloned_predicates.pop()
              if cloned_predicates.length is 0
                return true
                break
        return false

  # **direct_descendant_predicate** returns a predicate
  # that evaluates to `true` iff `child_selector` evalutes
  # to `true` for the given `node` and `parent_selector` evalutes
  # to `true` for the given `node`'s parent.
  direct_descendant_predicate:(parent_selector,child_selector)->
    return (node,node_metadata,dom_metadata)->
      if child_selector(node,node_metadata,dom_metadata)
        parent = node_metadata.parent
        parent_metadata = dom_metadata[parent._stew_node_id]
        return parent_selector(parent,parent_metadata,dom_metadata)
      return false


  # **adjacent_sibling_predicate** returns a predicate
  # that evaluates to `true` iff `second` evaluates
  # to `true` for the given `node` and `first` evaluates
  # to `true` for the tag sibling immediately preceding
  # the given `node`.
  adjacent_sibling_predicate:(first,second)->
    return (node,node_metadata,dom_metadata)->
      if second(node,node_metadata,dom_metadata)
        prev_tag_index = node_metadata.sib_index - 1
        while prev_tag_index > 0
          if node_metadata.siblings[prev_tag_index].type is 'tag'
            prev_tag = node_metadata.siblings[prev_tag_index]
            return first(prev_tag,dom_metadata[prev_tag._stew_node_id],dom_metadata)
          else
            prev_tag_index -= 1
      return false

  # **preceding_sibling_predicate** returns a predicate
  # that evaluates to `true` iff `second` evaluates
  # to `true` for the given `node` and `first` evaluates
  # to `true` for some tag sibling preceding
  # the given `node`.
  preceding_sibling_predicate:(first,second)->
    return (node,node_metadata,dom_metadata)->
      if second(node,node_metadata,dom_metadata)
        for prev,index in node_metadata.siblings
          if index is node_metadata.sib_index
            return false
          else if prev.type is 'tag'
            if first(prev,dom_metadata[prev._stew_node_id],dom_metadata)
              return true
      return false

# The PredicateFactory class is exported under the name `PredicateFactory`.
exports = exports ? this
exports.PredicateFactory = PredicateFactory
