# Copyright 2003, 2004 Douglas Gregor
# Copyright 2003, 2004, 2005 Vladimir Prus
# Copyright 2006 Rene Rivera
# Distributed under the Boost Software License, Version 1.0.
# (See accompanying file LICENSE_1_0.txt or http://www.boost.org/LICENSE_1_0.txt)

# This module defines rules to handle generation of various outputs from source
# files documented with doxygen comments. The supported transformations are:
#
# * Source -> Doxygen XML -> BoostBook XML
# * Source -> Doxygen HTML
#
# The type of transformation is selected based on the target requested. For
# BoostBook XML, the default, specifying a target with an ".xml" suffix, or an
# empty suffix, will produce a <target>.xml and <target>.boostbook. For Doxygen
# HTML specifying a target with an ".html" suffix will produce a directory
# <target> with the Doxygen html files, and a <target>.html file redirecting to
# that directory.

import "class" : new ;
import targets ;
import feature ;
import property ;
import generators ;
import boostbook ;
import type ;
import path ;
import print ;
import regex ;
import stage ;
import project ;
import xsltproc ;
import make ;
import os ;
import toolset : flags ;
import alias ;
import common ;
import modules ;
import project ;
import utility ;
import errors ;


# Use to specify extra configuration paramters. These get translated
# into a doxyfile which configures the building of the docs.
feature.feature doxygen:param : : free ;

# Specify the "<xsl:param>boost.doxygen.header.prefix" XSLT option.
feature.feature prefix : : free ;

# Specify the "<xsl:param>boost.doxygen.reftitle" XSLT option.
feature.feature reftitle : : free ;

# Which processor to use for various translations from Doxygen.
feature.feature doxygen.processor : xsltproc doxproc : propagated implicit ;

# To generate, or not, index sections.
feature.feature doxygen.doxproc.index : no yes : propagated incidental ;

# The ID for the resulting BoostBook reference section.
feature.feature doxygen.doxproc.id : : free ;

# The title for the resulting BoostBook reference section.
feature.feature doxygen.doxproc.title : : free ;

# Location for images when generating XML
feature.feature doxygen:xml-imagedir : : free ;

# Indicates whether the entire directory should be deleted
feature.feature doxygen.rmdir : off on : optional incidental ;

# Doxygen configuration input file.
type.register DOXYFILE : doxyfile ;

# Doxygen XML multi-file output.
type.register DOXYGEN_XML_MULTIFILE : xml-dir : XML ;

# Doxygen XML coallesed output.
type.register DOXYGEN_XML : doxygen : XML ;

# Doxygen HTML multifile directory.
type.register DOXYGEN_HTML_MULTIFILE : html-dir : HTML ;

# Redirection HTML file to HTML multifile directory.
type.register DOXYGEN_HTML : : HTML ;

type.register DOXYGEN_XML_IMAGES : doxygen-xml-images ;

# Initialize the Doxygen module. Parameters are:
#   name: the name of the 'doxygen' executable. If not specified, the name
#         'doxygen' will be used
#
rule init ( name ? )
{
    if ! $(.initialized)
    {
        .initialized = true ;

        .doxproc = [ modules.binding $(__name__) ] ;
        .doxproc = $(.doxproc:D)/doxproc.py ;

        generators.register-composing doxygen.headers-to-doxyfile
            : H HPP CPP : DOXYFILE ;
        generators.register-standard doxygen.run
            : DOXYFILE : DOXYGEN_XML_MULTIFILE ;
        generators.register-standard doxygen.xml-dir-to-boostbook
            : DOXYGEN_XML_MULTIFILE : BOOSTBOOK : <doxygen.processor>doxproc ;
        generators.register-standard doxygen.xml-to-boostbook
            : DOXYGEN_XML : BOOSTBOOK : <doxygen.processor>xsltproc ;
        generators.register-standard doxygen.collect
            : DOXYGEN_XML_MULTIFILE : DOXYGEN_XML ;
        generators.register-standard doxygen.run
            : DOXYFILE : DOXYGEN_HTML_MULTIFILE ;
        generators.register-standard doxygen.html-redirect
            : DOXYGEN_HTML_MULTIFILE : DOXYGEN_HTML ;
        generators.register-standard doxygen.copy-latex-pngs
            : DOXYGEN_HTML : DOXYGEN_XML_IMAGES ;

        IMPORT $(__name__) : doxygen : : doxygen ;
    }

    if $(name)
    {
        modify-config ;
        .doxygen = $(name) ;
        check-doxygen ;
    }

    if ! $(.doxygen)
    {
        check-doxygen ;
    }
}

