##
# Borges::HTMLBuilder is a generic class for outputting HTML.

class Borges::HtmlBuilder

  SIMPLE_ELEMENTS = {
    :bold => :b,
    :code => :code,
    :div => :div,
    :emphasis => :em,
    :italic => :i,
    :paragraph => :p,
    :preformatted => :pre,
    :strong => :strong,
    :small => :small,
    :span => :span,
    :sub => :sub,
    :sup => :sup,
    :table_data => :td,
    :table_heading => :th,
    :underline => :u,
  }

  HTML_CHARACTERS = {}

  attr_accessor :document

  attr_reader :attributes

  SIMPLE_ELEMENTS.each do |method, element|
    eval %^
      def #{method}(obj = nil, &block)
        return tag_do(:#{element}, obj) unless obj.nil?
        return tag_do(:#{element}, block)
      end
    ^
  end

  ##
  # Creates a line break.

  def break
    tag(:br)
  end

  ##
  # Close the last open element.

  def close
    @document.pop_element 
  end

  ##
  # Adds a class attribute that contains +classes+

  def css_class(classes)
    @attributes[:class] = classes
  end

  ##
  # Creates a <div> that has an id attribute containing +name+.

  def div_named(name, obj = nil, &block)
    element_id(name)
    div(obj, &block)
  end

  ##
  # Creates a <div> that has an class attribute containing
  # +classes+.

  def div_with_class(classes, obj = nil, &block)
    css_class(classes)
    div(obj, &block)
  end

  ##
  # Adds an id attribute containing +e_id+

  def element_id(e_id)
    @attributes[:id] = e_id
  end

  ##
  # Turns an +obj+ into an HTML encoded string
  #
  # encode "foo > bar" # => "foo &gt; bar"

  def encode(obj)
    obj.to_s.gsub(/./) do |char|
      HTML_CHARACTERS[char[0]]
    end
  end

  ##
  # Adds +obj+ as an HTML encoded string.

  def encode_text(obj)
    text encode(obj)
  end

  ##
  # Creates a <fieldset> labeled +legend+

  def fieldset(legend, &block)
    tag_do(:fieldset, proc do
      tag_do(:legend, legend)
      block.call
    end)
  end

  ##
  # Creates a tag that goes in the <head> of the document with +name+.

  def head_tag(name)
    element = Borges::HtmlElement.new name, @attributes
    @document.head.add element
    @attributes = Borges::HtmlAttributes.new
    return element
  end

  ##
  # Creates a tag that goes in the <head> of the document with
  # +name+, that contains +child+ in its contents.

  def head_tag_with(name, child)
    head_tag(name).add(child)
  end

  ##
  # Creates a level 1 heading
  #
  # heading("My Heading")
  #
  # <h1>My Heading</h1>

  def heading(str)
    heading_level(str, 1)
  end

  ##
  # Creates a heading with level
  #
  # heading_level("My Heading", 2)
  #
  # <h2>My Heading</h2>

  def heading_level(obj, level)
    tag_do("h#{level}", obj)
  end

  ##
  # Adds an <hr> to the document.

  def horizontal_rule
    tag(:hr)
  end

  ##
  # Adds an <img> with +src+ as its src attribute.  If +alt+ is
  # specified, it is used as the image's alternate text,
  # otherwise the alt attribute is left empty.

  def image(src, alt = "")
    @attributes[:src] = src
    @attributes[:alt] = alt
    tag :img
  end
  
  ## 
  # Add an anchor on an <img> with +src+ as its src attribute. If +alt+ is
  # specified, it is used as the image's alternate text,
  # otherwise the alt attribute is left empty. 
  
  def image_url_anchor(url, src, alt='')
    url_anchor(url) do 
      image(src, alt)
    end
  end

  ##
  # Creates a new HtmlBuilder.
  #
  # +document+ must have the following interface:
  #   #head
  #   #add_element
  #   #pop_element
  #   #push_element

  def initialize(document)
    @attributes = Borges::HtmlAttributes.new
    @document = document
  end

  ##
  # Creates an <input> of type +input_type+ with an optional
  # +name+ and +value+.

  def input(input_type, name = nil, value = nil)
    @attributes[:name] = name unless name.nil?
    @attributes[:value] = encode value unless value.nil?
    @attributes[:type] = input_type
    tag :input
  end

  ##
  # Builds an unordered list of items from an Enumerable.  #text
  # will be called on each item.
  # 
  # list ["x", 5, { :x => :y }]
  # 
  # <ul><li>x</li><li>5</li><li>xy</li></ul>

  def list(items)
    list_do(items) do |x| text(x) end
  end

  ##
  # Builds an unordered list of items from an Enumerable.  The
  # given block will be called on each item.
  #
  # See also #list

  def list_do(items, &block)
    tag_do(:ul, proc do
      items.each do |item|
        tag_do(:li, proc do block.call(item) end)
      end
    end)
  end

  ##
  # Adds a new element +name+ to the document with all the
  # attributes specified for it.

  def open_tag(name)
    @document.push_element Borges::HtmlElement.new(name, @attributes)
    @attributes = Borges::HtmlAttributes.new
  end

  ##
  # Renders +obj+.
  #--
  # FIXME: better comment

  def render(obj)
    raise "Rendering nil object" if obj.nil? # XXX catch possible bugs
    obj.render_on(self)
  end

  def set_attribute(attr, val)
    @attributes[attr] = val
  end

  ##
  # Adds a non-breakin space (&nbsp;).

  def space
    text('&nbsp;')
  end

  ##
  # Adds a link to an external stylesheet with +url+.

  def style_link(url)
    @attributes[:rel] = 'stylesheet'
    @attributes[:type] = 'text/css'
    @attributes[:href] = url
    @attributes[:media] = 'all'
    head_tag(:link)
  end

  ##
  # Creates a <table> element.

  def table(&block)
    tag_do(:table, block)
  end

  ##
  # Creates a row of table headings from +headings+

  def table_headings(*headings)
    table_row do
      headings.each do |heading|
        table_heading(heading)
      end
    end
  end

  ##
  # Creates a table row.

  def table_row(&block)
    tag_do(:tr, block)
  end

  ##
  # Creates a table row with a table cell that spans +span+
  # columns.
  #--
  # FIXME: This has a funky name

  def table_row_span(span, &block)
    table_row do
      @attributes[:colspan] = span
      table_data(&block)
    end
  end

  ##
  # Creates a table row containing a single data cell.

  def table_row_with(obj = nil, &block)
    table_row do table_data(obj || block) end
  end

  ##
  # Creates a table row with +label+ in the first cell.

  def table_row_labeled(label, obj = nil, &block)
    table_row do
      css_class(:label)
      table_data(label)
      table_data(obj, &block)
    end
  end

  ##
  # Creates an empty table row.

  def table_spacer_row
    table_row do space end
  end

  ##
  # Creates an empty element with +name+

  def tag(name)
    open_tag(name)
    close
  end

  ##
  # Creates a +name+ element containing +obj+

  def tag_do(name, obj)
    open_tag(name)
    render(obj)
    close
  end

  ##
  # Turn an object into a string.  It is _not_ recommened to use this method
  # to dump raw HTML.
  #
  # text("hello world")
  #
  # hello world

  def text(obj)
    @document.add_element(obj.to_s)
  end

  ##
  # Sets the <title> of the document to +str+

  def title(str)
    head_tag_with(:title, str)
  end

  ##
  # Creates an anchor that links to +url+.

  def url_anchor(url, obj = nil, &block)
    @attributes[:href] = url
    return tag_do(:a, obj) unless obj.nil?
    return tag_do(:a, block)
  end

=begin
  def anchorWithUrl_title_do(urlString, titleString, &block)
    attributes.title(titleString)
    anchorWithUrl_do(urlString, &block)
  end

  ##
  # XXX Same as Hash#update

  def attributes=(attrs)
    @attributes ||= Borges::HtmlAttributes.new

    attrs.each do |key, val|
      @attributes[key] = val
    end
  end

  def buttonForUrl_withText(urlString, labelString)
    buttonForUrl_withText_data(urlString, labelString, [])
  end

  def buttonForUrl_withText_data(urlString, labelString, assocCollection)
    formWithMethod_action_do('GET', urlString, proc do

      assocCollection.each do |each|
        inputWithType_named_value('hidden', each.key, each.value)
      end

      submitButtonWithText(labelString)
    end)
  end

  def doesNotUnderstand(aMessage)
    argCount = aMessage.arguments.size
    if argCount == 0 then
      return tag(aMessage.selector)
    end

    if argCount == 1 then
      return tag_do(aMessage.selector.allButLast, aMessage.argument)
    end

    return super.doesNotUnderstand(aMessage)
  end

  def formWithAction_do(actionUrl, &block)
    formWithMethod_action_do('POST', actionUrl, &block)
  end

  def formWithMethod_action_do(methodString, actionUrl, &block)
    attributes.method(methodString)
    attributes.action(actionUrl)
    openTag('form')
    block.call
    close
  end

  ##
  # HTML4 image must have alt attribute

  def image_width_height(urlString, width, height)
    attributeAt_put('width', width)
    attributeAt_put('height', height)
    
    image(urlString)
  end

  def image_width_height_altText(urlString, width, height, altString)
    attributeAt_put('alt', altString)
    
    image_width_height(urlString, width, height)
  end

  def inputWithType(input_type)
    inputWithType_named(input_type, nil)
  end

  def inputWithType_named(input_type, name)
    inputWithType_named_value(input_type, name, nil)
  end

  def layoutTable(&block)
    attributes.border(0)
    attributes.cellspacing(0)
    attributes.cellpadding(0)
    table(&block)
  end

  def layoutTableOfWidth_do(width, &block)
    attributes.width(width)
    layoutTable(&block)
  end

  def metaTagNamed_content(nameString, contentString)
    attributes.name(nameString)
    attributes.content(contentString)
    headTag('meta')
  end

  def self.on(aDocument)
    inst = self.new
    inst.document = aDocument
    return inst
  end

  def scriptWithUrl(urlString)
    attributes.language('javascript')
    attributes.src(urlString)
    headTag('script')
  end

  def spanClass_with(aString, anObject)
      cssClass(aString)
      span(anObject)
  end

  def spanNamed_with(aString, anObject)
    cssId(aString)
    span(anObject)
  end

  def submitButton
    inputWithType('submit')
  end

  def submitButtonWithText(aString)
    attributes.value(aString)
    inputWithType('submit')
  end

  def tableRowWith(&block)
    tableRow do
      tableData(&block)
    end
  end

  def tableRowWith_with(aBlock, anotherBlock)
    tableRow do
      tableData(aBlock)
      tableData(anotherBlock)
    end
  end

  def tableRowWith_with_with(x, y, z)
    tableRow do
      tableData(x)
      tableData(y)
      tableData(z)
    end
  end

  def tableRowWithLabel_column_column(anObject, aBlock, anotherBlock)
    tableRow do
      cssClass('label')
      tableData(anObject)
      tableData(aBlock)
      tableData(anotherBlock)
    end
  end
=end

  ##
  # HtmlBuilder initialize

  (0..8).each do |i| HTML_CHARACTERS[i] = "&##{i};" end
  (9..13).each do |i| HTML_CHARACTERS[i] = i.chr end
  (14..31).each do |i| HTML_CHARACTERS[i] = "&##{i};" end
  (32..126).each do |i| HTML_CHARACTERS[i] = i.chr end
  (127..159).each do |i| HTML_CHARACTERS[i] = "&##{i};" end

  {'quot' => '"', 'lt' => '<', 'amp' => '&', 'gt' => '>'}.each do |s, c|
    HTML_CHARACTERS[c[0]] = "&#{s};"
  end

  %w(nbsp iexcl cent pound curren yen brvbar sect uml copy ordf
    laquo not shy reg macr deg plusmn sup2 sup3 acute micro para
    middot cedil sup1 ordm raquo frac14 frac12 frac34 iquest
    Agrave Aacute Acirc Atilde Auml Aring AElig Ccedil Egrave
    Eacute Ecirc Euml Igrave Iacute Icirc Iuml ETH Ntilde Ograve
    Oacute Ocirc Otilde Ouml times Oslash Ugrave Uacute Ucirc
    Uuml Yacute THORN szlig agrave aacute acirc atilde auml aring
    aelig ccedil egrave eacute ecirc euml igrave iacute icirc
    iuml eth ntilde ograve oacute ocirc otilde ouml divide oslash
    ugrave uacute ucirc uuml yacute thorn yuml).each_with_index do |s, i|
      HTML_CHARACTERS[(i + 160)] = "&#{s};"
  end

end

