# -*- ruby -*-

require 'xtemplate/xpath'
require 'xtemplate/util'

module XTemplate
  BIND_URI  = "http://xtemplate.sourceforge.net/xtemplate"

  class XNode
    include XPath
    include Util

    attr_accessor :name, :parent, :children, :attrs, :alt
    attr_accessor :pi, :propagation, :data_path, :option

    def initialize(name = nil, attrs = nil, children = nil, parent = nil,
		   data_path = nil, propagation = true,
		   exname = nil, exattr = nil, expand = nil, alt = nil, opt={})
      @name  = name
      @attrs = attrs || []
      @children = children || []
      @parent = parent
      @data_path = data_path
      @exname = exname
      @exattr = exattr
      @expand = expand # :normal, :strip
      @alt    = alt
      @option = opt
      if( propagation.nil? )
	@propagation = true
      else
	@propagation = propagation
      end
    end

    def add_child(node)
      case node
      when XNode
	node.parent = self
      else
	node = sanitize(node.to_s)
      end
      @children.push(node)
    end

    def add_attr(attr)
      @attrs.push([attr,SanitizedString.new("")])
    end

    def set_propagation(flag)
      @propagation = flag
    end

    def add_attrval(val)
      @attrs[-1][1].concat(sanitize(val))
    end

    def to_s
      s = ""
      dump(s)
      SanitizedString.new(s)
    end

    # also implemented in xt.c.
    def dump(io)
      if( @name )
	attrs = @attrs.collect{|a,val| "#{a}=\"#{val}\"" }
	if( attrs.size > 0 )
	  attrs = " " + attrs.join(" ")
	else
	  attrs = ""
	end
        if( @children.empty? )    # experimental
          io << "<#{@name}#{attrs} />"
          return io
        else
          io << "<#{@name}#{attrs}>"
        end
