require 'rdoc/generators/html_generator'
require 'puppet/util/rdoc/code_objects'
require 'digest/md5'

module Generators

    # This module holds all the classes needed to generate the HTML documentation
    # of a bunch of puppet manifests.
    #
    # It works by traversing all the code objects defined by the Puppet RDoc::Parser
    # and produces HTML counterparts objects that in turns are used by RDoc template engine
    # to produce the final HTML.
    #
    # It is also responsible of creating the whole directory hierarchy, and various index
    # files.
    #
    # It is to be noted that the whole system is built on top of ruby RDoc. As such there
    # is an implicit mapping of puppet entities to ruby entitites:
    #
    #         Puppet    =>    Ruby
    #         ------------------------
    #         Module          Module
    #         Class           Class
    #         Definition      Method
    #         Resource
    #         Node
    #         Plugin
    #         Fact

    MODULE_DIR = "modules"
    NODE_DIR = "nodes"
    PLUGIN_DIR = "plugins"

    # This is a specialized HTMLGenerator tailored to Puppet manifests
    class PuppetGenerator < HTMLGenerator

        def PuppetGenerator.for(options)
            AllReferences::reset
            HtmlMethod::reset

            if options.all_one_file
                PuppetGeneratorInOne.new(options)
            else
                PuppetGenerator.new(options)
            end
        end

        def initialize(options) #:not-new:
            @options    = options
            load_html_template
        end

        # loads our own html template file
        def load_html_template
            begin
                require 'puppet/util/rdoc/generators/template/puppet/puppet'
                extend RDoc::Page
            rescue LoadError
                $stderr.puts "Could not find Puppet template '#{template}'"
                exit 99
            end
        end

        def gen_method_index
            # we don't generate an all define index
            # as the presentation is per module/per class
        end

        # This is the central method, it generates the whole structures
        # along with all the indices.
        def generate_html
            super
            gen_into(@nodes)
            gen_into(@plugins)
        end

        ##
        # Generate:
        #  the list of modules
        #  the list of classes and definitions of a specific module
        #  the list of all classes
        #  the list of nodes
        #  the list of resources
        def build_indices
            @allfiles = []
            @nodes = []
            @plugins = []

            # contains all the seen modules
            @modules = {}
            @allclasses = {}

            # build the modules, classes and per modules classes and define list
            @toplevels.each do |toplevel|
                next unless toplevel.document_self
                file = HtmlFile.new(toplevel, @options, FILE_DIR)
                classes = []
                methods = []
                modules = []
                nodes = []

                # find all classes of this toplevel
                # store modules if we find one
                toplevel.each_classmodule do |k|
                    generate_class_list(classes, modules, k, toplevel, CLASS_DIR)
                end

                # find all defines belonging to this toplevel
                HtmlMethod.all_methods.each do |m|
                    # find parent module, check this method is not already
                    # defined.
                    if m.context.parent.toplevel === toplevel
                        methods << m
                    end
                end

                classes.each do |k|
                    @allclasses[k.index_name] = k if !@allclasses.has_key?(k.index_name)
                end

                # generate nodes and plugins found
                classes.each do |k|
                    if k.context.is_module?
                        k.context.each_node do |name,node|
                            nodes << HTMLPuppetNode.new(node, toplevel, NODE_DIR, @options)
                            @nodes << nodes.last
                        end
                        k.context.each_plugin do |plugin|
                            @plugins << HTMLPuppetPlugin.new(plugin, toplevel, PLUGIN_DIR, @options)
                        end
                        k.context.each_fact do |fact|
                            @plugins << HTMLPuppetPlugin.new(fact, toplevel, PLUGIN_DIR, @options)
                        end
                    end
                end

                @files << file
                @allfiles << { "file" => file, "modules" => modules, "classes" => classes, "methods" => methods, "nodes" => nodes }
            end

            @classes = @allclasses.values
        end

        # produce a class/module list of HTMLPuppetModule/HTMLPuppetClass
        # based on the code object traversal.
        def generate_class_list(classes, modules, from, html_file, class_dir)
            if from.is_module? and !@modules.has_key?(from.name)
                k = HTMLPuppetModule.new(from, html_file, class_dir, @options)
                classes << k
                @modules[from.name] = k
                modules << @modules[from.name]
            elsif from.is_module?
                modules << @modules[from.name]
            elsif !from.is_module?
                k = HTMLPuppetClass.new(from, html_file, class_dir, @options)
                classes << k
            end
            from.each_classmodule do |mod|
                generate_class_list(classes, modules, mod, html_file, class_dir)
            end
        end

        # generate all the subdirectories, modules, classes and files
        def gen_sub_directories
            begin
                super
                File.makedirs(MODULE_DIR)
                File.makedirs(NODE_DIR)
                File.makedirs(PLUGIN_DIR)
            rescue
                $stderr.puts $!.message
                exit 1
            end
        end

        # generate the index of modules
        def gen_file_index
            gen_top_index(@modules.values, 'All Modules', RDoc::Page::TOP_INDEX, "fr_modules_index.html")
        end

        # generate a top index
        def gen_top_index(collection, title, template, filename)
            template = TemplatePage.new(RDoc::Page::FR_INDEX_BODY, template)
            res = []
            collection.sort.each do |f|
                if f.document_self
                    res << { "classlist" => CGI.escapeHTML("#{MODULE_DIR}/fr_#{f.index_name}.html"), "module" => CGI.escapeHTML("#{CLASS_DIR}/#{f.index_name}.html"),"name" => CGI.escapeHTML(f.index_name) }
                end
            end

            values = {
                "entries"    => res,
                'list_title' => CGI.escapeHTML(title),
                'index_url'  => main_url,
                'charset'    => @options.charset,
                'style_url'  => style_url('', @options.css),
            }

            File.open(filename, "w") do |f|
                template.write_html_on(f, values)
            end
        end

        # generate the all classes index file and the combo index
        def gen_class_index
            gen_an_index(@classes, 'All Classes', RDoc::Page::CLASS_INDEX, "fr_class_index.html")
            @allfiles.each do |file|
                unless file['file'].context.file_relative_name =~ /\.rb$/
                    gen_composite_index(file,
                                        RDoc::Page::COMBO_INDEX,
                                        "#{MODULE_DIR}/fr_#{file["file"].context.module_name}.html")
                end
            end
        end

        def gen_composite_index(collection, template, filename)\
            return if FileTest.exists?(filename)

            template = TemplatePage.new(RDoc::Page::FR_INDEX_BODY, template)
            res1 = []
            collection['classes'].sort.each do |f|
                if f.document_self
                    unless f.context.is_module?
                        res1 << { "href" => "../"+CGI.escapeHTML(f.path), "name" => CGI.escapeHTML(f.index_name) }
                    end
                end
            end

            res2 = []
            collection['methods'].sort.each do |f|
                if f.document_self
                    res2 << { "href" => "../"+f.path, "name" => f.index_name.sub(/\(.*\)$/,'') }
                end
            end

            module_name = []
            res3 = []
            res4 = []
            collection['modules'].sort.each do |f|
                module_name << { "href" => "../"+CGI.escapeHTML(f.path), "name" => CGI.escapeHTML(f.index_name) }
                unless f.facts.nil?
                    f.facts.each do |fact|
                        res3 << {"href" => "../"+CGI.escapeHTML(AllReferences["PLUGIN(#{fact.name})"].path), "name" => CGI.escapeHTML(fact.name)}
                    end
                end
                unless f.plugins.nil?
                    f.plugins.each do |plugin|
                        res4 << {"href" => "../"+CGI.escapeHTML(AllReferences["PLUGIN(#{plugin.name})"].path), "name" => CGI.escapeHTML(plugin.name)}
                    end
                end
            end

            res5 = []
            collection['nodes'].sort.each do |f|
                if f.document_self
                    res5 << { "href" => "../"+CGI.escapeHTML(f.path), "name" => CGI.escapeHTML(f.name) }
                end
            end

            values = {
                "module" => module_name,
                "classes"    => res1,
                'classes_title' => CGI.escapeHTML("Classes"),
                'defines_title' => CGI.escapeHTML("Defines"),
                'facts_title' => CGI.escapeHTML("Custom Facts"),
                'plugins_title' => CGI.escapeHTML("Plugins"),
                'nodes_title' => CGI.escapeHTML("Nodes"),
                'index_url'  => main_url,
                'charset'    => @options.charset,
                'style_url'  => style_url('', @options.css),
            }

            values["defines"] = res2 if res2.size>0
            values["facts"] = res3 if res3.size>0
            values["plugins"] = res4 if res4.size>0
            values["nodes"] = res5 if res5.size>0

            File.open(filename, "w") do |f|
                template.write_html_on(f, values)
            end
        end

        # returns the initial_page url
        def main_url
            main_page = @options.main_page
            ref = nil
            if main_page
                ref = AllReferences[main_page]
                if ref
                    ref = ref.path
                else
                    $stderr.puts "Could not find main page #{main_page}"
                end
            end

            unless ref
                for file in @files
                    if file.document_self and file.context.global
                        ref = CGI.escapeHTML("#{CLASS_DIR}/#{file.context.module_name}.html")
                        break
                    end
                end
            end

            unless ref
                for file in @files
                    if file.document_self and !file.context.global
                        ref = CGI.escapeHTML("#{CLASS_DIR}/#{file.context.module_name}.html")
                        break
                    end
                end
            end

            unless ref
                $stderr.puts "Couldn't find anything to document"
                $stderr.puts "Perhaps you've used :stopdoc: in all classes"
                exit(1)
            end

            ref
        end

    end

    # This module is used to hold/generate a list of puppet resources
    # this is used in HTMLPuppetClass and HTMLPuppetNode
    module ResourceContainer
        def collect_resources
            list = @context.resource_list
            @resources = list.collect {|m| HTMLPuppetResource.new(m, self, @options) }
        end

        def build_resource_summary_list(path_prefix='')
            collect_resources unless @resources
            resources = @resources.sort
            res = []
            resources.each do |r|
                res << {
                  "name" => CGI.escapeHTML(r.name),
                  "aref" => CGI.escape(path_prefix)+"\#"+CGI.escape(r.aref)
                }
            end
            res
        end

        def build_resource_detail_list(section)
            outer = []
            resources = @resources.sort
            resources.each do |r|
                row = {}
                if r.section == section and r.document_self
                    row["name"]        = CGI.escapeHTML(r.name)
                    desc = r.description.strip
                    row["m_desc"]      = desc unless desc.empty?
                    row["aref"]        = r.aref
                    row["params"]      = r.params
                    outer << row
                end
            end
            outer
        end
    end

    class HTMLPuppetClass < HtmlClass
        include ResourceContainer

        def value_hash
            super
            rl = build_resource_summary_list
            @values["resources"] = rl unless rl.empty?

            @context.sections.each do |section|
                secdata = @values["sections"].select { |secdata| secdata["secsequence"] == section.sequence }
                if secdata.size == 1
                    secdata = secdata[0]

                    rdl = build_resource_detail_list(section)
                    secdata["resource_list"] = rdl unless rdl.empty?
                end
            end
            @values
        end
    end

    class HTMLPuppetNode < ContextUser
        include ResourceContainer

        attr_reader :path

        def initialize(context, html_file, prefix, options)
            super(context, options)

            @html_file = html_file
            @is_module = context.is_module?
            @values    = {}

            context.viewer = self

            if options.all_one_file
                @path = context.full_name
            else
                @path = http_url(context.full_name, prefix)
            end

            AllReferences.add("NODE(#{@context.full_name})", self)
        end

        def name
            @context.name
        end

        # return the relative file name to store this class in,
        # which is also its url
        def http_url(full_name, prefix)
            path = full_name.dup
            if path['<<']
                path.gsub!(/<<\s*(\w*)/) { "from-#$1" }
            end
            File.join(prefix, path.split("::").collect { |p| Digest::MD5.hexdigest(p) }) + ".html"
        end

        def parent_name
            @context.parent.full_name
        end

        def index_name
            name
        end

        def write_on(f)
            value_hash
            template = TemplatePage.new(RDoc::Page::BODYINC,
                                        RDoc::Page::NODE_PAGE,
                                        RDoc::Page::METHOD_LIST)
            template.write_html_on(f, @values)
        end

        def value_hash
            class_attribute_values
            add_table_of_sections

            @values["charset"] = @options.charset
            @values["style_url"] = style_url(path, @options.css)

            d = markup(@context.comment)
            @values["description"] = d unless d.empty?

            ml = build_method_summary_list
            @values["methods"] = ml unless ml.empty?

            rl = build_resource_summary_list
            @values["resources"] = rl unless rl.empty?

            il = build_include_list(@context)
            @values["includes"] = il unless il.empty?

            @values["sections"] = @context.sections.map do |section|

                secdata = {
                    "sectitle" => section.title,
                    "secsequence" => section.sequence,
                    "seccomment" => markup(section.comment)
                }

                al = build_alias_summary_list(section)
                secdata["aliases"] = al unless al.empty?

                co = build_constants_summary_list(section)
                secdata["constants"] = co unless co.empty?

                al = build_attribute_list(section)
                secdata["attributes"] = al unless al.empty?

                cl = build_class_list(0, @context, section)
                secdata["classlist"] = cl unless cl.empty?

                mdl = build_method_detail_list(section)
                secdata["method_list"] = mdl unless mdl.empty?

                rdl = build_resource_detail_list(section)
                secdata["resource_list"] = rdl unless rdl.empty?

                secdata
            end

            @values
        end

        def build_attribute_list(section)
            atts = @context.attributes.sort
            res = []
            atts.each do |att|
                next unless att.section == section
                if att.visibility == :public || att.visibility == :protected || @options.show_all
                    entry = {
                        "name"   => CGI.escapeHTML(att.name),
                        "rw"     => att.rw,
                        "a_desc" => markup(att.comment, true)
                    }
                    unless att.visibility == :public || att.visibility == :protected
                        entry["rw"] << "-"
                    end
                    res << entry
                end
            end
            res
        end

        def class_attribute_values
            h_name = CGI.escapeHTML(name)

            @values["classmod"]  = "Node"
            @values["title"]     = CGI.escapeHTML("#{@values['classmod']}: #{h_name}")

            c = @context
            c = c.parent while c and !c.diagram

            if c && c.diagram
                @values["diagram"] = diagram_reference(c.diagram)
            end

            @values["full_name"] = h_name

            parent_class = @context.superclass

            if parent_class
                @values["parent"] = CGI.escapeHTML(parent_class)

                if parent_name
                    lookup = parent_name + "::" + parent_class
                else
                    lookup = parent_class
                end
                lookup = "NODE(#{lookup})"
                parent_url = AllReferences[lookup] || AllReferences[parent_class]
                if parent_url and parent_url.document_self
                    @values["par_url"] = aref_to(parent_url.path)
                end
            end

            files = []
            @context.in_files.each do |f|
                res = {}
                full_path = CGI.escapeHTML(f.file_absolute_name)

                res["full_path"]     = full_path
                res["full_path_url"] = aref_to(f.viewer.path) if f.document_self

                if @options.webcvs
                    res["cvsurl"] = cvs_url( @options.webcvs, full_path )
                end

                files << res
            end

            @values['infiles'] = files
        end

        def <=>(other)
            self.name <=> other.name
        end
    end

    class HTMLPuppetModule < HtmlClass

        def initialize(context, html_file, prefix, options)
            super(context, html_file, prefix, options)
        end

        def value_hash
            @values = super

            fl = build_facts_summary_list
            @values["facts"] = fl unless fl.empty?

            pl = build_plugins_summary_list
            @values["plugins"] = pl unless pl.empty?

            nl = build_nodes_list(0, @context)
            @values["nodelist"] = nl unless nl.empty?

            @values
        end

        def build_nodes_list(level, context)
            res = ""
            prefix = "&nbsp;&nbsp;::" * level;

            context.nodes.sort.each do |node|
                if node.document_self
                    res <<
                    prefix <<
                    "Node " <<
                    href(url(node.viewer.path), "link", node.full_name) <<
                    "<br />\n"
                end
            end
            res
        end

        def build_facts_summary_list
            potentially_referenced_list(context.facts) {|fn| ["PLUGIN(#{fn})"] }
        end

        def build_plugins_summary_list
            potentially_referenced_list(context.plugins) {|fn| ["PLUGIN(#{fn})"] }
        end

        def facts
            @context.facts
        end

        def plugins
            @context.plugins
        end

    end

    class HTMLPuppetPlugin < ContextUser
        attr_reader :path

        def initialize(context, html_file, prefix, options)
            super(context, options)

            @html_file = html_file
            @is_module = false
            @values    = {}

            context.viewer = self

            if options.all_one_file
                @path = context.full_name
            else
                @path = http_url(context.full_name, prefix)
            end

            AllReferences.add("PLUGIN(#{@context.full_name})", self)
        end

        def name
            @context.name
        end

        # return the relative file name to store this class in,
        # which is also its url
        def http_url(full_name, prefix)
            path = full_name.dup
            if path['<<']
                path.gsub!(/<<\s*(\w*)/) { "from-#$1" }
            end
            File.join(prefix, path.split("::")) + ".html"
        end

        def parent_name
            @context.parent.full_name
        end

        def index_name
            name
        end

        def write_on(f)
            value_hash
            template = TemplatePage.new(RDoc::Page::BODYINC,
                                        RDoc::Page::PLUGIN_PAGE,
                                        RDoc::Page::PLUGIN_LIST)
            template.write_html_on(f, @values)
        end

        def value_hash
            attribute_values
            add_table_of_sections

            @values["charset"] = @options.charset
            @values["style_url"] = style_url(path, @options.css)

            d = markup(@context.comment)
            @values["description"] = d unless d.empty?

            if context.is_fact?
                unless context.confine.empty?
                    res = {}
                    res["type"] = context.confine[:type]
                    res["value"] = context.confine[:value]
                    @values["confine"] = [res]
                end
            else
                @values["type"] = context.type
            end

            @values["sections"] = @context.sections.map do |section|
                secdata = {
                    "sectitle" => section.title,
                    "secsequence" => section.sequence,
                    "seccomment" => markup(section.comment)
                }
                secdata
            end

            @values
        end

        def attribute_values
            h_name = CGI.escapeHTML(name)

            if @context.is_fact?
                @values["classmod"]  = "Fact"
            else
                @values["classmod"]  = "Plugin"
            end
            @values["title"]     = "#{@values['classmod']}: #{h_name}"

            c = @context
            @values["full_name"] = h_name

            files = []
            @context.in_files.each do |f|
                res = {}
                full_path = CGI.escapeHTML(f.file_absolute_name)

                res["full_path"]     = full_path
                res["full_path_url"] = aref_to(f.viewer.path) if f.document_self

                if @options.webcvs
                    res["cvsurl"] = cvs_url( @options.webcvs, full_path )
                end

                files << res
            end

            @values['infiles'] = files
        end

        def <=>(other)
            self.name <=> other.name
        end

    end

    class HTMLPuppetResource
        include MarkUp

        attr_reader :context

        @@seq = "R000000"

        def initialize(context, html_class, options)
            @context    = context
            @html_class = html_class
            @options    = options
            @@seq       = @@seq.succ
            @seq        = @@seq

            context.viewer = self

            AllReferences.add(name, self)
        end

        def as_href(from_path)
            if @options.all_one_file
                "#" + path
            else
                HTMLGenerator.gen_url(from_path, path)
            end
        end

        def name
            @context.name
        end

        def section
            @context.section
        end

        def index_name
            "#{@context.name}"
        end

        def params
            @context.params
        end

        def parent_name
            if @context.parent.parent
                @context.parent.parent.full_name
            else
                nil
            end
        end

        def aref
            @seq
        end

        def path
            if @options.all_one_file
                aref
            else
                @html_class.path + "#" + aref
            end
        end

        def description
            markup(@context.comment)
        end

        def <=>(other)
            @context <=> other.context
        end

        def document_self
            @context.document_self
        end

        def find_symbol(symbol, method=nil)
            res = @context.parent.find_symbol(symbol, method)
            if res
                res = res.viewer
            end
            res
        end

    end

    class PuppetGeneratorInOne < HTMLGeneratorInOne
        def gen_method_index
            gen_an_index(HtmlMethod.all_methods, 'Defines')
        end
    end

end
