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

#include <assert.h>
#include <set>
#include <stack>
#include <stdio.h>
#include <string>
#include <vector>

bool
cmFortranParser_s::FindIncludeFile(const char* dir, const char* includeName,
                                   std::string& fileName)
{
    // If the file is a full path, include it directly.
    if(cmSystemTools::FileIsFullPath(includeName))
    {
        fileName = includeName;
        return cmSystemTools::FileExists(fileName, true);
    }
    // Check for the file in the directory containing the including
    // file.
    std::string fullName = dir;
    fullName += "/";
    fullName += includeName;
    if(cmSystemTools::FileExists(fullName, true))
    {
        fileName = fullName;
        return true;
    }

    // Search the include path for the file.
    for(std::string const& i : this->IncludePath)
    {
        fullName = i;
        fullName += "/";
        fullName += includeName;
        if(cmSystemTools::FileExists(fullName, true))
        {
            fileName = fullName;
            return true;
        }
    }
    return false;
}

cmFortranParser_s::cmFortranParser_s(std::vector<std::string> const& includes,
                                     std::set<std::string> const&    defines,
                                     cmFortranSourceInfo&            info)
: IncludePath(includes)
, PPDefinitions(defines)
, Info(info)
{
    this->InInterface     = false;
    this->InPPFalseBranch = 0;

    // Initialize the lexical scanner.
    cmFortran_yylex_init(&this->Scanner);
    cmFortran_yyset_extra(this, this->Scanner);

    // Create a dummy buffer that is never read but is the fallback
    // buffer when the last file is popped off the stack.
    YY_BUFFER_STATE buffer =
        cmFortran_yy_create_buffer(nullptr, 4, this->Scanner);
    cmFortran_yy_switch_to_buffer(buffer, this->Scanner);
}

cmFortranParser_s::~cmFortranParser_s()
{
    cmFortran_yylex_destroy(this->Scanner);
}

bool
cmFortranParser_FilePush(cmFortranParser* parser, const char* fname)
{
    // Open the new file and push it onto the stack.  Save the old
    // buffer with it on the stack.
    if(FILE* file = cmsys::SystemTools::Fopen(fname, "rb"))
    {
        YY_BUFFER_STATE current =
            cmFortranLexer_GetCurrentBuffer(parser->Scanner);
        std::string     dir = cmSystemTools::GetParentDirectory(fname);
        cmFortranFile   f(file, current, dir);
        YY_BUFFER_STATE buffer =
            cmFortran_yy_create_buffer(nullptr, 16384, parser->Scanner);
        cmFortran_yy_switch_to_buffer(buffer, parser->Scanner);
        parser->FileStack.push(f);
        return true;
    }
    return false;
}

bool
cmFortranParser_FilePop(cmFortranParser* parser)
{
    // Pop one file off the stack and close it.  Switch the lexer back
    // to the next one on the stack.
    if(parser->FileStack.empty())
    {
        return false;
    }
    cmFortranFile f = parser->FileStack.top();
    parser->FileStack.pop();
    fclose(f.File);
    YY_BUFFER_STATE current = cmFortranLexer_GetCurrentBuffer(parser->Scanner);
    cmFortran_yy_delete_buffer(current, parser->Scanner);
    cmFortran_yy_switch_to_buffer(f.Buffer, parser->Scanner);
    return true;
}

int
cmFortranParser_Input(cmFortranParser* parser, char* buffer, size_t bufferSize)
{
    // Read from the file on top of the stack.  If the stack is empty,
    // the end of the translation unit has been reached.
    if(!parser->FileStack.empty())
    {
        cmFortranFile& ff   = parser->FileStack.top();
        FILE*          file = ff.File;
        size_t         n    = fread(buffer, 1, bufferSize, file);
        if(n > 0)
        {
            ff.LastCharWasNewline = buffer[n - 1] == '\n';
        } else if(!ff.LastCharWasNewline)
        {
            // The file ended without a newline.  Inject one so
            // that the file always ends in an end-of-statement.
            buffer[0]             = '\n';
            n                     = 1;
            ff.LastCharWasNewline = true;
        }
        return static_cast<int>(n);
    }
    return 0;
}

void
cmFortranParser_StringStart(cmFortranParser* parser)
{
    parser->TokenString.clear();
}

const char*
cmFortranParser_StringEnd(cmFortranParser* parser)
{
    return parser->TokenString.c_str();
}

