# -*- encoding: utf-8 -*-

require 'time'
require 'ostruct'
require 'shellwords'
require 'webgen/path_handler/base'
require 'webgen/path_handler/page_utils'
require 'webgen/context'
require 'webgen/path'
require 'webgen/content_processor/r_doc'

module Webgen
  class PathHandler

    # Path handler for Ruby API documentation via rdoc.
    class Api

      include Base
      include PageUtils

      # The mandatory meta info keys that need to be set on an api path.
      MANDATORY_INFOS = %W[rdoc_options]

      # Create the feed nodes.
      def create_nodes(path, blocks)
        if MANDATORY_INFOS.any? {|t| path.meta_info[t].nil?}
          raise Webgen::NodeCreationError.new("At least one of #{MANDATORY_INFOS.join('/')} is missing",
                                              "path_handler.api", path)
        end

        path['api_name'] ||= path.basename
        path['dir_name'] ||= path.basename

        cache_dir = @website.tmpdir(File.join('path_handler.api', path['api_name']))
        rdoc = rdoc_object(path['rdoc_options'], cache_dir)
        output_flag_file = rdoc.output_flag_file(cache_dir)

        dir_node = create_directory(path, Webgen::Path.new(path.parent_path + path['dir_name'] + '/'), false)

        api = OpenStruct.new
        api.directory = dir_node
        api.class_nodes = {}
        api.file_nodes = {}

        rdoc.store.all_classes_and_modules.sort.each do |klass|
          adapt_rdoc_class(path, klass)
          klass_node = create_page_node_for_class(path, dir_node, klass, output_flag_file)
          api.class_nodes[klass.full_name] = klass_node
          klass_node.node_info[:api] = api
          create_fragment_nodes_for_constants(path, klass_node, klass)
          create_fragment_nodes_for_attributes(path, klass_node, klass)
          create_fragment_nodes_for_methods(path, klass_node, klass)
        end

        rdoc.store.all_files.sort.each do |file|
          next unless file.text?
          file_node = create_page_node_for_file(path, dir_node, file, output_flag_file)
          api.file_nodes[file.full_name] = file_node
          file_node.node_info[:api] = api
        end

        nil
      end

      # Create a directory for the path, applying needed meta information from the api path.
      #
      # Also creates the parent directories when necessary.
      def create_directory(api_path, path, set_proxy_path = true)
        if (dir = @website.tree[path.alcn])
          return dir
        end

        parent_path = Webgen::Path.new(path.parent_path)
        if !@website.tree[parent_path.alcn]
          create_directory(api_path, parent_path)
        end

        path['modified_at'] = api_path['modified_at']
        path['handler'] = 'directory'
        path['proxy_path'] ||= "../#{path.basename}.html" if set_proxy_path
        @website.ext.path_handler.create_secondary_nodes(path).first
      end
      private :create_directory

      # Create the RDoc instance and use it for generating the API data.
      #
      # If possible, cached data available under +cache_dir+ is used.
      def rdoc_object(options, cache_dir)
        start_time = Time.now

        rdoc = RDoc::RDoc.new
        rdoc.options = rdoc_options(options)
        rdoc.store = rdoc_store(rdoc.options, cache_dir)

        rdoc.last_modified.replace(rdoc.setup_output_dir(cache_dir, false))

        if !(rdoc.parse_files(rdoc.options.files)).empty?
          rdoc.store.complete(rdoc.options.visibility)
          rdoc.store.save
          rdoc.update_output_dir(cache_dir, start_time, rdoc.last_modified)
        end
        rdoc.store.load_all

        # We need a dummy generator with some methods
        rdoc.generator = Object.new
        def (rdoc.generator).class_dir; nil; end
        def (rdoc.generator).file_dir; nil; end

        rdoc
      end
      protected :rdoc_object

      # Return a fully initialized RDoc::Options object.
      #
      # Some of the user specified options may not be used if they would interfere with this class'
      # job.
      def rdoc_options(user_options)
        user_options = Shellwords.split(user_options) if user_options.kind_of?(String)
        options = RDoc::Options.new
        options.parse(user_options)
        options.verbosity = 0
        options.dry_run = false
        options.update_output_dir = true
        options.force_output = false
        options.finish
        options
      end
      protected :rdoc_options

      # Return a fully initialized RDoc::Store object.
      def rdoc_store(options, cache_dir)
        store = RDoc::Store.new(cache_dir)
        store.encoding = options.encoding
        store.dry_run = options.dry_run
        store.main = options.main_page
        store.title = options.title
        store.load_cache
        store
      end
      protected :rdoc_store

      # Adapt a RDoc class/module object to provide a different output path depending on the
      # output_structure meta information of the API path.
      def adapt_rdoc_class(api_path, klass)
        case api_path['output_structure']
        when 'hierarchical'
          api_path['use_proxy_path'] = false
          def klass.http_url(prefix)
            if classes_and_modules.size > 0
              super(prefix).sub(/\.html/, '/index.html')
            else
              super(prefix)
            end
          end
        else
          api_path['use_proxy_path'] = true
        end
      end
      protected :adapt_rdoc_class

      # Create a page node for the given class +klass+ and return it.
      #
      # A link definition entry for the class is also created.
      def create_page_node_for_class(api_path, dir_node, klass, output_flag_file)
        klass_path_str = klass.http_url(dir_node.alcn)

        create_directory(api_path, Webgen::Path.new(File.dirname(klass_path_str) + '/'), api_path['use_proxy_path'])

        path = Webgen::Path.new(klass_path_str, 'handler' => 'page', 'modified_at' => api_path['modified_at'],
                                'title' => "#{klass.full_name}", 'api_class_name' => klass.full_name,
                                'api_name' => api_path['api_name'], 'template' => api_path['api_template'])
        node = @website.ext.path_handler.create_secondary_nodes(path).first

        node.node_info[:rdoc_object] = klass
        @website.ext.item_tracker.add(node, :file, output_flag_file)
        add_link_definition(api_path, klass.full_name, node.alcn, klass.full_name)

        node
      end
      protected :create_page_node_for_class

      # Creates fragment nodes for constants under the "Constants" fragment.
      def create_fragment_nodes_for_constants(api_path, klass_node, klass)
        return if klass.constants.none? {|const| const.display? }
        constants_url = "#{klass_node.alcn}#Constants"
        path = Webgen::Path.new(constants_url,
                                {'handler' => 'copy', 'modified_at' => api_path['modified_at'],
                                 'pipeline' => [], 'no_output' => true, 'title' => "Constants"})
        const_node = @website.ext.path_handler.create_secondary_nodes(path).first
        klass.constants.sort_by(&:name).each do |const|
          create_fragment_node_for_constant(api_path, const_node, const)
        end
      end
      protected :create_fragment_nodes_for_constants

      # Create a fragment node for the given constant.
      #
      # A link definition entry for the method is also created.
      def create_fragment_node_for_constant(api_path, parent_node, constant)
        constant_url = "#{parent_node.alcn.sub(/#.*$/, '')}##{constant.name}"
        path = Webgen::Path.new(constant_url,
                                {'handler' => 'copy', 'modified_at' => api_path['modified_at'],
                                 'parent_alcn' => parent_node.alcn,
                                 'pipeline' => [], 'no_output' => true, 'title' => constant.name})
        @website.ext.path_handler.create_secondary_nodes(path)
        add_link_definition(api_path, constant.full_name, constant_url, constant.full_name)
      end
      protected :create_fragment_node_for_constant

      # Creates fragment nodes for attributes under the "Attributes" fragment.
      def create_fragment_nodes_for_attributes(api_path, parent_node, klass)
        return if klass.attributes.none? {|attr| attr.display? }
        attributes_url = "#{parent_node.alcn}#Attributes"
        path = Webgen::Path.new(attributes_url,
                                {'handler' => 'copy', 'modified_at' => api_path['modified_at'],
                                  'parent_alcn' => parent_node.alcn,
                                 'pipeline' => [], 'no_output' => true, 'title' => "Attributes"})
        attr_node = @website.ext.path_handler.create_secondary_nodes(path).first
        klass.attributes.sort_by(&:name).each do |attribute|
          create_fragment_node_for_method(api_path, attr_node, attribute)
        end
      end
      protected :create_fragment_nodes_for_attributes

      # Creates fragment nodes for methods under the "Class Methods" or "Instance Methods"
      # fragments.
      def create_fragment_nodes_for_methods(api_path, klass_node, klass)
        ["Class", "Instance"].each do |type|
          method_list = type == 'Class' ? klass.class_method_list : klass.instance_methods
          next if method_list.empty?
          meth_url = "#{klass_node.alcn}##{type}-Methods"
          path = Webgen::Path.new(meth_url,
                                  {'handler' => 'copy', 'modified_at' => api_path['modified_at'],
                                    'pipeline' => [], 'no_output' => true,
                                    'title' => "#{type} Methods"})
          meth_node = @website.ext.path_handler.create_secondary_nodes(path).first
          method_list.sort_by(&:name).each do |method|
            create_fragment_node_for_method(api_path, meth_node, method)
          end
        end
      end
      protected :create_fragment_nodes_for_attributes

      # Create a fragment node for the given method.
      #
      # A link definition entry for the method is also created.
      def create_fragment_node_for_method(api_path, parent_node, method)
        method_url = "#{parent_node.alcn.sub(/#.*$/, '')}##{method.aref}"
        path = Webgen::Path.new(method_url,
                                {'handler' => 'copy', 'modified_at' => api_path['modified_at'],
                                  'parent_alcn' => parent_node.alcn,
                                  'pipeline' => [], 'no_output' => true, 'title' => method.name})
        @website.ext.path_handler.create_secondary_nodes(path)
        add_link_definition(api_path, method.full_name, method_url, method.full_name)
      end
      protected :create_fragment_node_for_method

      # Create a page node for the given file and return it.
      def create_page_node_for_file(api_path, dir_node, file, output_flag_file)
        file_path_str = file.http_url(dir_node.alcn)

        create_directory(api_path, Webgen::Path.new(File.dirname(file_path_str) + '/'))

        path = Webgen::Path.new(file_path_str, 'handler' => 'page', 'modified_at' => api_path['modified_at'],
                                'title' => "File #{file.full_name}", 'api_file_name' => file.full_name,
                                'api_name' => api_path['api_name'], 'template' => api_path['api_template'])
        node = @website.ext.path_handler.create_secondary_nodes(path).first

        node.node_info[:rdoc_object] = file
        @website.ext.item_tracker.add(node, :file, output_flag_file)

        node
      end
      protected :create_page_node_for_file

      # Add a link definition for the given node.
      def add_link_definition(api_path, link_name, url, title)
        link = if api_path['prefix_link_defs']
                 "#{api_path['api_name']}:#{link_name}"
               else
                 link_name
               end
        @website.ext.link_definitions[link] = [url, title]
      end
      protected :add_link_definition

    end

  end
end
