#!/usr/bin/env ruby
# -*- coding: ASCII-8BIT -*-
=begin
= NAME
rd2 - converter from RD to other mark-up language.
= SYNOPSIS
  rd2 [-r <visitor>] [options] <file>

= DESCRIPTION
rd2 inputs from ((|<file>|)) and outputs into (({STDOUT})). you can
choose ((|<visitor>|)) to select output format. For example, use
"rd/rd2html-lib.rb" to translate it into HTML, and "rd/rd2man-lib.rb"
to tranlate it into roff with man macros.

= OPTIONS
please read output of 
  % rd2 --help
and
  % rd2 -r rd/rd2html-lib.rb --help

= FILES
  * ~/.rd2rc - User configration file.

= SEE ALSO
ruby(1)
=end

require "kconv"
require "optparse"
require "rd/rdfmt"
require "rd/visitor"
require "rd/version"

module Kconv
  NAME2CONST = {
    "iso-2022-jp" => Kconv::JIS,
    "euc-jp" => Kconv::EUC,
    "shift_jis" => Kconv::SJIS,
  }
  NAME_ALIAS = {
    'jis' => 'iso-2022-jp',
    'euc' => 'euc-jp',
    'sjis' => 'shift_jis',
    'shift-jis' => 'shift_jis',
  }
  if defined?(Kconv::UTF8)
    NAME2CONST['utf-8'] = Kconv::UTF8
    NAME_ALIAS['utf8'] = 'utf-8'
  end
end

include RD

SYSTEM_NAME = "RDtool -- rd2"
SYSTEM_VERSION = "$Version: " + RD::VERSION + "$" #"
RD2_VERSION = Version.new_from_version_string(SYSTEM_NAME, SYSTEM_VERSION)

$Visitor_Class = nil
$Visitor = nil
$RD2_Sub_OptionParser = nil
$RD2_OptionParser = nil
$DEFAULT_FORMAT_LIB = "rd/rd2html-lib"
$RC = {}
$RC["filter"] = Hash.new(RD::INCLUDE_FILTER)

