#!/usr/bin/env ruby

# ================================================================
# Poki: A poor man's wiki generator
# Code: https://github.com/johnkerl/poki
# Docs: http://johnkerl.org/poki/doc

# Customized for Miller 2019-09-15

# John Kerl
# kerl.john.r@gmail.com
# 2015-04-26
# ================================================================

$us                           = File.basename $0
$default_config_file_name     = "poki-cfg.json"
$output_file_mode_during_edit = 0644;
$output_file_mode_after_edit  = 0444;

require 'getoptlong'
require 'fileutils'
require 'json'

# ================================================================
def usage()
   $stderr.puts <<EOF
Usage: #{$us} [options]
Options:
-c {config file}         Default #{$default_config_file_name}
-h|--help                Print this message
Please see ./doc in poki's source directory
https://github.com/johnkerl/poki
https://johnkerl.org/poki/doc
EOF
end

# ================================================================
def main()
  config_file_name    = $default_config_file_name

  opts = GetoptLong.new(
    [ '-c', GetoptLong::REQUIRED_ARGUMENT ],
    [ '-t', GetoptLong::REQUIRED_ARGUMENT ],
    [ '-p', GetoptLong::REQUIRED_ARGUMENT ],
    [ '-h','--help', GetoptLong::NO_ARGUMENT ]
  )

  begin
    opts.each do |opt, arg|
      case opt
        when '-c'; config_file_name = arg
        when '-h'; usage
        when '--help'; usage
      end
    end
    rescue GetoptLong::Error
      usage
  end
  usage if ARGV.length != 0

  overall_config = load_config_file(config_file_name)

  generate_pages(overall_config)
end

# ================================================================
# Sample config file:

# {
#   "main_title": "Main Title",
#   "sections": [
#     {
#       "external_name": "Overview",
#       "internal_name": "overview",
#       "pages": [
#         {
#           "external_name": "About",
#           "file_name": "index.html"
#         },
#         {
#           "external_name": "Repo",
#           "link": "http://github.com/foo/bar"
#         }
#       ]
#     }
#   ]
# }

def load_config_file(config_file_name)
  JSON.load File.read config_file_name
end

# ================================================================
# Note: we use map.fetch('key') throughout so it will raise KeyError on missing
# values, rathre than map['key'] which will return nil.
def generate_pages(overall_config)
  for section_config in overall_config.fetch('sections')
    for page_config in section_config.fetch('pages')

      # External links aren't associated with a local input HTML file.
      next unless page_config['file_name'] # nullable

      input_html_path = "#{overall_config.fetch('content_file_prefix')}#{page_config.fetch('file_name')}"
      output_html_name = page_config.fetch('file_name')
      generate_page(overall_config, section_config, page_config, input_html_path, output_html_name)

    end
  end
end

# ----------------------------------------------------------------
def generate_page(overall_config, section_config, page_config, input_html_path, output_html_name)
  if test(?f, output_html_name)
    FileUtils.chmod($output_file_mode_during_edit, output_html_name)
  end
  template_file_name = overall_config.fetch('template_file_name')
  page_title = page_config.fetch('external_name')

  File.open(output_html_name, "w", $output_file_mode_during_edit) do |output_handle|
    File.readlines(template_file_name).each do |template_line|

      # Template directive: Mark autogenerated pages as autogenerated.
      if template_line =~ /POKI_PUT_AUTOGEN_DISCLAIMER_HERE/
        output_handle.puts(template_line.sub('POKI_PUT_AUTOGEN_DISCLAIMER_HERE', ''))
        output_handle.puts "<!-- PAGE GENERATED FROM #{template_file_name} and #{input_html_path} BY #{$us}. -->"
        output_handle.puts "<!-- PLEASE MAKE CHANGES THERE AND THEN RE-RUN #{$us}. -->"

      # Template directive: For navbar.
      elsif template_line =~ /POKI_PUT_NAVBAR_HERE/
        output_handle.puts(template_line.sub('POKI_PUT_NAVBAR_HERE', ''))
        generate_navbar(overall_config, section_config, page_config, output_html_name, output_handle)

      # Template directive: Page titles in browser window title, as well as page-content title.
      elsif template_line =~ /POKI_PUT_TITLE_HERE/
        output_handle.puts(template_line.sub('POKI_PUT_TITLE_HERE', page_title))

      # Template directive: Page body.
      elsif template_line =~ /POKI_PUT_BODY_HERE/
        output_handle.puts(template_line.sub('POKI_PUT_BODY_HERE', ''))
        generate_page_body(overall_config, section_config, page_config, input_html_path, output_handle)

      # Absent any other directives, copy the template file to the output file.
      else
        output_handle.write(template_line)

      end
    end
    FileUtils.chmod($output_file_mode_after_edit, output_html_name)
    puts "Generated #{output_html_name}"
  end
end

