#!/usr/bin/ruby

# Written by J. David Eisenberg
# 1 December 2009
# Apache License, as with other Android stuff.

require 'rubygems'
require 'nokogiri'
require 'open-uri'

#
# This is the new path for opening files
#
BASE_PATH = "/usr/local/android/docs/reference"

# All the URLs in the documentation will be adjusted to
# be relative to this RELATIVE_PATH
# You may be thinking: "Wait a moment. This
# directory is already inside the reference directory,
# so why do ../../, and then re-enter the reference
# directory? Wouldn't ../ get you there more easily?
#
# Yes, it would, but all the current HTML files do
# the same thing (backing out one extra level, then
# re-entering the reference directory). The people
# at Google must have done this for a compelling reason,
# and who am I to question their wisdom?
RELATIVE_PATH = "../../reference"

# An array of hashes. The hash keys are:
# :name         the name of the entity (used for sorting)
# :prototype  the viewable prototype of the entity,
#               including link to its definition
# :class_name the class name
# :class_href URL leading to the page with class definition
# :access   public or protected
# :type     return type for methods; type for constants; "" for constructors
# :level    api level; "" for apilevel-
# :description the description of the entity
entities = []

#
# Get the api level from a class attribute
# The attribute values of apilevel-1 to apilevel-5
# return the integer value. Some
# elements have class="apilevel-" with no
# number. To be consistent, this method
# returns the null string. In all other
# cases, the method returns zero.
#
def get_level(element)
  level = 0
  if element[:class] then
    if /apilevel-(\d+)/ =~ element[:class] then
      level = Regexp.last_match[1].to_i
    elsif /apilevel-/ =~ element[:class] then
      level = ""
    end
  end
  return level
end

#
# Replace everything in the path up to the word "reference"
# with the new leading part of the path.
#
def adjust_path(path, new_start)
  return path.gsub(%r|^[./]+/reference|, new_start)
end

#
# Replace the value of the href="" attribute,
# up to and including the word "reference"
# with the new leading part of the path.
# Do this for all hrefs in the string.
#
def adjust_href(href, new_start)
  return href.gsub(%r|href="[./]+/reference|,
    "href=\"" + new_start)
end

