/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
   file Copyright.txt or https://cmake.org/licensing for details.  */
#include "cmRST.h"

#include "cmAlgorithms.h"
#include "cmSystemTools.h"
#include "cmVersion.h"

#include "cmsys/FStream.hxx"
#include <algorithm>
#include <ctype.h>
#include <iterator>
#include <stddef.h>
#include <utility>

cmRST::cmRST(std::ostream& os, std::string const& docroot)
: OS(os)
, DocRoot(docroot)
, IncludeDepth(0)
, OutputLinePending(false)
, LastLineEndedInColonColon(false)
, Markup(MarkupNone)
, Directive(DirectiveNone)
, CMakeDirective("^.. (cmake:)?("
                 "command|variable"
                 ")::[ \t]+([^ \t\n]+)$")
, CMakeModuleDirective("^.. cmake-module::[ \t]+([^ \t\n]+)$")
, ParsedLiteralDirective("^.. parsed-literal::[ \t]*(.*)$")
, CodeBlockDirective("^.. code-block::[ \t]*(.*)$")
, ReplaceDirective("^.. (\\|[^|]+\\|) replace::[ \t]*(.*)$")
, IncludeDirective("^.. include::[ \t]+([^ \t\n]+)$")
, TocTreeDirective("^.. toctree::[ \t]*(.*)$")
, ProductionListDirective("^.. productionlist::[ \t]*(.*)$")
, NoteDirective("^.. note::[ \t]*(.*)$")
, ModuleRST("^#\\[(=*)\\[\\.rst:$")
, CMakeRole("(:cmake)?:("
            "command|cpack_gen|generator|variable|envvar|module|policy|"
            "prop_cache|prop_dir|prop_gbl|prop_inst|prop_sf|"
            "prop_test|prop_tgt|"
            "manual"
            "):`(<*([^`<]|[^` \t]<)*)([ \t]+<[^`]*>)?`")
, InlineLink("`(<*([^`<]|[^` \t]<)*)([ \t]+<[^`]*>)?`_")
, InlineLiteral("``([^`]*)``")
, Substitution("(^|[^A-Za-z0-9_])"
               "((\\|[^| \t\r\n]([^|\r\n]*[^| \t\r\n])?\\|)(__|_|))"
               "([^A-Za-z0-9_]|$)")
, TocTreeLink("^.*[ \t]+<([^>]+)>$")
{
    this->Replace["|release|"] = cmVersion::GetCMakeVersion();
}

bool
cmRST::ProcessFile(std::string const& fname, bool isModule)
{
    cmsys::ifstream fin(fname.c_str());
    if(fin)
    {
        this->DocDir = cmSystemTools::GetFilenamePath(fname);
        if(isModule)
        {
            this->ProcessModule(fin);
        } else
        {
            this->ProcessRST(fin);
        }
        this->OutputLinePending = true;
        return true;
    }
    return false;
}

void
cmRST::ProcessRST(std::istream& is)
{
    std::string line;
    while(cmSystemTools::GetLineFromStream(is, line))
    {
        this->ProcessLine(line);
    }
    this->Reset();
}

void
cmRST::ProcessModule(std::istream& is)
{
    std::string line;
    std::string rst;
    while(cmSystemTools::GetLineFromStream(is, line))
    {
        if(!rst.empty() && rst != "#")
        {
            // Bracket mode: check for end bracket
            std::string::size_type pos = line.find(rst);
            if(pos == std::string::npos)
            {
                this->ProcessLine(line);
            } else
            {
                if(line[0] != '#')
                {
                    this->ProcessLine(line.substr(0, pos));
                }
                rst.clear();
                this->Reset();
                this->OutputLinePending = true;
            }
        } else
        {
            // Line mode: check for .rst start (bracket or line)
            if(rst == "#")
            {
                if(line == "#")
                {
                    this->ProcessLine("");
                    continue;
                }
                if(line.substr(0, 2) == "# ")
                {
                    this->ProcessLine(line.substr(2));
                    continue;
                }
                rst.clear();
                this->Reset();
                this->OutputLinePending = true;
            }
            if(line == "#.rst:")
            {
                rst = "#";
            } else if(this->ModuleRST.find(line))
            {
                rst = "]" + this->ModuleRST.match(1) + "]";
            }
        }
    }
    if(rst == "#")
    {
        this->Reset();
    }
}