class RD2Command
  PartArgument = Struct.new(:part, :filter)

  def initialize(argv: ARGV, argf: ARGF, stdout: $stdout, stderr: $stderr, program_name: $PROGRAM_NAME)
    @argv = argv
    @argf = argf
    @stdout = stdout
    @stderr = stderr
    @program_name = program_name
    @include_path = []
    @with_part = []
    @output_file = nil
    @output_index = false
    @out_code = nil
    @from_rdo = false
  end

  def run
    load_user_config
    require_implicit_format_lib

    begin
      option_parser.parse!(@argv)
    rescue OptionParser::ParseError => e
      @stderr.print("Error: #{e.inspect}\n")
      @stderr.print(option_parser.to_s)
      return 1
    end

    load_default_format_library unless $Visitor_Class
    ensure_visitor_loaded
    $Visitor.input_filename = @argf.filename

    tree = build_tree
    configure_visitor
    write_output(tree, $Visitor.visit(tree))
    write_index(tree) if @output_index
    0
  rescue Racc::ParseError => e
    @stderr.puts(e.message)
    10
  ensure
    cleanup_tmp_files
  end

  private

  def option_parser
    @option_parser ||= @argv.options do |q|
      $RD2_OptionParser = q
      q.banner = "Usage: #{File.basename(@program_name)} [options] rd-file > output\n"
      q.on_head("global options:")

      q.on("-rLIB", "--require=LIB", String, "choose format library.") do |lib|
        load_format_library(lib)
      end

      q.on("-oNAME", String, "indicate base name of output file") do |name|
        @output_file = name
      end

      q.on("--out-code=KCODE",
           Kconv::NAME2CONST.keys, Kconv::NAME_ALIAS,
           "character encoding of output.(jis|euc|sjis|utf8)") do |code|
        @out_code = code
      end

      q.on("--output-index", "output method index file (*.rmi)") do
        @output_index = true
      end

      q.on("-IPATH", "--include-path=PATH", String,
           "add PATH to list of include path") do |path|
        @include_path.unshift(path)
      end

      q.accept(PartArgument, /(\w+)(?:\s*:\s*(\w+))?/) do |_src, part, filter|
        PartArgument.new(part, filter || part)
      end
      q.on("--with-part=PART", PartArgument, "include PART with Filter") do |entry|
        @with_part << entry
        unless @include_path.include?(RD::RDTree.tmp_dir)
          @include_path << RD::RDTree.tmp_dir
        end
      end

      q.on("--from-rdo", "load from *.rdo instead of *.rd") do
        @from_rdo = true
      end

      q.on("--version", "print versions.") do
        @stderr.puts(RD2_VERSION)
        @stderr.puts(Tree::version)
        @stderr.puts(Visitor::version)
        @stderr.puts($Visitor_Class.version) if $Visitor_Class
        raise SystemExit.new(0)
      end

      q.on_tail("--help", "print this message") do
        @stderr.print(q.to_s)
        raise SystemExit.new(0)
      end
    end
  end

  def load_user_config
    user_config = File.expand_path("~/.rd2rc")
    if File.readable?(user_config)
      load user_config
    else
      load "rd/dot.rd2rc"
    end
  rescue LoadError, StandardError
    load "rd/dot.rd2rc"
  end

  def require_implicit_format_lib
    base = File.basename(@program_name, ".*").downcase
    return unless /rd2.+/ =~ base

    load_format_library("rd/#{$&}-lib.rb")
  end

  def load_format_library(lib)
    require lib
    ensure_visitor_loaded
  end

  def load_default_format_library
    load_format_library($DEFAULT_FORMAT_LIB)
  end

  def ensure_visitor_loaded
    return unless $Visitor_Class
    return if $Visitor

    $Visitor = $Visitor_Class.new
    load_sub_option_parser
  end

  def load_sub_option_parser
    return unless $RD2_Sub_OptionParser

    require $RD2_Sub_OptionParser
    $RD2_Sub_OptionParser = nil
  end

  def build_tree
    if @from_rdo
      build_tree_from_rdo
    else
      build_tree_from_rd
    end
  end

  def build_tree_from_rdo
    rdos = []
    @argv.each do |path|
      rdos << File.open(path)
      dirname = File.dirname(path)
      @include_path.push(dirname, File.join(dirname, "include"))
    end
    tree = RDTree.new_from_rdo(*rdos)
    tree.include_path = @include_path
    tree
  ensure
    rdos&.each(&:close)
  end

  def build_tree_from_rd
    dir = @argf.filename ? File.dirname(@argf.filename) : "."
    @include_path.push(dir, File.join(dir, "include"))

    src = read_input_lines
    if src.find { |line| /\S/ === line } && !src.find { |line| /^=begin\b/ === line }
      src.unshift("=begin\n").push("=end\n")
    end

    tree = RDTree.new(src, @include_path, nil)
    @with_part.each do |entry|
      tree.filter[entry.part] = $RC["filter"][entry.filter]
    end
    tree.parse
    tree
  end

  def read_input_lines
    @argf.readlines
  end

  def configure_visitor
    @with_part.each do |entry|
      $Visitor.include_suffix.push(entry.part)
    end

    $Visitor.filename = @output_file if @output_file

    return unless @out_code

    begin
      $Visitor.charcode = @out_code
      $Visitor.lang = "ja"
    rescue NameError
    end
  end

  def write_output(tree, out)
    out = Kconv.kconv(out, Kconv::NAME2CONST[@out_code], Kconv::AUTO) if @out_code

    if @output_file
      suffix = if $Visitor.respond_to?(:output_suffix)
                 $Visitor.output_suffix
               else
                 $Visitor.class::OUTPUT_SUFFIX
               end
      filename = @output_file + "." + suffix
      File.open(filename, "w") { |file| file.print(out) }
      @stderr.print("#{File.basename(@program_name)}: output to #{filename}...\n")
    else
      @stdout.print(out)
    end
  end

  def write_index(tree)
    unless @output_file
      raise %Q[Error: option "--output-index" must be used with option "-oNAME"]
    end

    require "rd/rd2rmi-lib"
    rmivisitor = RD2RMIVisitor.new
    rmivisitor.filename = @output_file
    filename = @output_file + ".rmi"
    File.open(filename, "w") { |file| file.print(rmivisitor.visit(tree)) }
    @stderr.print("#{File.basename(@program_name)}: output to #{filename}...\n")
  end

  def cleanup_tmp_files
    Dir.glob("#{RD::RDTree.tmp_dir}/rdtmp.#{$$}.*.*").each do |path|
      File.delete(path)
    end
  end
end

exit(RD2Command.new.run)
