require 'pp'
module TracWiki

    class RawHtml
      def initialize(text)
        @text = text
      end
      def to_s
        @text
      end
    end

    class Node
      attr_accessor :tag
      attr_accessor :par
      attr_accessor :cont
      attr_accessor :attrs
      def initialize(tag_name, par=nil, attrs={}, cont=[])
        @tag = nil
        @tag = tag_name.to_sym if tag_name
        @par = par
        cont = [ cont ] if cont.is_a? String
        @cont = cont || []
        @attrs = attrs || {}
      end

      def add(cont)
        @cont << cont
      end
    end
    class Tree
      attr_accessor :cur

      def initialize
        @root = Node.new(nil)
        @cur = @root
      end

      def tag(tag, attrs = nil, cont = nil)
        if cont.nil? && ! attrs.is_a?(Hash)
          # tag(:b, "ahoj") -> tag(:b, {}, "ahoj")
          cont = attrs
          attrs = nil
        end
        cont = [ cont ] if cont.is_a? String
        @cur.add(Node.new(tag, @cur, attrs, cont))
        self
      end

      def tag_beg(tag_name, attrs = nil, cont = nil)
        node = Node.new(tag_name, @cur, attrs, cont)
        @cur.add(node)
        @cur = node
        self
      end

      def tag_end(tag_name)
        c = @cur
        ts = tag_name.to_sym
        while c.tag != ts
          c = c.par
          if c.nil?
            return "no such tag in stack, ingoring "
          end
        end
        @cur = c.par
        self

#        if @cur.tag == tag_name.to_sym
#          @cur = @cur.par
#        else
#          #pp(@root)
#          raise "tag_end: cur tag is not <#{tag_name}>, but <#{@cur.tag}>"
#        end
#        self
      end

      # add space if needed
      def add_spc
        if @cur.cont.size > 0
          last = @cur.cont.last
          if last.is_a?(String) && last[-1] == ?\s
            return
          end
        end
        add(' ')
      end

      def add(cont)
        @cur.add(cont)
        self
      end

      def add_raw(cont)
        #cont_san = Sanitize.clean(cont, san_conf)
        @cur.add(RawHtml.new(cont))
        self
      end


     def find_par(tag_name, node = nil)
       node = @cur if node.nil?
       while ! node.par.nil?
         if node.tag == tag_name
            return node.par
         end
         node = node.par
       end
       nil
     end

     def to_html
       ret = tree_to_html(@root)
       ret
     end
     def tree_to_html(node)
        tag = node.tag
        if tag.nil?
          return cont_to_s(node.cont)
        end

        nl = ''
        nl = "\n"  if TAGS_APPEND_NL.include? tag

        if ! TAGS_ALLOVED.include? tag
          return '' if node.cont.size == 0
          return cont_to_s(node.cont)
        end
        if node.cont.size == 0
          if TAGS_SKIP_EMPTY.include? tag
             return ''
          end
          if TAGS_FORCE_PAIR.include? tag
            return "<#{tag}#{attrs_to_s(tag, node.attrs)}></#{tag}>#{nl}"
          end
          return "<#{tag}#{attrs_to_s(tag, node.attrs)}/>#{nl}"
        end

        return "<#{tag}#{attrs_to_s(tag, node.attrs)}>#{cont_to_s(node.cont)}</#{tag}>#{nl}"
     end


     TAGS_APPEND_NL = [:div, :p, :li, :ol, :ul, :dl, :table, :tr, :td , :th]
     TAGS_FORCE_PAIR = [:a, :td, :h1, :h2, :h3, :h4, :h5, :h6, :div, :script, :i]
     TAGS_ALLOVED = [:a,
                     :button,
                     :h1, :h2, :h3, :h4, :h5, :h6,
                     :div, :span, :p, :pre,
                     :li, :ul, :ol, :dl, :dt, :dd,
                     :b, :tt, :u, :del, :blockquote, :strong, :em, :sup, :sub, :i,
                     :table,  :tr, :td, :th,
                     :br , :img, :hr,
                     :form, :textarea, :input, :select, :option, :label,
     ]
     TAGS_SKIP_EMPTY = [ :p , :ol, :li, :strong, :em  ]
     ATTRIBUTES_ALLOWED = { :form  =>  [:action, :method],
                            :input =>  [:size, :type, :value, :name],
                            :textarea => [:name, :rows, :cols],
                            :select => [:multiple, :name],
                            :option => [:disabled, :selected, :label, :value, :name],
                            :a     =>  [:name, :href],
                            :img   =>  [:src, :width, :height, :align, :valign, :style, :alt, :title],
                            :td    =>  [:colspan, :rowspan, :style],
                            :th    =>  [:colspan, :rowspan, :style],
                            :ol    =>  [:type ],
                            :label =>  [:for],
                            :_all  =>  [:class, :title, :id, :style],
                           }

     ATTRIBUTE_DATA_REX = /\Adata-[-a-z]+\Z/i
     ATTRIBUTE_STYLE_REX = /\A( text-align:(center|right|left) |
                                margin:    \d+(px|em)? |
                                (background|color):    [-_#a-zA-Z0-9]+ |
                                [\s;]
                             )+\Z/x
     def attrs_to_s(tag, attrs)
       return '' if attrs.nil? || attrs.size == 0
       ret = ['']
       tag_attrs = ATTRIBUTES_ALLOWED[tag] || []
       attrs.each_pair do |k,v|
         #print "a: #{k} #{v}\n"
         next if v.nil?
         k_sym = k.to_sym
         next if ! ( ATTRIBUTES_ALLOWED[:_all].include?(k_sym) || tag_attrs.include?(k_sym) || k =~ ATTRIBUTE_DATA_REX )
         next if k_sym == :style && v !~ ATTRIBUTE_STYLE_REX
         ret.push "#{TracWiki::Parser.escapeHTML(k.to_s)}=\"#{TracWiki::Parser.escapeHTML(v.to_s)}\""
       end
       return ret.sort.join(' ')
     end

     def cont_to_s(cont)
       cont = [cont] if cont.is_a? String
       cont.map do |c|
          if c.is_a? Node
             tree_to_html(c)
          elsif c.is_a? RawHtml
             c.to_s
          else
             TracWiki::Parser.escapeHTML(c.to_s)
          end
        end.join('')
      end

#     def san_conf
#        return @san_conf if @san_conf
#        conf = { elements:   ['tt', 'form', 'input', 'span', 'div'],
#                 output: :xhtml,
#                 attributes: { 'form'  =>  ['action', 'meth'],
#                               'input' =>  ['type', 'value'],
#                               'span'  =>  ['class', 'id'],
#                               'div'   =>  ['class', 'id'],
#                               'a'     =>  ['class', 'id', 'name', 'href'],
#                               :all    =>  ['class', 'id'],
#                             },
#               }
#                   
#        @san_conf = Sanitize::Config::RELAXED.merge(conf){|k,o,n| o.is_a?(Hash) ? o.merge(n) :
#                                                                  o.is_a?(Array) ? o + n :
#                                                                  n }
#
#        #pp @san_conf
#        @san_conf
#     end
    end
end