void
cmFortranParser_StringAppend(cmFortranParser* parser, char c)
{
    parser->TokenString += c;
}

void
cmFortranParser_SetInInterface(cmFortranParser* parser, bool in)
{
    if(parser->InPPFalseBranch)
    {
        return;
    }

    parser->InInterface = in;
}

bool
cmFortranParser_GetInInterface(cmFortranParser* parser)
{
    return parser->InInterface;
}

void
cmFortranParser_SetOldStartcond(cmFortranParser* parser, int arg)
{
    parser->OldStartcond = arg;
}

int
cmFortranParser_GetOldStartcond(cmFortranParser* parser)
{
    return parser->OldStartcond;
}

void
cmFortranParser_Error(cmFortranParser* parser, const char* msg)
{
    parser->Error = msg ? msg : "unknown error";
}

void
cmFortranParser_RuleUse(cmFortranParser* parser, const char* module_name)
{
    if(parser->InPPFalseBranch)
    {
        return;
    }

    // syntax:   "use module_name"
    // requires: "module_name.mod"
    std::string const& mod_name = cmSystemTools::LowerCase(module_name);
    parser->Info.Requires.insert(mod_name + ".mod");
}

void
cmFortranParser_RuleLineDirective(cmFortranParser* parser, const char* filename)
{
    // This is a #line directive naming a file encountered during preprocessing.
    std::string included = filename;

    // Skip #line directives referencing non-files like
    // "<built-in>" or "<command-line>".
    if(included.empty() || included[0] == '<')
    {
        return;
    }

    // Fix windows file path separators since our lexer does not
    // process escape sequences in string literals.
    cmSystemTools::ReplaceString(included, "\\\\", "\\");
    cmSystemTools::ConvertToUnixSlashes(included);

    // Save the named file as included in the source.
    if(cmSystemTools::FileExists(included, true))
    {
        parser->Info.Includes.insert(included);
    }
}

void
cmFortranParser_RuleInclude(cmFortranParser* parser, const char* name)
{
    if(parser->InPPFalseBranch)
    {
        return;
    }

    // If processing an include statement there must be an open file.
    assert(!parser->FileStack.empty());

    // Get the directory containing the source in which the include
    // statement appears.  This is always the first search location for
    // Fortran include files.
    std::string dir = parser->FileStack.top().Directory;

    // Find the included file.  If it cannot be found just ignore the
    // problem because either the source will not compile or the user
    // does not care about depending on this included source.
    std::string fullName;
    if(parser->FindIncludeFile(dir.c_str(), name, fullName))
    {
        // Found the included file.  Save it in the set of included files.
        parser->Info.Includes.insert(fullName);

        // Parse it immediately to translate the source inline.
        cmFortranParser_FilePush(parser, fullName.c_str());
    }
}

void
cmFortranParser_RuleModule(cmFortranParser* parser, const char* module_name)
{
    if(parser->InPPFalseBranch)
    {
        return;
    }

    if(!parser->InInterface)
    {
        // syntax:   "module module_name"
        // provides: "module_name.mod"
        std::string const& mod_name = cmSystemTools::LowerCase(module_name);
        parser->Info.Provides.insert(mod_name + ".mod");
    }
}

void
cmFortranParser_RuleSubmodule(cmFortranParser* parser, const char* module_name,
                              const char* submodule_name)
{
    if(parser->InPPFalseBranch)
    {
        return;
    }

    // syntax:   "submodule (module_name) submodule_name"
    // requires: "module_name.mod"
    // provides: "module_name@submodule_name.smod"
    //
    // FIXME: Some compilers split the submodule part of a module into a
    // separate "module_name.smod" file.  Whether it is generated or
    // not depends on conditions more subtle than we currently detect.
    // For now we depend directly on "module_name.mod".

    std::string const& mod_name = cmSystemTools::LowerCase(module_name);
    std::string const& sub_name = cmSystemTools::LowerCase(submodule_name);
    parser->Info.Requires.insert(mod_name + ".mod");
    parser->Info.Provides.insert(mod_name + "@" + sub_name + ".smod");
}

