#!/usr/bin/env ruby

# Copyright (C) 2023 Dave Gauer <dave@ratfactor.com>
# This program is free software. Please see the LICENSE file.
 
# Required modules from Ruby Std-Lib (not gems)
require 'fileutils' # For mkdir_p, cp, mv
require 'cgi'       # For CGI.escapeHTML

# .git must exist (we must already be at root of repo)
if !Dir.exist?('.git')
  puts "ERROR: Could not access .git directory."
  puts "Please run this program from the root of a repo."
  exit 3
end

# This program must be run from the root of a repo.
repo_dir = Dir.pwd
name = File.basename(repo_dir)

# Load and check config file
config_file = "#{ENV['HOME']}/.config/reporat.conf.rb"
if !File.exist?(config_file)
  puts "ERROR: Could not find config file '#{config_file}'."
  puts "See README.md for example."
  exit 1
end
require config_file
[:my_output_dir, :my_header, :my_footer].each do |m|
  if !self.respond_to?(m, :include_private)
    puts "Config file must define method '#{m}'."
    exit 2
  end
end

# Call the first config method to get output directory
root_output_dir = my_output_dir()

# Get the repo description (almost certainly exists)
description = ''
if File.exist?('.git/description')
  description = File.read('.git/description')
end

puts "RepoRat generating site for:"
puts "    #{name}"
puts "    #{description}"

# Now assemble output paths based on name
output_dir = "#{root_output_dir}/#{name}"
bare_output_dir = "#{output_dir}/#{name}.git"

# Get list of files in repo HEAD
# (Method cribbed from repo2html by m455.)
file_list_str = `git ls-tree -r --name-only HEAD`
file_list = file_list_str.split("\n").map(&:chomp)

# Detect README (very strict naming) and type of formatting
readme_type = 'text'
readme_file = file_list.find { |f| f.match?(/\bREADME(\.md)?$/) }
if !readme_file
  puts "ERROR: Couldn't find README or README.md"
  puts "(It must exist and be comitted to the Git repo.)"
  exit 7
end
if readme_file.match?(/\.md$/)
  readme_type = 'markdown'
end

# Prompt to create repo output directory if it doesn't exist yet
if !Dir.exist?(output_dir)
  puts "Output directory '#{output_dir}' does not yet exist."
  puts "Let's create it and populate it with a \"bare\" Git repo."
  answer = nil
  until answer == 'y' or answer == 'n'
    print "Proceed? (y/n) "
    answer = $stdin.gets.chomp
  end
  if answer == 'y'
    # Create dir(s)! mkdir_p creates subdirs as needed
    puts "Creating '#{output_dir}'..."
    FileUtils.mkdir_p(output_dir)
    # Create bare repo suitable for "dumb http" git cloning.
    # (No need to puts here since Git says what it's doing)
    `git clone --bare . #{bare_output_dir}`
  else
    puts "Okay, exiting!"
    exit 6
  end
end

# Make raw source files dir
raw_dir = "#{output_dir}/raw"
FileUtils.mkdir_p(raw_dir)

# Make html source files dir
html_dir = "#{output_dir}/html"
FileUtils.mkdir_p(html_dir)

# Is this file readable by humans?
# Returns empty string if readable, otherwise a reason it's not.
def is_readable(fname)
  max_readable_avg = 100 # As determined by me :-)
  lens = []
  total_lens = 0
  longest_line = 0
  control_chars = false

  File.open(fname) do |f|
    line_len = 0
    f.each_char do |c|

      # If found newline, count
      line_len += 1
      if c == "\n"
        lens.push line_len
        total_lens += line_len
        line_len = 0
      end

      # First byte of encoded char
      b = c.bytes[0]
      if b < 9
        control_chars = true
      end
    end

    avg = total_lens / (lens.length+1)

    if control_chars
      return "it contains one or more control characters"
    end

    if avg > 100
      return "the average line length, <b>#{avg}</b> chars, is too long"
    end

    return ""
  end
end

file_page_count = 0
file_binary_count = 0

