require 'nokogiri'
require 'ragtag/meta/data'
require 'ragtag/core_ext/opvars'

# = RAGTAG - A Tag Attribute Language for Ruby
#
# RubyTals is a Ruby variation on Zope Page Templates and it's TAL specification.
# It differs from TAL in that it is specifically geared for use by Ruby.
#
# == Usage
#
#   s = %q{
#     <html>
#     <body>
#       <h1 r:content="x">[X]</h1>
#       <div r:each="animal" r:do="a">
#         <b r:content="a">[ANIMAL]</b>
#       </div>
#       <div r:if="animal.size > 1">
#         There are <b r:content="animal.size">[ANIMAL SIZE]</b> animals.
#       </div>
#     </body>
#     </html>
#   }
#
#   x = 'Our Little Zoo'
#   animal = ['Zebra', 'Monkey', 'Tiger' ]
#
#   puts Ragtag.compile(s, binding)
#
# == Note
#
# Presently RagTag clauses can run arbitraty Ruby code. Although
# upping the safety level before executing a compiled template
# should be sufficiently protective in most cases, perhaps it would
# be better to limit valid expressions to single object references,
# ie. 'this.that', and then use a substitution of '.' for '/'.
# Not only would this be highly protective, it would also be more
# compatible with the original TAL spec; albeit this isn't exacty
# how TALs interprets the '/' divider.
#
# On the other hand perhaps it is too much restraint. For instance
# it would require the if-clause in the above exmaple to be
# something like:
#
#       <div r:if="animal/plenty">
#
# and have a definition in the evaluating code:
#
#   def animal.plenty
#     size > 1
#   end
#
# It's a classic Saftey vs. Usability trade-off. Something to
# consider for the future.

class RagTag

  #
  attr :xml

  #
  attr :scope

  #
  def self.compile(xml, scope=nil)
    new(xml).compile(scope)
  end

  #def self.execute( script, data )
  #  vars.each_pair { |k,v|
  #  }
  #  eval script
  #end

  #
  def initialize(xml)
    case xml
    when String
      @xml = Nokogiri::XML(xml)
    else
      @xml = xml
    end
  end

  #
  def compile(scope=nil)
    scope = scope || $TOPLEVEL_BINDING
    parse(@xml.root, scope)
    xml
  end

  #def parse_all(node)
  #  while node
  #    parse(node)
  #    node = node.next
  #  end
  #end

  #$rtals_each_stack = []

  #
  def parse(node, scope)
    case node
    when Nokogiri::XML::Text
      # nothing
    when Nokogiri::XML::NodeSet
      parse_nodeset(node, scope)
    when Nokogiri::XML::Element
      if value = node['define']
        eval(value, scope)
      end

      if node['if']
        parse_if(node, scope)
      #elsif node['condition']
      #  parse_condition(node, scope)
      end

      if node['content']
        parse_content(node, scope)
      elsif node['replace']
        parse_replace(node, scope)
      end

      if node['attr'] || node['attributes']
        parse_attributes(node, scope)
      end

      if node['each']
        parse_each(node, scope)
        return
      elsif node['repeat']
        parse_repeat(node, scope)
        return
      end

      node.children.each do |child|
        parse(child, scope)
      end

      if node['omit'] && node['omit'] != 'false'
        parse_omit(node, scope)
      end
    else
      raise node.inspect
    end
    return node
  end

  #
  def parse_nodeset(nodeset, scope)
    nodeset.each do |node|
      parse(node, scope)
    end
    nodeset
  end

  #
  def parse_content(node, scope)
    value = node['content']
    node.content = eval(value, scope)
    node.remove_attribute('content')
  end

  #
  def parse_replace(node, scope)
    value = node['replace']
    node.before(eval(value, scope).to_s)
    node.unlink
  end

  #
  def parse_attributes(node, scope)
    if attrs = node['attr']
      assoc = attrs.split(',').map{ |e| e.strip.split(':') }
      assoc.each do |(k,v)|
        node[k] = eval(v, scope).to_s
      end
      node.remove_attribute('attr')
    end
    if attrs = node['attributes']
      assoc = attrs.split(',').map{ |e| e.strip.split(':') }
      assoc.each do |(k,v)|
        node[k] = eval(v, scope).to_s
      end
      node.remove_attribute('attributes')
    end
    node
  end

  #
  def parse_if(node, scope)
    value = node['if']
    if eval(value, scope)
      node.remove_attribute('if')
      parse(node.children, scope)
    else
      node.unlink
    end
    node
  end

  ## Like #parse_if but does not keep the conditional node.
  #def parse_condition(node, scope)
  #  value = node['condition']
  #  if eval(value, scope)
  #    parse(node.children, scope).each do |x|
  #      x.unlink
  #      node.add_previous_sibling(x) 
  #    end
  #    node.unlink
  #  else
  #    node.unlink
  #  end
  #end

  #
  def parse_each(node, scope)
    value = node['each']
    args  = node['do'] || 'x'
    copy  = node.dup
    node.children.remove
    bindings = eval("#{value}.map{ |#{args}| binding }", scope)
    bindings.each do |each_scope|
      sect = parse(copy.dup.children, each_scope)
      sect.each do |x|
        node << x
      end
    end
    node.remove_attribute('each')
    node.remove_attribute('do')
    value
  end

  #
  def parse_repeat(node, scope)
    value = node['repeat']
    args  = node['do'] || 'x'
    copy  = node.dup
    copy.remove_attribute('repeat')
    copy.remove_attribute('do')
    bindings = eval("#{value}.map{ |#{args}| binding }", scope)
    bindings.each do |each_scope|
      sect = parse(copy.dup, each_scope)
      node.add_previous_sibling(sect)
      # parse_omit(sect, scope) if sect['omit'] && sect['omit'] != 'false'
    end
    node.unlink
    value
  end

  #
  def parse_omit(node, scope)
    #parse(node.children, scope).each do |x|
    node.children.each do |x|
      x.unlink
      node.add_previous_sibling(x) 
    end
    node.unlink
  end

end