rule freeze-config ( )
{
    if ! $(.initialized)
    {
        errors.user-error "doxygen must be initialized before it can be used." ;
    }
    if ! $(.config-frozen)
    {
        .config-frozen = true ;

        if [ .is-cygwin ]
        {
            .is-cygwin = true ;
        }
    }
}

rule modify-config ( )
{
    if $(.config-frozen)
    {
        errors.user-error "Cannot change doxygen after it has been used." ;
    }
}

rule check-doxygen ( )
{
    if --debug-configuration in [ modules.peek : ARGV ]
    {
        ECHO "notice:" using doxygen ":" $(.doxygen) ;
    }
    local extra-paths ;
    if [ os.name ] = NT
    {
        local ProgramFiles = [ modules.peek : ProgramFiles ] ;
        if $(ProgramFiles)
        {
            extra-paths = "$(ProgramFiles:J= )" ;
        }
        else
        {
            extra-paths = "C:\\Program Files" ;
        }
    }
    .doxygen = [ common.get-invocation-command doxygen :
                 doxygen : $(.doxygen) : $(extra-paths) ] ;
}

rule name ( )
{
    freeze-config ;
    return $(.doxygen) ;
}

rule .is-cygwin ( )
{
    if [ os.on-windows ]
    {
        local file = [ path.make [ modules.binding $(__name__) ] ] ;
        local dir = [ path.native
                      [ path.join [ path.parent $(file) ] doxygen ] ] ;
        local command =
         "cd \"$(dir)\" && \"$(.doxygen)\" windows-paths-check.doxyfile 2>&1" ;
        result = [ SHELL $(command) ] ;
        if [ MATCH "(Parsing file /)" : $(result) ]
        {
            return true ;
        }
    }
}

# Runs Doxygen on the given Doxygen configuration file (the source) to generate
# the Doxygen files. The output is dumped according to the settings in the
# Doxygen configuration file, not according to the target! Because of this, we
# essentially "touch" the target file, in effect making it look like we have
# really written something useful to it. Anyone that uses this action must deal
# with this behavior.
#
actions doxygen-action
{
    $(RM) "$(*.XML)" & "$(NAME:E=doxygen)" "$(>)" && echo "Stamped" > "$(<)"
}


# Runs the Python doxproc XML processor.
#
actions doxproc
{
    python "$(DOXPROC)" "--xmldir=$(>)" "--output=$(<)" "$(OPTIONS)" "--id=$(ID)" "--title=$(TITLE)"
}


rule translate-path ( path )
{
    freeze-config ;
    if [ os.on-windows ]
    {
        if [ os.name ] = CYGWIN
        {
            if $(.is-cygwin)
            {
                return $(path) ;
            }
            else
            {
                return $(path:W) ;
            }
        }
        else
        {
            if $(.is-cygwin)
            {
                match = [ MATCH ^(.):(.*) : $(path) ] ;
                if $(match)
                {
                    return /cygdrive/$(match[1])$(match[2]:T) ;
                }
                else
                {
                    return $(path:T) ;
                }
            }
            else
            {
                return $(path) ;
            }
        }
    }
    else
    {
        return $(path) ;
    }
}