# Each file in repo...
file_list.each do |fname|
  file_page_count += 1

  # Make raw file subdir as needed
  file_dir = File.dirname("#{raw_dir}/#{fname}")
  if !Dir.exist?(file_dir)
    puts "Creating directory '#{file_dir}'..."
    FileUtils.mkdir_p(file_dir)
  end

  # copy source file to output
  FileUtils.cp(fname, "#{raw_dir}/#{fname}")

  # Make HTML output subdir as needed
  file_html_dir = File.dirname("#{html_dir}/#{fname}")
  if !Dir.exist?(file_html_dir)
    puts "Creating directory '#{file_html_dir}'..."
    FileUtils.mkdir_p(file_html_dir)
  end

  # make an html file for this file
  html_out = "#{html_dir}/#{fname}.html"

  # Figure out a path relative to the mini-site's root
  rrp = '../' * (html_out.count('/') - raw_dir.count('/'))

  File.open(html_out, 'w') do |f|
    f.puts my_header({
      name: name,
      description: description,
      page_type: :file,
      root_rel_prefix: rrp,
      file_fname: fname,
    })

    f.puts "<h2>#{name}/#{fname}</h2>"

    # link to raw source file
    f.puts "<p>Download raw file: <a href=\"#{rrp}raw/#{fname}\">#{fname}</a></p>"

    # if image, display in page
    if fname.end_with?(".jpg", ".gif", ".png", ".svg")

      f.puts "<img src=\"#{rrp}raw/#{fname}\" alt=\"\" style=\"margin: 2em auto; display: block;\">"
    end

    # Human-readable file? ("" or string contains reason it isn't):
    readability = is_readable(fname)

    # if source, display with line nums
    if readability == ""
      f.puts "<div class=\"source-file\">"
      ln = 0
      src_txt = File.read(fname) rescue fail("Couldn't read #{fname}")
      src_txt.each_line do |l|
        ln += 1
        hl = CGI.escapeHTML(l)
        lns = " " * (6 - ln.to_s.length)
        f.print "<a id=\"L#{ln}\" href=\"#L#{ln}\">#{lns}#{ln} </a>#{hl}"
      end
      f.puts "</div>"
    else
      file_binary_count += 1
      f.puts "<div>(This file was determined to not be human-readable because #{readability}.)</div>"
    end

    f.puts my_footer()
  end
end

# Create file list page.
files_page_out = "#{output_dir}/files.html"

File.open(files_page_out, 'w') do |f|
  f.puts my_header({
    name: name,
    description: description,
    page_type: :files_list,
    root_rel_prefix: '',
  })

  f.puts "<h2>Files</h2>"
  f.puts "<p>This repo contains #{file_list.length} file(s):</p>"
  f.puts "<ul class=\"file-list\">"
  file_list.each do |fname|
    f.puts "  <li><a href=\"html/#{fname}.html\">#{fname}</a></li>"
  end
  f.puts "</ul>"

  f.puts my_footer()
end

# Create commit history page.
commits_page_out = "#{output_dir}/commits.html"
File.open(commits_page_out, 'w') do |f|
  f.puts my_header({
    name: name,
    description: description,
    page_type: :commits,
    root_rel_prefix: '',
  })

  f.puts "<h2>Commit history</h2>"
  f.puts "<pre class=\"commits\">"
  f.puts `git log`
  f.puts "</pre>"

  f.puts my_footer()
end

# Create project landing page.
index_out = "#{output_dir}/index.html"
File.open(index_out, 'w') do |f|
  f.puts my_header({
    name: name,
    description: description,
    page_type: :main,
    root_rel_prefix: '',
  })

  file_show_max = 20

  f.puts "<h2>Files</h2>"
  f.puts "<ul>"
  file_list.take(file_show_max).each do |fname|
    f.puts "  <li><a href=\"html/#{fname}.html\">#{fname}</a></li>"
  end
  if file_list.length > file_show_max
    f.puts "<li>...<br><a href=\"files.html\">View all #{file_list.length} files</a></li>"
  end
  f.puts "</ul>"

  # start div.readme:
  f.puts "<div class=\"readme\"><b class=\"filename\">#{readme_file}</b><br>"

  if readme_type == 'text'
    f.puts "<pre>"
    f.puts File.read(readme_file)
    f.puts "</pre>"
  end

  if readme_type == 'markdown'
    #require 'rdoc'
    #data = File.read(readme_file)
    #fmt = RDoc::Markup::ToHtml.new(RDoc::Options.new, nil)
    #html = RDoc::Markdown.parse(data).accept(fmt)
    #f.puts html
    readme_html = `markdown #{readme_file}`
    f.puts readme_html
  end

  # end div.readme:
  f.puts "</div>"

  f.puts my_footer()
end

# Lastly, sync and update the output's bare repo for "dumb http" Git cloning.
update_bare = <<CMD
  cd #{bare_output_dir}
  git fetch #{repo_dir} '*:*'
  git update-server-info
CMD
`#{update_bare}`

puts "Output complete at '#{output_dir}'"
