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

#include "cmConfigureTemplate.h"

#include "cmSystemTools.h"

#include <cmsys/FStream.hxx>

cmConfigureTemplate::cmConfigureTemplate()
  : WarnUninitialized(false)
  , CheckSystemVars(false)
{
  this->cmNamedCurly.compile("^[A-Za-z0-9/_.+-]+{");
  this->cmDefineRegex.compile("#cmakedefine[ \t]+([A-Za-z_0-9]*)");
  this->cmDefine01Regex.compile("#cmakedefine01[ \t]+([A-Za-z_0-9]*)");
}

cmConfigureTemplate::~cmConfigureTemplate()
{
}

void cmConfigureTemplate::SetWarnUninitialized(bool warnUninitialized)
{
  this->WarnUninitialized = warnUninitialized;
}

void cmConfigureTemplate::SetCheckSystemVars(bool checkSystemVars)
{
  this->CheckSystemVars = checkSystemVars;
}

void cmConfigureTemplate::SetOverrides(std::map<std::string, std::string> const& overrides)
{
  this->Overrides = overrides;
}

typedef enum { NORMAL, ENVIRONMENT, CACHE } t_domain;
struct t_lookup
{
  t_lookup()
    : domain(NORMAL)
    , loc(0)
  {
  }
  t_domain domain;
  size_t loc;
};

cmake::MessageType cmConfigureTemplate::ConfigureString(
  cmState::Snapshot stateSnapshot, std::string const& input,
  std::string& errorstr, std::string& output, bool escapeQuotes, bool atOnly,
  const char* filename, long line,
  std::map<std::string, const char*>* substitutions,
  std::vector<std::string>& uninitializedSubstitutions) const
{
  // Split input to handle one line at a time.
  std::string::const_iterator lineStart = input.begin();
  while (lineStart != input.end()) {
    // Find the end of this line.
    std::string::const_iterator lineEnd = lineStart;
    while (lineEnd != input.end() && *lineEnd != '\n') {
      ++lineEnd;
    }

    // Copy the line.
    std::string lineString(lineStart, lineEnd);

    // Skip the newline character.
    bool haveNewline = (lineEnd != input.end());
    if (haveNewline) {
      ++lineEnd;
    }

    // Replace #cmakedefine instances.
    if (this->cmDefineRegex.find(lineString)) {
      std::string variable = this->cmDefineRegex.match(1);
      const char* def = this->GetDefinition(stateSnapshot, variable);
      if (!cmSystemTools::IsOff(def)) {
        cmSystemTools::ReplaceString(lineString, "#cmakedefine", "#define");
        output += lineString;
      } else {
        output += "/* #undef ";
        output += this->cmDefineRegex.match(1);
        output += " */";
      }
    } else if (this->cmDefine01Regex.find(lineString)) {
      std::string variable = this->cmDefine01Regex.match(1);
      const char* def = this->GetDefinition(stateSnapshot, variable);
      cmSystemTools::ReplaceString(lineString, "#cmakedefine01", "#define");
      output += lineString;
      if (!cmSystemTools::IsOff(def)) {
        output += " 1";
      } else {
        output += " 0";
      }
    } else {
      output += lineString;
    }

    if (haveNewline) {
      output += "\n";
    }

    // Move to the next line.
    lineStart = lineEnd;
  }
  return this->ExpandVariablesInString(
    stateSnapshot, errorstr, output, escapeQuotes, true, atOnly, filename,
    line, true, true, substitutions, uninitializedSubstitutions);
}