# ----------------------------------------------------------------
def generate_navbar(overall_config, section_config, page_config, output_html_name, output_handle)
  template_file_name = overall_config.fetch('template_file_name')
  output_handle.puts "<!-- NAVBAR GENERATED FROM #{template_file_name} BY #{$us} -->"

  for other_section_config in overall_config.fetch('sections')
    other_section_internal_name = other_section_config.fetch('internal_name')
    other_section_external_name = other_section_config.fetch('external_name')
    font_weight = 'normal'
    if (other_section_internal_name == section_config.fetch('internal_name'))
      font_weight = 'bold'
    end
    output_handle.puts("<button");
    output_handle.puts("  style=\"font-weight:#{font_weight};font-size:100%;color:maroon;border:0\"");
    output_handle.puts("  padding=0");
    output_handle.puts("  onclick=\"vis_collapse_all_navbar_sections(); vis_toggle_by_name('navbar_section_toggle_#{other_section_internal_name}');\"");
    output_handle.puts("  href=\"javascript:;\"");
    output_handle.puts("  >");
    output_handle.puts("    #{other_section_external_name}");
    output_handle.puts("</button>");
  end

  for other_section_config in overall_config.fetch('sections')
    other_section_internal_name = other_section_config.fetch('internal_name')
    other_section_external_name = other_section_config.fetch('external_name')
    display = 'none'
    if (other_section_internal_name == section_config.fetch('internal_name'))
      display = 'block'
    end
    output_handle.puts("<div id=\"navbar_section_toggle_#{other_section_internal_name}\" style=\"display: #{display};text-align: left\">")
    for other_page_config in other_section_config.fetch('pages')
      other_page_file_name = other_page_config['file_name'] # nullable
      other_page_link_name = other_page_config['link'] # nullable
      other_page_external_name = other_page_config.fetch('external_name')

      if other_page_link_name != nil
        url = other_page_link_name.sub(/^ext:/, '')
        output_handle.puts "<br/><a href=\"#{url}\">#{other_page_link_name}</a>"
      else
        bs = ""
        be = ""
        if File.basename(output_html_name) == other_page_file_name
          # Bold the current file in the navbar.
          bs = "<b>"
          be = "</b>"
        end
        output_handle.puts "<br/><a href=\"#{other_page_file_name}\">#{bs}#{other_page_external_name}#{be}</a>"
      end
    end
    output_handle.puts("</div>")
  end
end

# ----------------------------------------------------------------
def generate_page_body(overall_config, section_config, page_config, input_html_path, output_handle)
  output_handle.puts "<!-- BODY COPIED FROM #{input_html_path} BY #{$us} -->"
  File.readlines(input_html_path).each do |content_line|

    # Page-content directive: table of contents.
    if content_line =~ /POKI_PUT_TOC_HERE/
      generate_toc(overall_config, section_config, page_config, input_html_path, output_handle)

    # Page-content directive: include other file (do HTML escapes)
    elsif content_line =~ /POKI_INCLUDE_ESCAPED\(([^)]+)\)HERE/
      included_file_name = $1
      include_escaped(included_file_name, output_handle)

    # Page-content directive: include other file (do HTML escapes) and print its output
    elsif content_line =~ /POKI_INCLUDE_AND_RUN_ESCAPED\(([^)]+)\)HERE/
      included_file_name = $1
      cmd = File.readlines(included_file_name).join('')
      run_command(cmd, output_handle, true)

    # Page-content directive: run a script which generates HTML and print its output
    elsif content_line =~ /POKI_RUN_UNESCAPED\(([^)]+)\)HERE/
      included_file_name = $1
      cmd = File.readlines(included_file_name).join('')
      run_command(cmd, output_handle, false)

    # Page-content directive: include other file (do HTML escapes) and print its output
    elsif content_line =~ /POKI_RUN_CONTENT_GENERATOR\(([^)]+)\)HERE/
      cmd = $1
      run_content_generator(cmd, output_handle)

    # Page-content directive: format as if included from a file.
    elsif content_line =~ /POKI_CARDIFY\(([^)]+)\)HERE/
      content_line = $1
      cardify(content_line, output_handle, true)
    elsif content_line =~ /POKI_CARDIFY{{([^)]+)}}HERE/
      content_line = $1
      cardify(content_line, output_handle, true)

    elsif content_line =~ /POKI_CARDIFY{{(.+)}}HERE/
      content_line = $1
      cardify(content_line, output_handle, true)

    # Page-content directive: include other file (do HTML escapes)
    elsif content_line =~ /POKI_RUN_COMMAND{{(.+)}}HERE/
      cmd = $1
      run_command(cmd, output_handle, true)

    # Page-content directive: include other file (do HTML escapes)
    elsif content_line =~ /POKI_RUN_COMMAND_TOLERATING_ERROR{{(.+)}}HERE/
      cmd = $1
      run_command_tolerating_error(cmd, output_handle)

    # Page-content directive: link to sibling in pageset.
    elsif content_line =~ /POKI_PUT_LINK_FOR_PAGE\(([^)]+)\)HERE/
      other_page_link = $1
      other_page_name = other_page_link.sub(/#.*/, '')

      other_page_title = nil
      for loop_section_config in overall_config.fetch('sections')
        for loop_page_config in loop_section_config.fetch('pages')
          loop_page_file_name = loop_page_config['file_name'] # nullable
          loop_page_external_name = loop_page_config.fetch('external_name')
          if other_page_name == loop_page_file_name
            other_page_title = loop_page_external_name
          end
        end
      end

      if other_page_title.nil?
        raise "Couldn't find page title for \"#{other_page_name}\"."
      end
      href = "<a href=\"#{other_page_link}\">#{other_page_title}</a>"
      content_line.sub!(/POKI_PUT_LINK_FOR_PAGE\([^)]+\)HERE/, href)
      output_handle.puts(content_line)

    # Page-content directive: automark <h1>, <h2>, etc. with jump-to tags so they can be found by the
    # table of contents. Example: <h1>Title</h1> becomes <h1>Title</h1> <a id="#Title">
    elsif content_line =~ /<h([1-9])>(.*)<\/h[1-9]>/
      tag = $2.strip
      # If there's a section with a space in it (e.g. "Naming conventions") then
      # make the href text the same but underscorify the link itself (e.g.
      # "Naming_conventions").
      output_handle.print("<a id=\"#{tag.gsub(' ', '_')}\"/>")
      output_handle.write(content_line)

    else
      output_handle.write(content_line)
    end
  end