void
cmRST::Reset()
{
    if(!this->MarkupLines.empty())
    {
        this->UnindentLines(this->MarkupLines);
    }
    switch(this->Directive)
    {
        case DirectiveNone:
            break;
        case DirectiveParsedLiteral:
            this->ProcessDirectiveParsedLiteral();
            break;
        case DirectiveLiteralBlock:
            this->ProcessDirectiveLiteralBlock();
            break;
        case DirectiveCodeBlock:
            this->ProcessDirectiveCodeBlock();
            break;
        case DirectiveReplace:
            this->ProcessDirectiveReplace();
            break;
        case DirectiveTocTree:
            this->ProcessDirectiveTocTree();
            break;
    }
    this->Markup    = MarkupNone;
    this->Directive = DirectiveNone;
    this->MarkupLines.clear();
}

void
cmRST::ProcessLine(std::string const& line)
{
    bool lastLineEndedInColonColon  = this->LastLineEndedInColonColon;
    this->LastLineEndedInColonColon = false;

    // A line starting in .. is an explicit markup start.
    if(line == ".." || (line.size() >= 3 && line[0] == '.' && line[1] == '.' &&
                        isspace(line[2])))
    {
        this->Reset();
        this->Markup = (line.find_first_not_of(" \t", 2) == std::string::npos
                            ? MarkupEmpty
                            : MarkupNormal);
        if(this->CMakeDirective.find(line))
        {
            // Output cmake domain directives and their content normally.
            this->NormalLine(line);
        } else if(this->CMakeModuleDirective.find(line))
        {
            // Process cmake-module directive: scan .cmake file comments.
            std::string file = this->CMakeModuleDirective.match(1);
            if(file.empty() || !this->ProcessInclude(file, IncludeModule))
            {
                this->NormalLine(line);
            }
        } else if(this->ParsedLiteralDirective.find(line))
        {
            // Record the literal lines to output after whole block.
            this->Directive = DirectiveParsedLiteral;
            this->MarkupLines.push_back(this->ParsedLiteralDirective.match(1));
        } else if(this->CodeBlockDirective.find(line))
        {
            // Record the literal lines to output after whole block.
            // Ignore the language spec and record the opening line as blank.
            this->Directive = DirectiveCodeBlock;
            this->MarkupLines.push_back("");
        } else if(this->ReplaceDirective.find(line))
        {
            // Record the replace directive content.
            this->Directive   = DirectiveReplace;
            this->ReplaceName = this->ReplaceDirective.match(1);
            this->MarkupLines.push_back(this->ReplaceDirective.match(2));
        } else if(this->IncludeDirective.find(line))
        {
            // Process the include directive or output the directive and its
            // content normally if it fails.
            std::string file = this->IncludeDirective.match(1);
            if(file.empty() || !this->ProcessInclude(file, IncludeNormal))
            {
                this->NormalLine(line);
            }
        } else if(this->TocTreeDirective.find(line))
        {
            // Record the toctree entries to process after whole block.
            this->Directive = DirectiveTocTree;
            this->MarkupLines.push_back(this->TocTreeDirective.match(1));
        } else if(this->ProductionListDirective.find(line))
        {
            // Output productionlist directives and their content normally.
            this->NormalLine(line);
        } else if(this->NoteDirective.find(line))
        {
            // Output note directives and their content normally.
            this->NormalLine(line);
        }
    }
    // An explicit markup start followed nothing but whitespace and a
    // blank line does not consume any indented text following.
    else if(this->Markup == MarkupEmpty && line.empty())
    {
        this->NormalLine(line);
    }
    // Indented lines following an explicit markup start are explicit markup.
    else if(this->Markup && (line.empty() || isspace(line[0])))
    {
        this->Markup = MarkupNormal;
        // Record markup lines if the start line was recorded.
        if(!this->MarkupLines.empty())
        {
            this->MarkupLines.push_back(line);
        }
    }
    // A blank line following a paragraph ending in "::" starts a literal block.
    else if(lastLineEndedInColonColon && line.empty())
    {
        // Record the literal lines to output after whole block.
        this->Markup    = MarkupNormal;
        this->Directive = DirectiveLiteralBlock;
        this->MarkupLines.push_back("");
        this->OutputLine("", false);
    }
    // Print non-markup lines.
    else
    {
        this->NormalLine(line);
        this->LastLineEndedInColonColon =
            (line.size() >= 2 && line[line.size() - 2] == ':' &&
             line[line.size() - 1] == ':');
    }
}