cmake::MessageType cmConfigureTemplate::ConfigureFile(
  cmState::Snapshot stateSnapshot, std::string const& outFile,
  std::string const& inFile, bool escapeQuotes, bool atOnly,
  const cmNewLineStyle& newLine, std::string& errorstr,
  std::map<std::string, const char*>* substitutions,
  std::vector<std::string>& uninitializedSubstitutions) const
{
  std::string::size_type pos = outFile.rfind('/');
  if (pos != std::string::npos) {
    std::string path = outFile.substr(0, pos);
    cmSystemTools::MakeDirectory(path.c_str());
  }

  std::string newLineCharacters;
  std::ios::openmode omode = std::ios::out | std::ios::trunc;
  if (newLine.IsValid()) {
    newLineCharacters = newLine.GetCharacters();
    omode |= std::ios::binary;
  } else {
    newLineCharacters = "\n";
  }
  std::string tempOutputFile = outFile;
  tempOutputFile += ".tmp";
  cmsys::ofstream fout(tempOutputFile.c_str(), omode);
  if (!fout) {
    cmSystemTools::Error("Could not open file for write in copy operation ",
                         tempOutputFile.c_str());
    cmSystemTools::ReportLastSystemError("");
    return cmake::FATAL_ERROR;
  }
  cmsys::ifstream fin(inFile.c_str());
  if (!fin) {
    cmSystemTools::Error("Could not open file for read in copy operation ",
                         inFile.c_str());
    return cmake::FATAL_ERROR;
  }

  cmsys::FStream::BOM bom = cmsys::FStream::ReadBOM(fin);
  if (bom != cmsys::FStream::BOM_None && bom != cmsys::FStream::BOM_UTF8) {
    std::ostringstream e;
    e << "File starts with a Byte-Order-Mark that is not UTF-8:\n  " << inFile;
    errorstr = e.str();
    return cmake::FATAL_ERROR;
  }
  // rewind to copy BOM to output file
  fin.seekg(0);

  // now copy input to output and expand variables in the
  // input file at the same time
  std::string inLine;
  std::string outLine;

  cmake::MessageType mtype = cmake::LOG;

  mode_t perm = 0;
  cmSystemTools::GetPermissions(inFile.c_str(), perm);

  while (cmSystemTools::GetLineFromStream(fin, inLine)) {
    outLine = "";

    cmake::MessageType linemtype = this->ConfigureString(
      stateSnapshot, inLine, errorstr, outLine, escapeQuotes, atOnly,
      CM_NULLPTR, -1, substitutions, uninitializedSubstitutions);

    if (linemtype != cmake::LOG) {
      mtype = linemtype;
    }
    fout << outLine << newLineCharacters;
  }

  // close the files before attempting to copy
  fin.close();
  fout.close();
  if (!cmSystemTools::CopyFileIfDifferent(tempOutputFile.c_str(),
                                          outFile.c_str())) {
    return mtype;
  } else {
    cmSystemTools::SetPermissions(outFile.c_str(), perm);
  }
  cmSystemTools::RemoveFile(tempOutputFile);

  return mtype;
}

