require 'rdoc'

require 'rdoc/parser'

# Simple must come first
require 'rdoc/parser/simple'
require 'rdoc/parser/ruby'
require 'rdoc/parser/c'
require 'rdoc/parser/f95'
require 'rdoc/parser/perl'

require 'rdoc/stats'
require 'rdoc/options'

require 'rdoc/diagram'

require 'find'
require 'fileutils'
require 'time'

module RDoc

  ##
  # Encapsulate the production of rdoc documentation. Basically you can use
  # this as you would invoke rdoc from the command line:
  #
  #   rdoc = RDoc::RDoc.new
  #   rdoc.document(args)
  #
  # Where +args+ is an array of strings, each corresponding to an argument
  # you'd give rdoc on the command line. See rdoc/rdoc.rb for details.

  class RDoc

    Generator = Struct.new(:file_name, :class_name, :key)

    ##
    # Accessor for statistics.  Available after each call to parse_files

    attr_reader :stats

    ##
    # This is the list of output generator that we support

    GENERATORS = {}

    $LOAD_PATH.collect do |d|
      File.expand_path d
    end.find_all do |d|
      File.directory? "#{d}/rdoc/generator"
    end.each do |dir|
      Dir.entries("#{dir}/rdoc/generator").each do |gen|
        next unless /(\w+)\.rb$/ =~ gen
        type = $1
        unless GENERATORS.has_key? type
          GENERATORS[type] = Generator.new("rdoc/generator/#{gen}",
                                           "#{type.upcase}".intern,
                                           type)
        end
      end
    end

    def initialize
      @stats = nil
    end

    ##
    # Report an error message and exit

    def error(msg)
      raise ::RDoc::Error, msg
    end

    ##
    # Create an output dir if it doesn't exist. If it does exist, but doesn't
    # contain the flag file <tt>created.rid</tt> then we refuse to use it, as
    # we may clobber some manually generated documentation

    def setup_output_dir(op_dir, force)
      flag_file = output_flag_file(op_dir)
      if File.exist?(op_dir)
        unless File.directory?(op_dir)
          error "'#{op_dir}' exists, and is not a directory"
        end
        begin
          created = File.read(flag_file)
        rescue SystemCallError
          error "\nDirectory #{op_dir} already exists, but it looks like it\n" +
            "isn't an RDoc directory. Because RDoc doesn't want to risk\n" +
            "destroying any of your existing files, you'll need to\n" +
            "specify a different output directory name (using the\n" +
            "--op <dir> option).\n\n"
        else
          last = (Time.parse(created) unless force rescue nil)
        end
      else
        FileUtils.mkdir_p(op_dir)
      end
      last
    end

    ##
    # Update the flag file in an output directory.

    def update_output_dir(op_dir, time)
      File.open(output_flag_file(op_dir), "w") {|f| f.puts time.rfc2822 }
    end

    ##
    # Return the path name of the flag file in an output directory.

    def output_flag_file(op_dir)
      File.join(op_dir, "created.rid")
    end

    ##
    # The .document file contains a list of file and directory name patterns,
    # representing candidates for documentation. It may also contain comments
    # (starting with '#')

    def parse_dot_doc_file(in_dir, filename, options)
      # read and strip comments
      patterns = File.read(filename).gsub(/#.*/, '')

      result = []

      patterns.split.each do |patt|
        candidates = Dir.glob(File.join(in_dir, patt))
        result.concat(normalized_file_list(options,  candidates))
      end
      result
    end

    ##
    # Given a list of files and directories, create a list of all the Ruby
    # files they contain.
    #
    # If +force_doc+ is true we always add the given files, if false, only
    # add files that we guarantee we can parse.  It is true when looking at
    # files given on the command line, false when recursing through
    # subdirectories.
    #
    # The effect of this is that if you want a file with a non-standard
    # extension parsed, you must name it explicitly.

    def normalized_file_list(options, relative_files, force_doc = false,
                             exclude_pattern = nil)
      file_list = []

      relative_files.each do |rel_file_name|
        next if exclude_pattern && exclude_pattern =~ rel_file_name
        stat = File.stat(rel_file_name)
        case type = stat.ftype
        when "file"
          next if @last_created and stat.mtime < @last_created

          if force_doc or ::RDoc::Parser.can_parse(rel_file_name) then
            file_list << rel_file_name.sub(/^\.\//, '')
          end
        when "directory"
          next if rel_file_name == "CVS" || rel_file_name == ".svn"
          dot_doc = File.join(rel_file_name, DOT_DOC_FILENAME)
          if File.file?(dot_doc)
            file_list.concat(parse_dot_doc_file(rel_file_name, dot_doc, options))
          else
            file_list.concat(list_files_in_directory(rel_file_name, options))
          end
        else
          raise RDoc::Error, "I can't deal with a #{type} #{rel_file_name}"
        end
      end

      file_list
    end

    ##
    # Return a list of the files to be processed in a directory. We know that
    # this directory doesn't have a .document file, so we're looking for real
    # files. However we may well contain subdirectories which must be tested
    # for .document files.

    def list_files_in_directory(dir, options)
      files = Dir.glob File.join(dir, "*")

      normalized_file_list options, files, false, options.exclude
    end

    ##
    # Parse each file on the command line, recursively entering directories.

    def parse_files(options)
      @stats = Stats.new options.verbosity

      files = options.files
      files = ["."] if files.empty?

      file_list = normalized_file_list(options, files, true, options.exclude)

      return [] if file_list.empty?

      file_info = []

      file_list.each do |filename|
        @stats.add_file filename

        content = File.open(filename, "rb") { |f| f.read }
        content.gsub!(/\r$/, '')

        if defined?(::Encoding) then
          if /coding[=:]\s*([^\s;]+)/ =~ content[/\A(?:!.*\n)?(.*\n)/, 1]
            if enc = ::Encoding.find($1)
              content.force_encoding(enc)
            end
          end
        end

        top_level = ::RDoc::TopLevel.new filename

        parser = ::RDoc::Parser.for top_level, filename, content, options,
                                    @stats

        file_info << parser.scan
      end

      file_info
    end

    ##
    # Format up one or more files according to the given arguments.
    #
    # For simplicity, _argv_ is an array of strings, equivalent to the strings
    # that would be passed on the command line. (This isn't a coincidence, as
    # we _do_ pass in ARGV when running interactively). For a list of options,
    # see rdoc/rdoc.rb. By default, output will be stored in a directory
    # called +doc+ below the current directory, so make sure you're somewhere
    # writable before invoking.
    #
    # Throws: RDoc::Error on error

    def document(argv)
      TopLevel::reset

      @options = Options.new GENERATORS
      @options.parse argv

      @last_created = nil

      unless @options.all_one_file then
        @last_created = setup_output_dir @options.op_dir, @options.force_update
      end

      start_time = Time.now

      file_info = parse_files @options

      @options.title = "RDoc Documentation"

      if file_info.empty?
        $stderr.puts "\nNo newer files." unless @options.quiet
      else
        @gen = @options.generator

        $stderr.puts "\nGenerating #{@gen.key.upcase}..." unless @options.quiet

        require @gen.file_name

        gen_class = ::RDoc::Generator.const_get @gen.class_name
        @gen = gen_class.for @options

        pwd = Dir.pwd

        Dir.chdir @options.op_dir unless @options.all_one_file

        begin
          Diagram.new(file_info, @options).draw if @options.diagram
          @gen.generate(file_info)
          update_output_dir(".", start_time)
        ensure
          Dir.chdir(pwd)
        end
      end

      unless @options.quiet
        puts
        @stats.print
      end
    end
  end
end