# Generates a doxygen configuration file (doxyfile) given a set of C++ sources
# and a property list that may contain <doxygen:param> features.
#
rule headers-to-doxyfile ( target : sources * : properties * )
{
    local text "# Generated by Boost.Build version 2" ;

    local output-dir ;

    # Translate <doxygen:param> into command line flags.
    for local param in [ feature.get-values <doxygen:param> : $(properties) ]
    {
        local namevalue = [ regex.match ([^=]*)=(.*) : $(param) ] ;
        if $(namevalue[1]) = OUTPUT_DIRECTORY
        {
            output-dir = [ translate-path
                           [ utility.unquote $(namevalue[2]) ] ] ;
            text += "OUTPUT_DIRECTORY = \"$(output-dir)\"" ;
        }
        else
        {
            text += "$(namevalue[1]) = $(namevalue[2])" ;
        }
    }

    if ! $(output-dir)
    {
        output-dir = [ translate-path [ on $(target) return $(LOCATE) ] ] ;
        text += "OUTPUT_DIRECTORY = \"$(output-dir)\"" ;
    }

    local headers = ;
    for local header in $(sources:G=)
    {
        header = [ translate-path $(header) ] ;
        headers += \"$(header)\" ;
    }

    # Doxygen generates LaTex by default. So disable it unconditionally, or at
    # least until someone needs, and hence writes support for, LaTex output.
    text += "GENERATE_LATEX = NO" ;
    text += "INPUT = $(headers:J= )" ;
    print.output $(target) plain ;
    print.text $(text) : true ;
}


# Run Doxygen. See doxygen-action for a description of the strange properties of
# this rule.
#
rule run ( target : source : properties * )
{
    freeze-config ;
    if <doxygen.rmdir>on in $(properties)
    {
        local output-dir =
            [ path.make
                [ MATCH <doxygen:param>OUTPUT_DIRECTORY=\"?([^\"]*) :
                  $(properties) ] ] ;
        local html-dir =
            [ path.make
                [ MATCH <doxygen:param>HTML_OUTPUT=(.*) :
                  $(properties) ] ] ;
        if $(output-dir) && $(html-dir) &&
            [ path.glob $(output-dir) : $(html-dir) ]
        {
            HTMLDIR on $(target) =
                [ path.native [ path.join $(output-dir) $(html-dir) ] ] ;
            rm-htmldir $(target) ;
        }
    }
    doxygen-action $(target) : $(source) ;
    NAME on $(target) = $(.doxygen) ;
    RM on $(target) = [ modules.peek common : RM ] ;
    *.XML on $(target) =
        [ path.native
            [ path.join
                [ path.make [ on $(target) return $(LOCATE) ] ]
                $(target:B:S=)
                *.xml ] ] ;
}

if [ os.name ] = NT
{
    RMDIR = rmdir /s /q ;
}
else
{
    RMDIR = rm -rf ;
}

actions quietly rm-htmldir
{
    $(RMDIR) $(HTMLDIR)
}

# The rules below require Boost.Book stylesheets, so we need some code to check
# that the boostbook module has actualy been initialized.
#
rule check-boostbook ( )
{
    if ! [ modules.peek boostbook : .initialized ]
    {
        ECHO "error: the boostbook module is not initialized" ;
        ECHO "error: you've attempted to use the 'doxygen' toolset, " ;
        ECHO "error: which requires Boost.Book," ;
        ECHO "error: but never initialized Boost.Book." ;
        EXIT "error: Hint: add 'using boostbook ;' to your user-config.jam" ;
    }
}


# Collect the set of Doxygen XML files into a single XML source file that can be
# handled by an XSLT processor. The source is completely ignored (see
# doxygen-action), because this action picks up the Doxygen XML index file
# xml/index.xml. This is because we can not teach Doxygen to act like a NORMAL
# program and take a "-o output.xml" argument (grrrr). The target of the
# collection will be a single Doxygen XML file.
#
rule collect ( target : source : properties * )
{
    check-boostbook ;
    local collect-xsl-dir
        = [ path.native [ path.join [ boostbook.xsl-dir ] doxygen collect ] ] ;
    local source-path
        = [ path.make [ on $(source) return $(LOCATE) ] ] ;
    local collect-path
        = [ path.root [ path.join $(source-path) $(source:B) ] [ path.pwd ] ] ;
    local native-path
        = [ path.native $(collect-path) ] ;
    local real-source
        = [ path.native [ path.join $(collect-path) index.xml ] ] ;
    xsltproc.xslt $(target) : $(real-source) $(collect-xsl-dir:S=.xsl)
        : <xsl:param>doxygen.xml.path=$(native-path) ;
}