void
cmRST::NormalLine(std::string const& line)
{
    this->Reset();
    this->OutputLine(line, true);
}

void
cmRST::OutputLine(std::string const& line_in, bool inlineMarkup)
{
    if(this->OutputLinePending)
    {
        this->OS << "\n";
        this->OutputLinePending = false;
    }
    if(inlineMarkup)
    {
        std::string            line = this->ReplaceSubstitutions(line_in);
        std::string::size_type pos  = 0;
        for(;;)
        {
            std::string::size_type* first      = nullptr;
            std::string::size_type  role_start = std::string::npos;
            std::string::size_type  link_start = std::string::npos;
            std::string::size_type  lit_start  = std::string::npos;
            if(this->CMakeRole.find(line.c_str() + pos))
            {
                role_start = this->CMakeRole.start();
                first      = &role_start;
            }
            if(this->InlineLiteral.find(line.c_str() + pos))
            {
                lit_start = this->InlineLiteral.start();
                if(!first || lit_start < *first)
                {
                    first = &lit_start;
                }
            }
            if(this->InlineLink.find(line.c_str() + pos))
            {
                link_start = this->InlineLink.start();
                if(!first || link_start < *first)
                {
                    first = &link_start;
                }
            }
            if(first == &role_start)
            {
                this->OS << line.substr(pos, role_start);
                std::string text = this->CMakeRole.match(3);
                // If a command reference has no explicit target and
                // no explicit "(...)" then add "()" to the text.
                if(this->CMakeRole.match(2) == "command" &&
                   this->CMakeRole.match(5).empty() &&
                   text.find_first_of("()") == std::string::npos)
                {
                    text += "()";
                }
                this->OS << "``" << text << "``";
                pos += this->CMakeRole.end();
            } else if(first == &lit_start)
            {
                this->OS << line.substr(pos, lit_start);
                std::string text = this->InlineLiteral.match(1);
                pos += this->InlineLiteral.end();
                this->OS << "``" << text << "``";
            } else if(first == &link_start)
            {
                this->OS << line.substr(pos, link_start);
                std::string text    = this->InlineLink.match(1);
                bool        escaped = false;
                for(char c : text)
                {
                    if(escaped)
                    {
                        escaped = false;
                        this->OS << c;
                    } else if(c == '\\')
                    {
                        escaped = true;
                    } else
                    {
                        this->OS << c;
                    }
                }
                pos += this->InlineLink.end();
            } else
            {
                break;
            }
        }
        this->OS << line.substr(pos) << "\n";
    } else
    {
        this->OS << line_in << "\n";
    }
}

std::string
cmRST::ReplaceSubstitutions(std::string const& line)
{
    std::string            out;
    std::string::size_type pos = 0;
    while(this->Substitution.find(line.c_str() + pos))
    {
        std::string::size_type start      = this->Substitution.start(2);
        std::string::size_type end        = this->Substitution.end(2);
        std::string            substitute = this->Substitution.match(3);
        std::map<std::string, std::string>::iterator replace =
            this->Replace.find(substitute);
        if(replace != this->Replace.end())
        {
            std::pair<std::set<std::string>::iterator, bool> replaced =
                this->Replaced.insert(substitute);
            if(replaced.second)
            {
                substitute = this->ReplaceSubstitutions(replace->second);
                this->Replaced.erase(replaced.first);
            }
        }
        out += line.substr(pos, start);
        out += substitute;
        pos += end;
    }
    out += line.substr(pos);
    return out;
}