cmake::MessageType cmConfigureTemplate::ExpandVariablesInString(
  cmState::Snapshot stateSnapshot, std::string& errorstr, std::string& source,
  bool escapeQuotes, bool noEscapes, bool atOnly, const char* filename,
  long line, bool removeEmpty, bool replaceAt,
  std::map<std::string, const char*>* substitutions,
  std::vector<std::string>& uninitializedSubstitutions) const
{
  // This method replaces ${VAR} and @VAR@ where VAR is looked up
  // with GetDefinition(), if not found in the map, nothing is expanded.
  // It also supports the $ENV{VAR} syntax where VAR is looked up in
  // the current environment variables.

  const char* in = source.c_str();
  const char* last = in;
  std::string result;
  result.reserve(source.size());
  std::vector<t_lookup> openstack;
  bool error = false;
  bool done = false;
  cmake::MessageType mtype = cmake::LOG;

  cmState* state = stateSnapshot.GetState();

  do {
    char inc = *in;
    switch (inc) {
      case '}':
        if (!openstack.empty()) {
          t_lookup var = openstack.back();
          openstack.pop_back();
          result.append(last, in - last);
          std::string const& lookup = result.substr(var.loc);
          const char* value = CM_NULLPTR;
          std::string varresult;
          std::string svalue;
          static const std::string lineVar = "CMAKE_CURRENT_LIST_LINE";
          switch (var.domain) {
            case NORMAL:
              if (filename && lookup == lineVar) {
                std::ostringstream ostr;
                ostr << line;
                varresult = ostr.str();
              } else {
                value = this->GetDefinition(stateSnapshot, lookup);
                if (substitutions) {
                  substitutions->insert(std::make_pair(lookup, value));
                }
              }
              break;
            case ENVIRONMENT:
              if (cmSystemTools::GetEnv(lookup, svalue)) {
                value = svalue.c_str();
              }
              break;
            case CACHE:
              value = state->GetCacheEntryValue(lookup);
              break;
          }
          // Get the string we're meant to append to.
          if (value) {
            if (escapeQuotes) {
              varresult = cmSystemTools::EscapeQuotes(value);
            } else {
              varresult = value;
            }
          } else if (!removeEmpty) {
            // check to see if we need to print a warning
            // if strict mode is on and the variable has
            // not been "cleared"/initialized with a set(foo ) call
            if (this->WarnUninitialized &&
                !stateSnapshot.IsInitialized(lookup)) {
              if (this->CheckSystemVars ||
                  cmSystemTools::IsSubDirectory(filename,
                                                state->GetSourceDirectory()) ||
                  cmSystemTools::IsSubDirectory(filename,
                                                state->GetBinaryDirectory())) {
                uninitializedSubstitutions.push_back(lookup);
              }
            }
          }
          result.replace(var.loc, result.size() - var.loc, varresult);
          // Start looking from here on out.
          last = in + 1;
        }
        break;
      case '$':
        if (!atOnly) {
          t_lookup lookup;
          const char* next = in + 1;
          const char* start = CM_NULLPTR;
          char nextc = *next;
          if (nextc == '{') {
            // Looking for a variable.
            start = in + 2;
            lookup.domain = NORMAL;
          } else if (nextc == '<') {
          } else if (!nextc) {
            result.append(last, next - last);
            last = next;
          } else if (cmHasLiteralPrefix(next, "ENV{")) {
            // Looking for an environment variable.
            start = in + 5;
            lookup.domain = ENVIRONMENT;
          } else if (cmHasLiteralPrefix(next, "CACHE{")) {
            // Looking for a cache variable.
            start = in + 7;
            lookup.domain = CACHE;
          } else {
            if (this->cmNamedCurly.find(next)) {
              errorstr = "Syntax $" +
                std::string(next, this->cmNamedCurly.end()) +
                "{} is not supported.  Only ${}, $ENV{}, "
                "and $CACHE{} are allowed.";
              mtype = cmake::FATAL_ERROR;
              error = true;
            }
          }
          if (start) {
            result.append(last, in - last);
            last = start;
            in = start - 1;
            lookup.loc = result.size();
            openstack.push_back(lookup);
          }
          break;
        }
      case '\\':
        if (!noEscapes) {
          const char* next = in + 1;
          char nextc = *next;
          if (nextc == 't') {
            result.append(last, in - last);
            result.append("\t");
            last = next + 1;
          } else if (nextc == 'n') {
            result.append(last, in - last);
            result.append("\n");
            last = next + 1;
          } else if (nextc == 'r') {
            result.append(last, in - last);
            result.append("\r");
            last = next + 1;
          } else if (nextc == ';' && openstack.empty()) {
            // Handled in ExpandListArgument; pass the backslash literally.
          } else if (isalnum(nextc) || nextc == '\0') {
            errorstr += "Invalid character escape '\\";
            if (nextc) {
              errorstr += nextc;
              errorstr += "'.";
            } else {
              errorstr += "' (at end of input).";
            }
            error = true;
          } else {
            // Take what we've found so far, skipping the escape character.
            result.append(last, in - last);
            // Start tracking from the next character.
            last = in + 1;
          }
          // Skip the next character since it was escaped, but don't read past
          // the end of the string.
          if (*last) {
            ++in;
          }
        }
        break;
      case '\n':
        // Onto the next line.
        ++line;
        break;
      case '\0':
        done = true;
        break;
      case '@':
        if (replaceAt) {
          const char* nextAt = strchr(in + 1, '@');
          if (nextAt && nextAt != in + 1 &&
              nextAt ==
                in + 1 + strspn(in + 1, "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
                                        "abcdefghijklmnopqrstuvwxyz"
                                        "0123456789/_.+-")) {
            std::string variable(in + 1, nextAt - in - 1);
            std::string varresult;
            const char* def = this->GetDefinition(stateSnapshot, variable);
            if (substitutions) {
              substitutions->insert(std::make_pair(variable, def));
            }
            if (def) {
              varresult = def;
            }
            if (escapeQuotes) {
              varresult = cmSystemTools::EscapeQuotes(varresult);
            }
            // Skip over the variable.
            result.append(last, in - last);
            result.append(varresult);
            in = nextAt;
            last = in + 1;
            break;
          }
        }
      // Failed to find a valid @ expansion; treat it as literal.
      /* FALLTHROUGH */
      default: {
        if (!openstack.empty() &&
            !(isalnum(inc) || inc == '_' || inc == '/' || inc == '.' ||
              inc == '+' || inc == '-')) {
          errorstr += "Invalid character (\'";
          errorstr += inc;
          result.append(last, in - last);
          errorstr += "\') in a variable name: "
                      "'" +
            result.substr(openstack.back().loc) + "'";
          mtype = cmake::FATAL_ERROR;
          error = true;
        }
        break;
      }
    }
    // Look at the next character.
  } while (!error && !done && *++in);

  // Check for open variable references yet.
  if (!error && !openstack.empty()) {
    // There's an open variable reference waiting.  Policy CMP0010 flags
    // whether this is an error or not.  The new parser now enforces
    // CMP0010 as well.
    errorstr += "There is an unterminated variable reference.";
    error = true;
  }

  if (error) {
    std::ostringstream emsg;
    emsg << "Syntax error in cmake code ";
    if (filename) {
      // This filename and line number may be more specific than the
      // command context because one command invocation can have
      // arguments on multiple lines.
      emsg << "at\n"
           << "  " << filename << ":" << line << "\n";
    }
    emsg << "when parsing string\n"
         << "  " << source << "\n";
    emsg << errorstr;
    mtype = cmake::FATAL_ERROR;
    errorstr = emsg.str();
  } else {
    // Append the rest of the unchanged part of the string.
    result.append(last);

    source = result;
  }

  return mtype;
}

const char* cmConfigureTemplate::GetDefinition(cmState::Snapshot stateSnapshot,
                                               std::string const& name) const
{
  std::map<std::string, std::string>::const_iterator overrideIt = this->Overrides.find(name);
  if (overrideIt != this->Overrides.end()) {
    return overrideIt->second.c_str();
  }
  const char* def = stateSnapshot.GetDefinition(name);
  if (!def) {
    def = stateSnapshot.GetState()->GetInitializedCacheValue(name);
  }
  return def;
}