#
# Collect all constants from the given <table> and
# add them to the entities array
#
def collect_constants(class_name, class_href, table, entities)
  rows = table.search("tr")
  rows.shift  # get rid of the header row
  
  rows.each do |row|
    level = get_level(row) # the <tr> element contains the api level
    
    # The first cell contains the constant's type.
    # Get rid of leading and trailing blanks, and replace
    # runs of whitespace with a single blank. The constant's
    # type may contain links, so adjust them.
    type = row.at("td[1]").inner_html.strip
    type = adjust_href(type.gsub(/\s+/, " "), RELATIVE_PATH)
    
    # The second cell is a link to the constant's definition.
    cell = row.at("td[2]")
    link = row.at("td[2]//a")
    
    # The link to the constant contains the name of the constant
    # following the #. The 'prototype' for a constant is the
    # inner HTML for the cell.
    name = link[:href].sub(/^[^#]+#/, "")
    prototype = adjust_path(cell.inner_html, RELATIVE_PATH)
    
    # The description may contain links; adjust them
    description = adjust_href(row.at("td[3]").inner_html, RELATIVE_PATH)
    
    entities << {
      :class_name => class_name, :class_href => class_href,
      :name => name, :prototype => prototype,
      :access => "public", :type => type, :level => level, :description => description
    }
  end
end

#
# Collect all constructors from the given <table> and
# add them to the entities array
#
def collect_constructors(class_name, class_href, table, entities)
  type = "" # constructors do not have a return type
  
  rows = table.search("tr")
  rows.shift  # get rid of the header row
  rows.each do |row|
    level = get_level(row) # the <tr> element contains the api level
    
    # The second cell contains a link to the constructor's definition.
    # (and it is always the first link in the cell)
    # the sorting name follows the # in that link.
    cell = row.at("td[2]")
    link = row.at("td[2]//a[1]")
    name = link[:href].sub(/^[^#]+#/, "")
    
    # The prototype is the inner html of the <nobr> content
    prototype = adjust_href(cell.at("nobr").inner_html, RELATIVE_PATH)
    
    # and the description is in the <div class="jd-descrdiv">
    description = cell.at(".//div[@class='jd-descrdiv']")
    if (description != nil) then
      description = adjust_href(description.inner_html, RELATIVE_PATH)
    else
      description = ""
    end
    
    entities << {
      :class_name => class_name, :class_href => class_href,
      :name => name, :prototype => prototype,
      :access => "public", :type => type, :level => level,
      :description => description
    }
  end
end

#
# Collect all constructors from the given <table> and
# add them to the entities array
#
def collect_methods(class_name, class_href,table, access, entities)
  rows = table.search("tr")
  rows.shift  # get rid of the header row
  rows.each do |row|
    level = get_level(row)  # the <tr> element contains the api level
    
    # The first cell contains the method's return type, which
    # is stripped of leading/trailing whitespace. Runs of
    # whitespace are replaced by blanks, and any links are
    # adjusted.
    type = row.at("td[1]/nobr").inner_html.strip
    type = adjust_href(type.gsub(/\s+/, " "), RELATIVE_PATH)
    
    # The second cell contains a link to the constructor's definition.
    # (and it is always the first link in the cell)
    # the sorting name follows the # in that link.
    cell = row.at("td[2]")
    link = row.at("td[2]//a[1]")
    name = link[:href].sub(/^[^#]+#/, "")

    # The prototype is the inner html of the <nobr> content
    prototype = adjust_href(cell.at("nobr").inner_html, RELATIVE_PATH)
    
    # and the description is in the <div class="jd-descrdiv">
    description = cell.at(".//div[@class='jd-descrdiv']")
    if (description != nil) then
      description = adjust_href(description.inner_html, RELATIVE_PATH)
    else
      description = ""
    end
    
    entities << {
      :class_name => class_name, :class_href => class_href,
      :name => name, :prototype => prototype,
      :access => access, :type => type, :level => level,
      :description => description
    }
  end
end

def collect_entities(class_name, class_href, entities)
  STDERR.puts "Processing class: #{class_name}"
  doc = Nokogiri::HTML(open(adjust_path(class_href, BASE_PATH)))
  
  #
  # All the definitions are in tables with class="jd-sumtable"
  #
  tables = doc.xpath("/html/body//div[@class='jd-descr']//table[@class='jd-sumtable']")
  tables.each do |table|
    id = table[:id]
    if (id == "constants") then
      collect_constants(class_name, class_href, table, entities)
    elsif (id == "pubctors") then
      collect_constructors(class_name, class_href, table, entities)
    elsif (id == "pubmethods") then
      collect_methods(class_name, class_href, table, "public", entities)
    elsif (id == "promethods") then
      collect_methods(class_name, class_href, table, "protected", entities)
    end
  end
end


# Insert the alphabet letter links at the top of
# every page. The current letter does not become a link.
#
def insert_letters(current_letter)
  result = ""
  larray = ('a'..'z').to_a
  larray << '_'
  result = %Q|<div class="jd-letterlist">\n|
  larray.each do |letter|
    if (letter != current_letter) then
      result += %Q|<a href="methods-#{letter}.html">#{letter.upcase}</a>\n|
    else
      result += "#{letter.upcase}\n"
    end
  end
  return result
end

#
# Insert a table row for each entry in the
# entities array from previous_pos up to but not
# including current_pos.
def insert_entities(entities, previous_pos, current_pos)
  i = previous_pos
  result = ""
  (previous_pos...current_pos).each do |i|
  
    # if the protoype contains a parenthesis, it's a
    # constructor or method. Display prototype and description.
    if entities[i][:prototype] =~ /\(/ then
      result += <<"METHOD"
<tr class=" api apilevel-#{entities[i][:level]}">
  <td class="jd-linkcol" width="100%">#{entities[i][:prototype]}
    <div class="jd-descrdiv">#{entities[i][:description]}</div>
  </td>
  <td class="jd-descrcol"><a 
    href="#{entities[i][:class_href]}">#{entities[i][:class_name]}</a>
METHOD
    else
    # otherwise, it's a constant
    # Display prototype and description and type of constant.
      result += <<"CONSTANT"
<tr class=" api apilevel-#{entities[i][:level]}">
  <td class="jd-linkcol" width="100%">#{entities[i][:prototype]}
    <div class="jd-descrdiv">#{entities[i][:description]}</div>
    <div class="jd-descrdiv">#{entities[i][:type]}</div>
  </td>
  <td class="jd-descrcol"><a 
    href="#{entities[i][:class_href]}">#{entities[i][:class_name]}</a>
CONSTANT
    end
    if entities[i][:access] == "protected" then
    	result += %Q|<div class="jd-descrdiv">(protected)</div>|
    end
    result += %Q|  </td>\n</tr>\n\n|
  end
  return result
end

#
# Emit a page that contains all the entities for the given
# letter; these are entries previous_pos...current_pos in
# the entities array.
def emit_page(entities, letter, previous_pos, current_pos)
  STDERR.puts "Emitting #{letter} from #{previous_pos} to #{current_pos}"
  if (current_pos != previous_pos) then
    infile = open("template.html", "r")
    outfile = open("methods-#{letter}.html", "w")
    
    # Use eval to interpolate expressions. Yes, I know
    # that eval is evil, and this is undoubtedly the worst
    # possible way to accomplish the goal, but it works.
    infile.each do |line|
      line.gsub!(/#\{([^}]+)\}/) { |match| eval(Regexp.last_match[1]) }
      outfile.puts line
    end
    
    outfile.close
    infile.close
  end
end

#
# Get all of the class names from the classes.html page
#
doc = Nokogiri::HTML(open("#{BASE_PATH}/classes.html"))

links = doc.xpath("//table[@class='jd-sumtable']/tr/td[@class='jd-linkcol']")

links.each do |link|
  class_href = adjust_path(link.at("a")[:href], RELATIVE_PATH)
  class_name = link.at("a/text()").to_s
  if (class_name != "R.styleable") then
    collect_entities(class_name, class_href, entities)
  end
end

STDERR.puts "Number of items: #{entities.length}"
#
# Sort by name. If names are equal, sort by class name
#
entities = entities.sort do |a, b|
  result = (a[:name].upcase <=> b[:name].upcase)
  if (result == 0)
    result = (a[:class_name] <=> b[:class_name])
  end
  result
end

#
# Emit the index pages. Every time the first letter
# of an entity changes, that means a previous page
# has finished and should be written.
previous_pos = 0
previous_letter = ""
letter = ""
entities.each_index do |i|
  letter = entities[i][:name][0,1].downcase
  if (letter != previous_letter) then
    emit_page(entities, previous_letter, previous_pos, i)
    previous_pos = i
    previous_letter = letter
  end
end

# Handle the last page of entities
if (previous_pos != entities.length) then
  emit_page(entities, previous_letter, previous_pos, entities.length)
end