void
cmFortranParser_RuleSubmoduleNested(cmFortranParser* parser,
                                    const char*      module_name,
                                    const char*      submodule_name,
                                    const char*      nested_submodule_name)
{
    if(parser->InPPFalseBranch)
    {
        return;
    }

    // syntax:   "submodule (module_name:submodule_name) nested_submodule_name"
    // requires: "module_name@submodule_name.smod"
    // provides: "module_name@nested_submodule_name.smod"

    std::string const& mod_name = cmSystemTools::LowerCase(module_name);
    std::string const& sub_name = cmSystemTools::LowerCase(submodule_name);
    std::string const& nest_name =
        cmSystemTools::LowerCase(nested_submodule_name);
    parser->Info.Requires.insert(mod_name + "@" + sub_name + ".smod");
    parser->Info.Provides.insert(mod_name + "@" + nest_name + ".smod");
}

void
cmFortranParser_RuleDefine(cmFortranParser* parser, const char* macro)
{
    if(!parser->InPPFalseBranch)
    {
        parser->PPDefinitions.insert(macro);
    }
}

void
cmFortranParser_RuleUndef(cmFortranParser* parser, const char* macro)
{
    if(!parser->InPPFalseBranch)
    {
        std::set<std::string>::iterator match;
        match = parser->PPDefinitions.find(macro);
        if(match != parser->PPDefinitions.end())
        {
            parser->PPDefinitions.erase(match);
        }
    }
}

void
cmFortranParser_RuleIfdef(cmFortranParser* parser, const char* macro)
{
    // A new PP branch has been opened
    parser->SkipToEnd.push(false);

    if(parser->InPPFalseBranch)
    {
        parser->InPPFalseBranch++;
    } else if(parser->PPDefinitions.find(macro) == parser->PPDefinitions.end())
    {
        parser->InPPFalseBranch = 1;
    } else
    {
        parser->SkipToEnd.top() = true;
    }
}

void
cmFortranParser_RuleIfndef(cmFortranParser* parser, const char* macro)
{
    // A new PP branch has been opened
    parser->SkipToEnd.push(false);

    if(parser->InPPFalseBranch)
    {
        parser->InPPFalseBranch++;
    } else if(parser->PPDefinitions.find(macro) != parser->PPDefinitions.end())
    {
        parser->InPPFalseBranch = 1;
    } else
    {
        // ignore other branches
        parser->SkipToEnd.top() = true;
    }
}

void
cmFortranParser_RuleIf(cmFortranParser* parser)
{
    /* Note: The current parser is _not_ able to get statements like
     *   #if 0
     *   #if 1
     *   #if MYSMBOL
     *   #if defined(MYSYMBOL)
     *   #if defined(MYSYMBOL) && ...
     * right.  The same for #elif.  Thus in
     *   #if SYMBOL_1
     *     ..
     *   #elif SYMBOL_2
     *     ...
     *     ...
     *   #elif SYMBOL_N
     *     ..
     *   #else
     *     ..
     *   #endif
     * _all_ N+1 branches are considered.  If you got something like this
     *   #if defined(MYSYMBOL)
     *   #if !defined(MYSYMBOL)
     * use
     *   #ifdef MYSYMBOL
     *   #ifndef MYSYMBOL
     * instead.
     */

    // A new PP branch has been opened
    // Never skip!  See note above.
    parser->SkipToEnd.push(false);
}

void
cmFortranParser_RuleElif(cmFortranParser* parser)
{
    /* Note: There are parser limitations.  See the note at
     * cmFortranParser_RuleIf(..)
     */

    // Always taken unless an #ifdef or #ifndef-branch has been taken
    // already.  If the second condition isn't meet already
    // (parser->InPPFalseBranch == 0) correct it.
    if(!parser->SkipToEnd.empty() && parser->SkipToEnd.top() &&
       !parser->InPPFalseBranch)
    {
        parser->InPPFalseBranch = 1;
    }
}

void
cmFortranParser_RuleElse(cmFortranParser* parser)
{
    // if the parent branch is false do nothing!
    if(parser->InPPFalseBranch > 1)
    {
        return;
    }

    // parser->InPPFalseBranch is either 0 or 1.  We change it depending on
    // parser->SkipToEnd.top()
    if(!parser->SkipToEnd.empty() && parser->SkipToEnd.top())
    {
        parser->InPPFalseBranch = 1;
    } else
    {
        parser->InPPFalseBranch = 0;
    }
}

void
cmFortranParser_RuleEndif(cmFortranParser* parser)
{
    if(!parser->SkipToEnd.empty())
    {
        parser->SkipToEnd.pop();
    }

    // #endif doesn't know if there was a "#else" in before, so it
    // always decreases InPPFalseBranch
    if(parser->InPPFalseBranch)
    {
        parser->InPPFalseBranch--;
    }
}
