/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
   file LICENSE.rst or https://cmake.org/licensing for details.  */
#include <cassert>
#include <cstdio>
#include <set>
#include <stack>
#include <string>
#include <utility>
#include <vector>

#include "cmFortranParser.h"
#include "cmStringAlgorithms.h"
#include "cmSystemTools.h"

bool cmFortranParser_s::FindIncludeFile(char const* dir,
                                        char const* 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 = cmStrCat(dir, '/', 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 = cmStrCat(i, '/', includeName);
    if (cmSystemTools::FileExists(fullName, true)) {
      fileName = fullName;
      return true;
    }
  }
  return false;
}

cmFortranParser_s::cmFortranParser_s(cmFortranCompiler fc,
                                     std::vector<std::string> includes,
                                     std::set<std::string> defines,
                                     cmFortranSourceInfo& info)
  : Compiler(std::move(fc))
  , IncludePath(std::move(includes))
  , PPDefinitions(std::move(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);
}

std::string cmFortranParser_s::ModName(std::string const& mod_name) const
{
  return mod_name + ".mod";
}

std::string cmFortranParser_s::SModName(std::string const& mod_name,
                                        std::string const& sub_name) const
{
  std::string const& SModExt =
    this->Compiler.SModExt.empty() ? ".mod" : this->Compiler.SModExt;
  // An empty separator means that the compiler does not use a prefix.
  if (this->Compiler.SModSep.empty()) {
    return sub_name + SModExt;
  }
  return mod_name + this->Compiler.SModSep + sub_name + SModExt;
}

bool cmFortranParser_FilePush(cmFortranParser* parser, char const* fname)
{
  // Do not revisit already processed files; treat them as non-existing.
  // Avoids infinite recursion caused by misinterpreted include guards.
  if (parser->VisitedFilePaths.insert(fname).second) {
    // 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();
}

char const* 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, char const* msg)
{
  parser->Error = msg ? msg : "unknown error";
}

void cmFortranParser_RuleUse(cmFortranParser* parser, char const* 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(parser->ModName(mod_name));
}

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

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

void cmFortranParser_RuleLineDirective(cmFortranParser* parser,
                                       char const* 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, char const* 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,
                                char const* 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(parser->ModName(mod_name));
  }
}

void cmFortranParser_RuleSubmodule(cmFortranParser* parser,
                                   char const* module_name,
                                   char const* 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(parser->ModName(mod_name));
  parser->Info.Provides.insert(parser->SModName(mod_name, sub_name));
}

void cmFortranParser_RuleSubmoduleNested(cmFortranParser* parser,
                                         char const* module_name,
                                         char const* submodule_name,
                                         char const* 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(parser->SModName(mod_name, sub_name));
  parser->Info.Provides.insert(parser->SModName(mod_name, nest_name));
}

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

void cmFortranParser_RuleUndef(cmFortranParser* parser, char const* 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, char const* 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, char const* 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--;
  }
}