# Translate Doxygen XML into BoostBook.
#
rule xml-to-boostbook ( target : source : properties * )
{
    check-boostbook ;
    local xsl-dir = [ boostbook.xsl-dir ] ;
    local d2b-xsl = [ path.native [ path.join [ boostbook.xsl-dir ] doxygen
        doxygen2boostbook.xsl ] ] ;

    local xslt-properties = $(properties) ;
    for local prefix in [ feature.get-values <prefix> : $(properties) ]
    {
        xslt-properties += "<xsl:param>boost.doxygen.header.prefix=$(prefix)" ;
    }
    for local title in [ feature.get-values <reftitle> : $(properties) ]
    {
        xslt-properties += "<xsl:param>boost.doxygen.reftitle=$(title)" ;
    }

    xsltproc.xslt $(target) : $(source) $(d2b-xsl) : $(xslt-properties) ;
}


flags doxygen.xml-dir-to-boostbook OPTIONS <doxygen.doxproc.index>yes : --enable-index ;
flags doxygen.xml-dir-to-boostbook ID <doxygen.doxproc.id> ;
flags doxygen.xml-dir-to-boostbook TITLE <doxygen.doxproc.title> ;


rule xml-dir-to-boostbook ( target : source : properties * )
{
    DOXPROC on $(target) = $(.doxproc) ;

    LOCATE on $(source:S=) = [ on $(source) return $(LOCATE) ] ;

    doxygen.doxproc $(target) : $(source:S=) ;
}


# Generate the HTML redirect to HTML dir index.html file.
#
rule html-redirect ( target : source : properties * )
{
    local uri = "$(target:B)/index.html" ;
    print.output $(target) plain ;
    print.text
"<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\"
    \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">
<html xmlns=\"http://www.w3.org/1999/xhtml\">
<head>
  <meta http-equiv=\"refresh\" content=\"0; URL=$(uri)\" />

  <title></title>
</head>

<body>
  Automatic redirection failed, please go to <a href=
  \"$(uri)\">$(uri)</a>.
</body>
</html>
"
        : true ;
}

