#!/usr/bin/env ruby

require 'optparse'

OPTIONS = {
  :instiki_root => nil,
  :storage => nil,
  :database => 'mysql'
}

ARGV.options do |opts|
  script_name = File.basename($0)
  opts.banner = "Usage: ruby #{script_name} [options]"

  opts.separator ""

  opts.on("-t", "--storage /full/path/to/storage", String,
          "Full path to your storage, ",
          "such as /home/joe/instiki/storage/2500",
          "It should be the directory that ",
          "contains .snapshot files.") do |storage|
            OPTIONS[:storage] = storage
          end

  opts.separator ""

  opts.on("-i", "--instiki /full/path/to/instiki", String,
          "Full path to your Instiki 0.10 installation, ",
          "such as /home/joe/instiki-0.10.2") do |instiki| 
            OPTIONS[:instiki] = instiki
          end

  opts.separator ""

  opts.on("-o", "--outfile /full/path/to/output_file", String,
          "Full path (including filename!) to where ",
          "you want the SQL output placed, such as ",
          "/home/joe/instiki.sql") do |outfile|
            OPTIONS[:outfile] = outfile
          end

  opts.on("-d", "--database {mysql|sqlite|postgres}", String,
          "Target database (they have slightly different syntax)",
          "default: mysql") do |database|
            OPTIONS[:database] = database
          end

  opts.separator ""

  opts.on_tail("-h", "--help",
          "Show this help message.") { puts opts; exit }

  opts.parse!
end

if OPTIONS[:instiki].nil? or OPTIONS[:storage].nil? or OPTIONS[:outfile].nil?
  $stderr.puts "Please specify full paths to Instiki 0.10 installation and storage,"
  $stderr.puts "as well as the path to the output file"
  $stderr.puts
  puts ARGV.options
  exit -1
end

if FileTest.exists? OPTIONS[:outfile]
  $stderr.puts "Output file #{OPTIONS[:outfile]} already exists!"
  $stderr.puts "Please specify a new file"
  $stderr.puts
  puts ARGV.options
  exit -1
end

raise "Directory #{OPTIONS[:instiki]} not found" unless File.directory?(OPTIONS[:instiki])
raise "Directory #{OPTIONS[:storage]} not found" unless File.directory?(OPTIONS[:storage])

expected_page_rb_path = File.join(OPTIONS[:instiki], 'app/models/page.rb')
raise "Instiki installation not found in #{OPTIONS[:instiki]}" unless File.file?(expected_page_rb_path)

expected_snapshot_pattern = File.join(OPTIONS[:storage], '*.snapshot')
raise "No snapshots found in #{expected_snapshot_pattern}" if Dir[expected_snapshot_pattern].empty?

INSTIKI_ROOT = File.expand_path(OPTIONS[:instiki])

ADDITIONAL_LOAD_PATHS = %w(
  app/models 
  lib 
  vendor/madeleine-0.7.1/lib
  vendor/RedCloth-3.0.3/lib
  vendor/RedCloth-3.0.4/lib
  vendor/rubyzip-0.5.8/lib
).map { |dir| "#{File.expand_path(File.join(INSTIKI_ROOT, dir))}"
}.delete_if { |dir| not File.exist?(dir) }

# Prepend to $LOAD_PATH
ADDITIONAL_LOAD_PATHS.reverse.each { |dir| $:.unshift(dir) if File.directory?(dir) }

require 'webrick'
require 'wiki_service'

# substitute an extremely expensive method with something cheap.
class Revision
  alias :__display_content :display_content
  def display_content
    return self
  end
end

class Time
  def ansi
    strftime('%Y-%m-%d %H:%M:%S')
  end
end

def sql_insert(table, hash)
  columns = hash.keys

  values = columns.map { |column| hash[column] }
  values = values.map do |value|
    if value.nil?
      'NULL'
    else
      if (value == false or value == true) and OPTIONS[:database] == 'mysql'
        value = value ? '1' : '0'
      end
      
      case OPTIONS[:database]
      when 'mysql', 'postgres'
        value = value.to_s.gsub("'", "\\\\'")
      when 'sqlite'
        value = value.to_s.gsub("'", "''")
      else 
        raise "Unsupported database option #{OPTIONS[:database]}"
      end
      "'#{value.gsub("\r\n", "\n")}'"
    end
  end
  
  output = "INSERT INTO #{table} ("
  output << columns.join(", ")

  output << ") VALUES ("
  output << values.join(", ")
  output << ");"
  output
end

def delete_all(outfile)
  %w(wiki_references revisions pages system webs).each { |table| outfile.puts "DELETE FROM #{table};" }
end

def next_id(key)
  $ids ||= {}
  if $ids[key].nil?
    $ids[key] = 1
  else
    $ids[key] = $ids[key] + 1
  end
  $ids[key]
end

def current_id(key)
  $ids[key] or raise "No curent ID for #{key.inspect}"
end

WikiService.storage_path = OPTIONS[:storage]
wiki = WikiService.instance

File.open(OPTIONS[:outfile], 'w') { |outfile|

  outfile.puts "BEGIN;"
  delete_all(outfile)
  outfile.puts "COMMIT;"

  wiki.webs.each_pair do |web_name, web|
    outfile.puts "BEGIN;"
    outfile.puts sql_insert(:webs, {
      :id               => next_id(:web),
      :name             => web.name,
      :address          => web.address,
      :password         => web.password,
      :additional_style => web.additional_style,
      :allow_uploads    => web.allow_uploads,
      :published        => web.published,
      :count_pages      => web.count_pages,
      :markup           => web.markup,
      :color            => web.color,
      :max_upload_size  => web.max_upload_size,
      :safe_mode        => web.safe_mode,
      :brackets_only    => web.brackets_only,
      :created_at       => web.pages.values.map { |p| p.revisions.first.created_at }.min.ansi,
      :updated_at       => web.pages.values.map { |p| p.revisions.last.created_at }.max.ansi
    })
    outfile.puts "COMMIT;"
    
    puts "Web #{web_name} has #{web.pages.keys.size} pages"
    web.pages.each_pair do |page_name, page|

      outfile.puts "BEGIN;"

      outfile.puts sql_insert(:pages, {
        :id         => next_id(:page),
        :web_id     => current_id(:web),
        :locked_by  => page.locked_by,
        :name       => page.name,
        :created_at => page.revisions.first.created_at.ansi,
        :updated_at => page.revisions.last.created_at.ansi
      })

      puts "  Page #{page_name} has #{page.revisions.size} revisions"
      page.revisions.each_with_index do |rev, i|
      
        outfile.puts sql_insert(:revisions, {
          :id         => next_id(:revision),
          :page_id    => current_id(:page),
          :content    => rev.content,
          :author     => rev.author.to_s,
          :ip         => (rev.author.is_a?(Author) ? rev.author.ip : 'N/A'),
          :created_at => rev.created_at.ansi,
          :updated_at => rev.created_at.ansi,
          :revised_at => rev.created_at.ansi
        })
        puts "    Revision #{i} created at #{rev.created_at.ansi}"
      end

      outfile.puts "COMMIT;"

    end
  end
}