void
cmRST::OutputMarkupLines(bool inlineMarkup)
{
    for(auto line : this->MarkupLines)
    {
        if(!line.empty())
        {
            line = " " + line;
        }
        this->OutputLine(line, inlineMarkup);
    }
    this->OutputLinePending = true;
}

bool
cmRST::ProcessInclude(std::string file, IncludeType type)
{
    bool found = false;
    if(this->IncludeDepth < 10)
    {
        cmRST r(this->OS, this->DocRoot);
        r.IncludeDepth      = this->IncludeDepth + 1;
        r.OutputLinePending = this->OutputLinePending;
        if(type != IncludeTocTree)
        {
            r.Replace = this->Replace;
        }
        if(file[0] == '/')
        {
            file = this->DocRoot + file;
        } else
        {
            file = this->DocDir + "/" + file;
        }
        found = r.ProcessFile(file, type == IncludeModule);
        if(type != IncludeTocTree)
        {
            this->Replace = r.Replace;
        }
        this->OutputLinePending = r.OutputLinePending;
    }
    return found;
}

void
cmRST::ProcessDirectiveParsedLiteral()
{
    this->OutputMarkupLines(true);
}

void
cmRST::ProcessDirectiveLiteralBlock()
{
    this->OutputMarkupLines(false);
}

void
cmRST::ProcessDirectiveCodeBlock()
{
    this->OutputMarkupLines(false);
}

void
cmRST::ProcessDirectiveReplace()
{
    // Record markup lines as replacement text.
    std::string& replacement = this->Replace[this->ReplaceName];
    replacement += cmJoin(this->MarkupLines, " ");
    this->ReplaceName.clear();
}

void
cmRST::ProcessDirectiveTocTree()
{
    // Process documents referenced by toctree directive.
    for(std::string const& line : this->MarkupLines)
    {
        if(!line.empty() && line[0] != ':')
        {
            if(this->TocTreeLink.find(line))
            {
                std::string const& link = this->TocTreeLink.match(1);
                this->ProcessInclude(link + ".rst", IncludeTocTree);
            } else
            {
                this->ProcessInclude(line + ".rst", IncludeTocTree);
            }
        }
    }
}

void
cmRST::UnindentLines(std::vector<std::string>& lines)
{
    // Remove the common indentation from the second and later lines.
    std::string            indentText;
    std::string::size_type indentEnd = 0;
    bool                   first     = true;
    for(size_t i = 1; i < lines.size(); ++i)
    {
        std::string const& line = lines[i];

        // Do not consider empty lines.
        if(line.empty())
        {
            continue;
        }

        // Record indentation on first non-empty line.
        if(first)
        {
            first      = false;
            indentEnd  = line.find_first_not_of(" \t");
            indentText = line.substr(0, indentEnd);
            continue;
        }

        // Truncate indentation to match that on this line.
        indentEnd = std::min(indentEnd, line.size());
        for(std::string::size_type j = 0; j != indentEnd; ++j)
        {
            if(line[j] != indentText[j])
            {
                indentEnd = j;
                break;
            }
        }
    }

    // Update second and later lines.
    for(size_t i = 1; i < lines.size(); ++i)
    {
        std::string& line = lines[i];
        if(!line.empty())
        {
            line = line.substr(indentEnd);
        }
    }

    std::vector<std::string>::const_iterator it = lines.begin();
    size_t leadingEmpty = std::distance(it, cmFindNot(lines, std::string()));

    std::vector<std::string>::const_reverse_iterator rit = lines.rbegin();
    size_t                                           trailingEmpty =
        std::distance(rit, cmFindNot(cmReverseRange(lines), std::string()));

    std::vector<std::string>::iterator contentEnd =
        cmRotate(lines.begin(), lines.begin() + leadingEmpty,
                 lines.end() - trailingEmpty);
    lines.erase(contentEnd, lines.end());
}