rule copy-latex-pngs ( target : source : requirements * )
{
    local directory = [ path.native
                        [ feature.get-values <doxygen:xml-imagedir> :
                          $(requirements) ] ] ;

    local location = [ on $(target) return $(LOCATE) ] ;

    local pdf-location =
        [ path.native
            [ path.join
                [ path.make $(location) ]
                [ path.make $(directory) ] ] ] ;
    local html-location =
        [ path.native
            [ path.join
                .
                html
                [ path.make $(directory) ] ] ] ;

    common.MkDir $(pdf-location) ;
    common.MkDir $(html-location) ;

    DEPENDS $(target) : $(pdf-location) $(html-location) ;

    if [ os.name ] = NT
    {
        CP on $(target) = copy /y ;
        FROM on $(target) = \\*.png ;
        TOHTML on $(target) = .\\html\\$(directory) ;
        TOPDF on $(target) = \\$(directory) ;
    }
    else
    {
        CP on $(target) = cp ;
        FROM on $(target) = /*.png ;
        TOHTML on $(target) = ./html/$(directory) ;
        TOPDF on $(target) = $(target:D)/$(directory) ;
    }
}

actions copy-latex-pngs
{
    $(CP) $(>:S=)$(FROM) $(TOHTML)
    $(CP) $(>:S=)$(FROM) $(<:D)$(TOPDF)
    echo "Stamped" > "$(<)"
}

# building latex images for doxygen XML depends
# on latex, dvips, and ps being in your PATH.
# This is true for most Unix installs, but
# not on Win32, where you will need to install
# MkTex and Ghostscript and add these tools
# to your path.

actions check-latex
{
    latex -version >$(<)
}

actions check-dvips
{
    dvips -version >$(<)
}

if [ os.name ] = "NT"
{
   actions check-gs
   {
       gswin32c -version >$(<)
   }
}
else
{
   actions check-gs
   {
       gs -version >$(<)
   }
}

rule check-tools ( )
{
    if ! $(.check-tools-targets)
    {
        # Find the root project.
        local root-project = [ project.current ] ;
        root-project = [ $(root-project).project-module ] ;
        while
            [ project.attribute $(root-project) parent-module ] &&
            [ project.attribute $(root-project) parent-module ] != user-config
        {
            root-project =
                [ project.attribute $(root-project) parent-module ] ;
        }

        .latex.check = [ new file-target latex.check
            :
            : [ project.target $(root-project) ]
            : [ new action : doxygen.check-latex ]
            :
            ] ;
        .dvips.check = [ new file-target dvips.check
            :
            : [ project.target $(root-project) ]
            : [ new action : doxygen.check-dvips ]
            :
            ] ;
        .gs.check = [ new file-target gs.check
            :
            : [ project.target $(root-project) ]
            : [ new action : doxygen.check-gs ]
            :
            ] ;
        .check-tools-targets = $(.latex.check) $(.dvips.check) $(.gs.check) ;
    }
    return $(.check-tools-targets) ;
}

project.initialize $(__name__) ;
project doxygen ;

class doxygen-check-tools-target-class : basic-target
{
    import doxygen ;
    rule construct ( name : sources * : property-set )
    {
        return [ property-set.empty ] [ doxygen.check-tools ] ;
    }
}

local project = [ project.current ] ;

targets.main-target-alternative
    [ new doxygen-check-tools-target-class check-tools : $(project)
        : [ targets.main-target-sources : check-tools : no-renaming ]
        : [ targets.main-target-requirements : $(project) ]
        : [ targets.main-target-default-build : $(project) ]
        : [ targets.main-target-usage-requirements : $(project) ]
    ] ;

# User-level rule to generate BoostBook XML from a set of headers via Doxygen.
#
rule doxygen ( target : sources * : requirements * : default-build * : usage-requirements * )
{
    freeze-config ;
    local project = [ project.current ] ;

    if $(target:S) = .html
    {
        # Build an HTML directory from the sources.
        local html-location = [ feature.get-values <location> : $(requirements) ] ;
        local output-dir ;
        if [ $(project).get build-dir ] 
        {
            # Explicitly specified build dir. Add html at the end.
            output-dir = [ path.join [ $(project).build-dir ] $(html-location:E=html) ] ;
        }
        else
        {
            # Trim 'bin' from implicit build dir, for no other reason that backward
            # compatibility.
            output-dir = [ path.join [ path.parent [ $(project).build-dir ] ] 
              $(html-location:E=html) ] ;
        }
        output-dir = [ path.root $(output-dir) [ path.pwd ] ] ;        
        local output-dir-native = [ path.native $(output-dir) ] ;
        requirements = [ property.change $(requirements) : <location> ] ;

        ## The doxygen configuration file.
        targets.main-target-alternative
            [ new typed-target $(target:S=.tag) : $(project) : DOXYFILE
                : [ targets.main-target-sources $(sources) : $(target:S=.tag) ]
                : [ targets.main-target-requirements $(requirements)
                    <doxygen:param>GENERATE_HTML=YES
                    <doxygen:param>GENERATE_XML=NO
                    <doxygen:param>"OUTPUT_DIRECTORY=\"$(output-dir-native)\""
                    <doxygen:param>HTML_OUTPUT=$(target:B)
                    : $(project) ]
                : [ targets.main-target-default-build $(default-build) : $(project) ]
            ] ;
        $(project).mark-target-as-explicit $(target:S=.tag) ;

        ## The html directory to generate by running doxygen.
        targets.main-target-alternative
            [ new typed-target $(target:S=.dir) : $(project) : DOXYGEN_HTML_MULTIFILE
                : $(target:S=.tag)
                : [ targets.main-target-requirements $(requirements)
                    <doxygen:param>"OUTPUT_DIRECTORY=\"$(output-dir-native)\""
                    <doxygen:param>HTML_OUTPUT=$(target:B)
                    : $(project) ]
                : [ targets.main-target-default-build $(default-build) : $(project) ]
                ] ;
        $(project).mark-target-as-explicit $(target:S=.dir) ;

        ## The redirect html file into the generated html.
        targets.main-target-alternative
            [ new typed-target $(target) : $(project) : DOXYGEN_HTML
                : $(target:S=.dir)
                : [ targets.main-target-requirements $(requirements)
                    <location>$(output-dir)
                    : $(project) ]
                : [ targets.main-target-default-build $(default-build) : $(project) ]
                ] ;
    }
    else
    {
        # Build a BoostBook XML file from the sources.
        local location-xml = [ feature.get-values <location> : $(requirements) ] ;
        requirements = [ property.change $(requirements) : <location> ] ;
        local target-xml = $(target:B=$(target:B)-xml) ;

        # Check whether we need to build images
        local images-location =
            [ feature.get-values <doxygen:xml-imagedir> : $(requirements) ] ;
        if $(images-location)
        {
            doxygen $(target).doxygen-xml-images.html : $(sources)
                : $(requirements)
                  <doxygen.rmdir>on
                  <doxygen:param>QUIET=YES
                  <doxygen:param>WARNINGS=NO
                  <doxygen:param>WARN_IF_UNDOCUMENTED=NO
                  <dependency>/doxygen//check-tools ;
            $(project).mark-target-as-explicit
                $(target).doxygen-xml-images.html ;

            targets.main-target-alternative
                [ new typed-target $(target).doxygen-xml-images
                    : $(project) : DOXYGEN_XML_IMAGES
                    : $(target).doxygen-xml-images.html
                    : [ targets.main-target-requirements $(requirements)
                        : $(project) ]
                    : [ targets.main-target-default-build $(default-build)
                        : $(project) ]
                ] ;

            $(project).mark-target-as-explicit
                $(target).doxygen-xml-images ;

            if ! [ regex.match "^(.*/)$" : $(images-location) ]
            {
                images-location = $(images-location)/ ;
            }

            requirements +=
                <dependency>$(target).doxygen-xml-images
                <xsl:param>boost.doxygen.formuladir=$(images-location) ;
        }

        ## The doxygen configuration file.
        targets.main-target-alternative
            [ new typed-target $(target-xml:S=.tag) : $(project) : DOXYFILE
                : [ targets.main-target-sources $(sources) : $(target-xml:S=.tag) ]
                : [ targets.main-target-requirements $(requirements)
                    <doxygen:param>GENERATE_HTML=NO
                    <doxygen:param>GENERATE_XML=YES
                    <doxygen:param>XML_OUTPUT=$(target-xml)
                    : $(project) ]
                : [ targets.main-target-default-build $(default-build) : $(project) ]
            ] ;
        $(project).mark-target-as-explicit $(target-xml:S=.tag) ;

        ## The Doxygen XML directory of the processed source files.
        targets.main-target-alternative
            [ new typed-target $(target-xml:S=.dir) : $(project) : DOXYGEN_XML_MULTIFILE
                : $(target-xml:S=.tag)
                : [ targets.main-target-requirements $(requirements)
                    : $(project) ]
                : [ targets.main-target-default-build $(default-build) : $(project) ]
            ] ;
        $(project).mark-target-as-explicit $(target-xml:S=.dir) ;

        ## The resulting BoostBook file is generated by the processor tool. The
        ## tool can be either the xsltproc plus accompanying XSL scripts. Or it
        ## can be the python doxproc.py script.
        targets.main-target-alternative
            [ new typed-target $(target-xml) : $(project) : BOOSTBOOK
                :  $(target-xml:S=.dir)
                : [ targets.main-target-requirements $(requirements)
                    : $(project) ]
                : [ targets.main-target-default-build $(default-build) : $(project) ]
            ] ;
        $(project).mark-target-as-explicit $(target-xml) ;

        targets.main-target-alternative
            [ new install-target-class $(target:S=.xml) : $(project)
                : $(target-xml)
                : [ targets.main-target-requirements $(requirements)
                    <location>$(location-xml:E=.)
                    <name>$(target:S=.xml)
                    : $(project) ]
                : [ targets.main-target-default-build $(default-build) : $(project) ]
            ] ;
        $(project).mark-target-as-explicit $(target:S=.xml) ;

        targets.main-target-alternative
            [ new alias-target-class $(target) : $(project)
                :
                : [ targets.main-target-requirements $(requirements)
                    : $(project) ]
                : [ targets.main-target-default-build $(default-build) : $(project) ]
                : [ targets.main-target-usage-requirements $(usage-requirements)
                    <dependency>$(target:S=.xml)
                    : $(project) ]
            ] ;
    }
}