end

# ----------------------------------------------------------------
def generate_toc(overall_config, section_config, page_config, input_html_path, output_handle)
  # Read all the <h1>, <h2>, <h3>, etc. from the current file and create internal links
  tags   = []
  depths = {}
  File.readlines(input_html_path).each do |scan_line|
    if scan_line =~ /^<h([1-9])>(.*)<\/h[1-9]>/
      depth = $1
      tag   = $2.strip
      tags << tag
      depths[tag] = depth.to_i - 1
    end
  end

  page_title = page_config.fetch('external_name')
  page_subtitle = page_config['external_subname'] # nullable
  output_handle.puts('<div class="pokitoc">')
  if page_subtitle.nil?
    output_handle.puts("<center><titleinbody>#{page_title}</titleinbody></center>")
  else
    output_handle.puts("<center><titleinbody>#{page_title}: #{page_subtitle}</titleinbody></center>")
  end
  tags.each do |tag|
    # Indent h2 more than h1, h3 more than h2, etc.
    depths[tag].times{output_handle.write "&nbsp;&nbsp;&nbsp;&nbsp;"}
    output_handle.write "&bull;&nbsp;"
    # If there's a section with a space in it (e.g. "Naming conventions") then
    # make the href text the same but underscorify the link itself (e.g.
    # "Naming_conventions").
    output_handle.write "<a href=\"##{tag.gsub(' ', '_')}\">#{tag}</a>"
    output_handle.puts "<br/>"
  end
  output_handle.puts("</div>")
  output_handle.puts("<p/>")
end

# ----------------------------------------------------------------
def include_escaped(included_file_name, output_handle)
  write_card(File.readlines(included_file_name), output_handle, true)
end

# ----------------------------------------------------------------
def run_command(cmd, output_handle, do_escape)
  cmd_output = `#{cmd} 2>&1`
  status = $?.to_i
  if status != 0
    raise "\"#{cmd}\" exited with non-zero code #{status}."
  end
  write_card(['$ '+cmd] + cmd_output.split(/\n/), output_handle, do_escape)
end

# ----------------------------------------------------------------
def run_command_tolerating_error(cmd, output_handle)
  cmd_output = `#{cmd} 2>&1`
  write_card(['$ '+cmd] + cmd_output.split(/\n/), output_handle, true)
end

# ----------------------------------------------------------------
def run_content_generator(cmd, output_handle)
  cmd_output = `#{cmd} 2>&1`
  status = $?.to_i
  if status != 0
    raise "\"#{cmd}\" exited with non-zero code #{status}."
  end
  output_handle.puts(cmd_output)
end

# ----------------------------------------------------------------
def cardify(content_line, output_handle, do_escape)
  write_card([content_line], output_handle, do_escape)
end

# ----------------------------------------------------------------
def write_card(content_lines, output_handle, do_escape)
  output_handle.puts('<p/>')
  output_handle.puts('<div class="pokipanel">')
  output_handle.puts('<pre>')
  content_lines.each do |content_line|
    if do_escape
      output_handle.puts(html_escape_line(content_line))
    else
      output_handle.puts(content_line)
    end
  end
  output_handle.puts('</pre>')
  output_handle.puts('</div>')
  output_handle.puts('<p/>')
end

# ----------------------------------------------------------------
def html_escape_line(line)
  line.gsub("&", "&amp;").gsub("<", "&lt;").gsub(">", "&gt;").rstrip
end

# ================================================================
# Top-down programming style, please.
main()