#        io << "<#{@name}#{attrs}>"
      end
      for s in @children
	if( s.is_a?(XNode) )
	  s.dump(io)
	else
          io << s
	end
      end
      if( @name )
	io << "</#{@name}>"
      end
      io
    end

    # also implemented in xt.c.
    def deep_dup(p = nil)
      node = XNode.new(@name && @name.dup,
		       @attrs && @attrs.dup, nil, p,
		       @data_path && @data_path.dup,
		       @propagation,
		       @exname && @exname.dup,
		       @exattr && @exattr.dup,
		       @expand, @alt, @option.dup)
      node.children = @children.collect{|child|
	if( child.is_a?(XNode) )
	  child.deep_dup(node)
	else
	  child
	end
      }
      node
    end

    def strip!(recursive=true)
      @children.reject!{|child| child.is_a?(String) && child =~ /\A\s*\z/ }
      if( recursive )
	@children.each{|child|
	  if( child.is_a?(XNode) )
	    child.strip!(true)
	  end
	}
      end
    end

    def to_hash(array = XArray, unsanitize_p = false)
      if( @hash )
	return @hash
      end
      ary  = array[]
      @children.each{|child|
	if( child.is_a?(XNode) )
	  ary.push(child.to_hash(array,unsanitize_p))
	else
	  text = child.to_s
	  if( ary[-1].is_a?(String) )
	    ary[-1].concat(text)
	  else
	    if( unsanitize_p )
	      text = unsanitize(text)
	    end
	    ary.push(text.dup)
	  end
	end
      }
      @attrs.each{|attr,val|
	if( unsanitize_p )
	  val = unsanitize(val)
	end
	ary.push({'@'+attr => val})
      }
      if( ary.size == 1 )
	ary = ary[0]
      end
      case @option[:type]
      when 'array'
	ary = Array[*ary]
      when 'hash'
	ary = eval_action("hash()", ary, nil)
      when 'int'
	ary = ary.to_i
      when 'float'
	ary = ary.to_f
      when 'string'
	ary = ary.to_s
      end

      if( @name )
	@hash = {@name => ary}
      else
	case ary
	when Hash
	  @hash = ary
	else
	  @hash = {}
	  ary.each{|h|
	    if( h.is_a?(Hash) )
	      h.each{|key,val|
		@hash[key] = val
	      }
	    end
	  }
	end
      end
      @hash
    end

    def lattrval(key)
      @attrs.each{|attr,val|
	if( attr == key )
	  return val
	end
      }
      nil
    end
    alias attrval lattrval

    def rattrval(key)
      @attrs.reverse.each{|attr,val|
	if( attr == key )
	  return val
	end
      }
      nil
    end

    DEFAULT_COMMAND = {
      :select       => true,
      :template     => true,
      :element      => true,
      :attribute    => true,
      :expand       => true,
      :include      => true,
      :copy_of      => true,
      :value_of     => true,
      :each         => true,
      :property     => true,
      :invoke       => true,
      :@id          => true,
      :@each        => true,
      :@type        => true,
    }
    def prepare(xmlns = nil, enable = nil, templates = nil)
      # find xmlns:xxx="..."
      enable ||= DEFAULT_COMMAND
      templates ||= {}
      tmp = []
      @attrs.each_with_index{|elem,idx|
	attr,val, = elem
	if( (attr =~ /^xmlns:(.+)/) && (val == BIND_URI) )
	  xmlns = $1
	  @attrs.delete_at(idx)
	  break
	end
      }

      if( @name )
	if( xmlns )
	  if( @name =~ /#{xmlns}:(.+)/ )
	    # special tags
	    cmd = $1.tr('-','_').intern
	    if( enable[cmd] )
	      case cmd
	      when :template
		t_name = attrval("name") || attrval("#{xmlns}:name")
		@name = nil
		@attrs = []
		if( t_name )
		  templates[t_name] = self
		end
	      when :expand
		@name = nil
		@data_path = attrval("id") || attrval("#{xmlns}:id")
		with_strip = attrval("strip") || attrval("#{xmlns}:strip")
		case with_strip
		when "yes"
		  @expand = :strip
		else
		  @expand = :normal
		end
	      when :select, :value_of
		@name = nil
		@data_path   = attrval("id") || attrval("#{xmlns}:id")
		if( @propagation = attrval("propagation") || attrval("#{xmlns}:propagation") )
		  @propagation = (@propagation == "yes")
		end
	      when :copy_of
		@name = nil
		@data_path = attrval("id") || attrval("#{xmlns}:id")
		with_tag   = attrval("with") || attrval("#{xmlns}:with") || ''
		if( @data_path )
		  @data_path = @data_path + "{dump(#{with_tag})}"
		end
		if( @propagation = attrval("propagation") || attrval("#{xmlns}:propagation") )
		  @propagation = (@propagation == "yes")
		end
              when :each
                @name = nil
                @data_path = attrval("id") || attrval("#{xmlns}:id")
                if( @data_path )
                  @data_path = @data_path + "{array()}"
                end
	      when :element
		@exname = attrval("name") || attrval("#{xmlns}:name")
		@data_path   = attrval("id") || attrval("#{xmlns}:id")
		@name = nil
		@attrs = []
	      when :attribute
		@name = nil
		@exattr = attrval("name") || attrval("#{xmlns}:name")
		@data_path   = attrval("id") || attrval("#{xmlns}:id")
              when :property, :invoke
                @name = nil
                @data_path = attrval("id") || attrval("#{xmlns}:id")
                @data_path += "."
                @data_path += attrval("name") || attrval("#{xmlns}:name") || attrval("method") || attrval("#{xmlns}:method")
                withval = attrval("with") || attrval("#{xmlns}:with") || ''
                if( withval )
                  @data_path += "(#{withval})"
                end
	      when :import
		src = attrval("src") || attrval("#{xmlns}:src")
		@data_path = "{data(#{src})}"
		@name = nil
	      when :include
		src = attrval("src") || attrval("#{xmlns}:src")
		case src
		when /^\$(.+)/
		  doc = eval(src)
		when /^file:\/\/(.+)/
		  doc = File.open($1){|file| file.read }
		when /^http:\/\/(.+)/
		  raise(NotImplementedError, "'#{src}' can not be included.")
		else
		  doc = File.open(src){|file| file.read }
		end
		parser = XMLParser.new()
		node = parser.parse(doc)
		node.prepare(xmlns, enable, templates)
		@name = nil
		add_child(node)
	      else
		raise(NotImplementedError, "'#{cmd.tr("_","-").to_s}' is not supported.")
	      end
              if( @alt = (attrval("alt") || attrval("#{xmlns}:id")) )
                @data_path += "{alt(#{@alt})}"
              end
	    else
	      raise(RuntimeError, "'#{cmd.tr("_","-").to_s}' is invalid.")
	    end
	  else
	    # parse attribute with xmlns
	    tmp = []
	    attr_id   = "#{xmlns}:id"
	    attr_type = "#{xmlns}:type"
            attr_each = "#{xmlns}:each"
            attr_alt  = "#{xmlns}:alt"
	    @attrs.each{|attr,val|
	      case attr
	      when attr_id
		if( enable[:@id] )
		  @data_path = val
		else
		  raise(RuntimeError, "'#{attr}' is invalid.")
		end
              when attr_each
                if( enable[:@each] )
                  @data_path = val + "{array()}"
                else
		  raise(RuntimeError, "'#{attr}' is invalid.")
                end
	      when attr_type
		if( enable[:@type] )
		  @option[:type] = val
		else
		  raise(RuntimeError, "'#{attr}' is invalid.")
		end
              when attr_alt
                @alt = val
	      else
		tmp.push([attr,val])
	      end
	    }
            if( @alt )
              @data_path += "{alt(#{@alt})}"
            end
	    @attrs = tmp
	  end
	else # xmlns
	  # parse attribute
	  tmp = []
	  @attrs.each{|attr,val|
	    case attr
	    when 'id'
	      if( enable[:@id] )
		@data_path = val
	      else
		tmp.push([attr,val])
	      end
            when 'each'
              if( enable[:@each] )
                @data_path = @data_path + "{array()}"
              else
                tmp.push([attr,val])
              end
	    else
	      tmp.push([attr,val])
	    end
	  }
	  @attrs = tmp
	end
      end
      @children.each{|child|
	if( child.is_a?(XNode) )
	  child.prepare(xmlns, enable, templates)
	end
      }
    end

    def expand_attr(data)
      # substituting attribute values.
      @attrs = ([[nil,@exname],[nil,@exattr]]+@attrs).collect{|attr,val|
	[
	  attr,
	  if( val )
	    val.gsub(/(@@?)[\{\(](.+?)[\}\)]/){|str|
	      at = $1
	      key = $2
	      case at.size
	      when 1
		if( data.is_a?(Hash) )
		  v = data[key]
		  case v
		  when Hash
		    str = v[TextNode] || ''
		  else
		    str = v.to_s
		  end
		else
		  str = ''
		end
	      when 2
		str[0,1] = ''
	      end
	      sanitize(str)
	    }
	  else
	    nil
	  end
	]
      }
      @exname = @attrs.shift()[1]
      @exattr = @attrs.shift()[1]
      if( @exname )
	@name   = @exname
	@exname = nil
      end
    end

    def expand_children(data, pdata, rdata, plugin)
      for child in @children
	if( child.is_a?(XNode) )
	  child.expand(data, pdata, rdata, plugin)
	end
      end
    end

    def expand_with_hash(data, pdata, rdata, plugin)
      # insert attributes and elements specified by ID.
      for key,attrval in data
	if( key == TextNode )
          if( attrval )
            @children = []
            add_child(attrval)
          end
	elsif( key[0] == ?@ )
	  attr = key[1..-1]
	  if( @name )
	    add_attr(attr)
	    add_attrval(attrval)
	  elsif( @propagation )
	    node = @parent
	    while( node )
	      if( node.name )
		node.add_attr(attr)
		node.add_attrval(attrval)
		break
	      end
	      node = node.parent
	    end
	  end
	end
      end
      expand_children(data, pdata, rdata, plugin)
    end

    # (1)current data (2)parent data (3)root data (4)plugin object
    EXPAND_CODE = %q`
    def expand(data, pdata, rdata, plugin)
      if( @alt && plugin._alt_exist?(@alt) )
        @parent.children.delete(self)
      end
      if( @data_path )
	case data
	when Hash
	  val = value_by_path2(@data_path, data, pdata, rdata, plugin)
	  case val
	  when Array
	    nodes = val.collect{|v|
	      node = deep_dup()
	      node.parent = self
	      case v
	      when Hash
		node.expand_with_hash(v, pdata, rdata, plugin)
	      else
		node.add_child(v)
	      end
	      node.expand_attr(v)
	      node
	    }
	    @name = nil
	    @attrs = []
	    @children = nodes
	    #@data_path = nil
	  when Hash
	    expand_attr(val)
	    expand_with_hash(val, pdata, rdata, plugin)
	  else
	    expand_attr(val)
	    add_child(val)
	  end
	  if( @expand )
	    @name = nil
	    @attrs = []
	    case val
	    when Hash
	      val.clear()
	      val[TextNode] = self.to_s()
	      if( @expand == :strip )
		val[TextNode] = val[TextNode].strip()
	      end
	    when Array
	      for i in 0..(val.size-1)
		s = @children[i].to_s()
		if( @expand == :strip )
		  s = s.strip()
		end
		case val[i]
		when Hash
		  val[i].clear()
		  val[i][TextNode] = s
		when String
		  val[i][0..-1] = @children[i].to_s()
		end
	      end
	    when String
	      s = self.to_s()
	      if( @expand == :strip )
		s = s.strip()
	      end
	      val[0..-1] = s
	    end
	    @children = []
	  end
	end
      else
	expand_attr(data)
	expand_children(data, pdata, rdata, plugin)
      end

      # expand <xx:attribute ..> after expanding children.
      if( @exattr )
	val = to_s()
	@children = []
	node = self
	while( node = node.parent )
	  if( node.name )
	    break
	  end
	end
	node.add_attr(@exattr)
	node.add_attrval(val)
	@exattr = nil
      end
    end
    ` # don't eliminate this quote
    eval(EXPAND_CODE)

    def XNode.use_default_expand()
      XNode.class_eval(EXPAND_CODE)
    end

    def XNode.use_simple_expand()
      code = EXPAND_CODE.sub(/value_by_path2\(@data_path, data, pdata, rdata, plugin\)/,
			     "data[@data_path]")
      XNode.class_eval(code)
    end
  end # XNode
end
