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

#include "cmsys/FStream.hxx"

#include "cmAlgorithms.h"
#include "cmGlobalGenerator.h"
#include "cmMakefile.h"
#include "cmStateDirectory.h"
#include "cmStateSnapshot.h"
#include "cmSystemTools.h"
#include "cmake.h"

#include <algorithm>

// -- Class methods

void cmQtAutoGenerator::Logger::SetVerbose(bool value)
{
  Verbose_ = value;
}

void cmQtAutoGenerator::Logger::SetColorOutput(bool value)
{
  ColorOutput_ = value;
}

std::string cmQtAutoGenerator::Logger::HeadLine(std::string const& title)
{
  std::string head = title;
  head += '\n';
  head.append(head.size() - 1, '-');
  head += '\n';
  return head;
}

void cmQtAutoGenerator::Logger::Info(GeneratorT genType,
                                     std::string const& message)
{
  std::string msg = GeneratorName(genType);
  msg += ": ";
  msg += message;
  if (msg.back() != '\n') {
    msg.push_back('\n');
  }
  {
    std::lock_guard<std::mutex> lock(Mutex_);
    cmSystemTools::Stdout(msg.c_str(), msg.size());
  }
}

void cmQtAutoGenerator::Logger::Warning(GeneratorT genType,
                                        std::string const& message)
{
  std::string msg;
  if (message.find('\n') == std::string::npos) {
    // Single line message
    msg += GeneratorName(genType);
    msg += " warning: ";
  } else {
    // Multi line message
    msg += HeadLine(GeneratorName(genType) + " warning");
  }
  // Message
  msg += message;
  if (msg.back() != '\n') {
    msg.push_back('\n');
  }
  msg.push_back('\n');
  {
    std::lock_guard<std::mutex> lock(Mutex_);
    cmSystemTools::Stdout(msg.c_str(), msg.size());
  }
}

void cmQtAutoGenerator::Logger::WarningFile(GeneratorT genType,
                                            std::string const& filename,
                                            std::string const& message)
{
  std::string msg = "  ";
  msg += Quoted(filename);
  msg.push_back('\n');
  // Message
  msg += message;
  Warning(genType, msg);
}

void cmQtAutoGenerator::Logger::Error(GeneratorT genType,
                                      std::string const& message)
{
  std::string msg;
  msg += HeadLine(GeneratorName(genType) + " error");
  // Message
  msg += message;
  if (msg.back() != '\n') {
    msg.push_back('\n');
  }
  msg.push_back('\n');
  {
    std::lock_guard<std::mutex> lock(Mutex_);
    cmSystemTools::Stderr(msg.c_str(), msg.size());
  }
}

void cmQtAutoGenerator::Logger::ErrorFile(GeneratorT genType,
                                          std::string const& filename,
                                          std::string const& message)
{
  std::string emsg = "  ";
  emsg += Quoted(filename);
  emsg += '\n';
  // Message
  emsg += message;
  Error(genType, emsg);
}

void cmQtAutoGenerator::Logger::ErrorCommand(
  GeneratorT genType, std::string const& message,
  std::vector<std::string> const& command, std::string const& output)
{
  std::string msg;
  msg.push_back('\n');
  msg += HeadLine(GeneratorName(genType) + " subprocess error");
  msg += message;
  if (msg.back() != '\n') {
    msg.push_back('\n');
  }
  msg.push_back('\n');
  msg += HeadLine("Command");
  msg += QuotedCommand(command);
  if (msg.back() != '\n') {
    msg.push_back('\n');
  }
  msg.push_back('\n');
  msg += HeadLine("Output");
  msg += output;
  if (msg.back() != '\n') {
    msg.push_back('\n');
  }
  msg.push_back('\n');
  {
    std::lock_guard<std::mutex> lock(Mutex_);
    cmSystemTools::Stderr(msg.c_str(), msg.size());
  }
}

std::string cmQtAutoGenerator::FileSystem::GetRealPath(
  std::string const& filename)
{
  std::lock_guard<std::mutex> lock(Mutex_);
  return cmSystemTools::GetRealPath(filename);
}

std::string cmQtAutoGenerator::FileSystem::CollapseCombinedPath(
  std::string const& dir, std::string const& file)
{
  std::lock_guard<std::mutex> lock(Mutex_);
  return cmSystemTools::CollapseCombinedPath(dir, file);
}

void cmQtAutoGenerator::FileSystem::SplitPath(
  const std::string& p, std::vector<std::string>& components,
  bool expand_home_dir)
{
  std::lock_guard<std::mutex> lock(Mutex_);
  cmSystemTools::SplitPath(p, components, expand_home_dir);
}

std::string cmQtAutoGenerator::FileSystem::JoinPath(
  const std::vector<std::string>& components)
{
  std::lock_guard<std::mutex> lock(Mutex_);
  return cmSystemTools::JoinPath(components);
}

std::string cmQtAutoGenerator::FileSystem::JoinPath(
  std::vector<std::string>::const_iterator first,
  std::vector<std::string>::const_iterator last)
{
  std::lock_guard<std::mutex> lock(Mutex_);
  return cmSystemTools::JoinPath(first, last);
}

std::string cmQtAutoGenerator::FileSystem::GetFilenameWithoutLastExtension(
  const std::string& filename)
{
  std::lock_guard<std::mutex> lock(Mutex_);
  return cmSystemTools::GetFilenameWithoutLastExtension(filename);
}

std::string cmQtAutoGenerator::FileSystem::SubDirPrefix(
  std::string const& filename)
{
  std::lock_guard<std::mutex> lock(Mutex_);
  return cmQtAutoGen::SubDirPrefix(filename);
}

void cmQtAutoGenerator::FileSystem::setupFilePathChecksum(
  std::string const& currentSrcDir, std::string const& currentBinDir,
  std::string const& projectSrcDir, std::string const& projectBinDir)
{
  std::lock_guard<std::mutex> lock(Mutex_);
  FilePathChecksum_.setupParentDirs(currentSrcDir, currentBinDir,
                                    projectSrcDir, projectBinDir);
}

std::string cmQtAutoGenerator::FileSystem::GetFilePathChecksum(
  std::string const& filename)
{
  std::lock_guard<std::mutex> lock(Mutex_);
  return FilePathChecksum_.getPart(filename);
}

bool cmQtAutoGenerator::FileSystem::FileExists(std::string const& filename)
{
  std::lock_guard<std::mutex> lock(Mutex_);
  return cmSystemTools::FileExists(filename);
}

bool cmQtAutoGenerator::FileSystem::FileExists(std::string const& filename,
                                               bool isFile)
{
  std::lock_guard<std::mutex> lock(Mutex_);
  return cmSystemTools::FileExists(filename, isFile);
}

unsigned long cmQtAutoGenerator::FileSystem::FileLength(
  std::string const& filename)
{
  std::lock_guard<std::mutex> lock(Mutex_);
  return cmSystemTools::FileLength(filename);
}

bool cmQtAutoGenerator::FileSystem::FileIsOlderThan(
  std::string const& buildFile, std::string const& sourceFile,
  std::string* error)
{
  bool res(false);
  int result = 0;
  {
    std::lock_guard<std::mutex> lock(Mutex_);
    res = cmSystemTools::FileTimeCompare(buildFile, sourceFile, &result);
  }
  if (res) {
    res = (result < 0);
  } else {
    if (error != nullptr) {
      error->append(
        "File modification time comparison failed for the files\n  ");
      error->append(Quoted(buildFile));
      error->append("\nand\n  ");
      error->append(Quoted(sourceFile));
    }
  }
  return res;
}

bool cmQtAutoGenerator::FileSystem::FileRead(std::string& content,
                                             std::string const& filename,
                                             std::string* error)
{
  bool success = false;
  if (FileExists(filename, true)) {
    unsigned long const length = FileLength(filename);
    {
      std::lock_guard<std::mutex> lock(Mutex_);
      cmsys::ifstream ifs(filename.c_str(), (std::ios::in | std::ios::binary));
      if (ifs) {
        content.reserve(length);
        content.assign(std::istreambuf_iterator<char>{ ifs },
                       std::istreambuf_iterator<char>{});
        if (ifs) {
          success = true;
        } else {
          content.clear();
          if (error != nullptr) {
            error->append("Reading from the file failed.");
          }
        }
      } else if (error != nullptr) {
        error->append("Opening the file for reading failed.");
      }
    }
  } else if (error != nullptr) {
    error->append(
      "The file does not exist, is not readable or is a directory.");
  }
  return success;
}

bool cmQtAutoGenerator::FileSystem::FileRead(GeneratorT genType,
                                             std::string& content,
                                             std::string const& filename)
{
  std::string error;
  if (!FileRead(content, filename, &error)) {
    Log()->ErrorFile(genType, filename, error);
    return false;
  }
  return true;
}

bool cmQtAutoGenerator::FileSystem::FileWrite(std::string const& filename,
                                              std::string const& content,
                                              std::string* error)
{
  bool success = false;
  // Make sure the parent directory exists
  if (MakeParentDirectory(filename)) {
    std::lock_guard<std::mutex> lock(Mutex_);
    cmsys::ofstream outfile;
    outfile.open(filename.c_str(),
                 (std::ios::out | std::ios::binary | std::ios::trunc));
    if (outfile) {
      outfile << content;
      // Check for write errors
      if (outfile.good()) {
        success = true;
      } else {
        if (error != nullptr) {
          error->assign("File writing failed");
        }
      }
    } else {
      if (error != nullptr) {
        error->assign("Opening file for writing failed");
      }
    }
  } else {
    if (error != nullptr) {
      error->assign("Could not create parent directory");
    }
  }
  return success;
}

bool cmQtAutoGenerator::FileSystem::FileWrite(GeneratorT genType,
                                              std::string const& filename,
                                              std::string const& content)
{
  std::string error;
  if (!FileWrite(filename, content, &error)) {
    Log()->ErrorFile(genType, filename, error);
    return false;
  }
  return true;
}

bool cmQtAutoGenerator::FileSystem::FileDiffers(std::string const& filename,
                                                std::string const& content)
{
  bool differs = true;
  {
    std::string oldContents;
    if (FileRead(oldContents, filename)) {
      differs = (oldContents != content);
    }
  }
  return differs;
}

bool cmQtAutoGenerator::FileSystem::FileRemove(std::string const& filename)
{
  std::lock_guard<std::mutex> lock(Mutex_);
  return cmSystemTools::RemoveFile(filename);
}

bool cmQtAutoGenerator::FileSystem::Touch(std::string const& filename)
{
  std::lock_guard<std::mutex> lock(Mutex_);
  return cmSystemTools::Touch(filename, false);
}

bool cmQtAutoGenerator::FileSystem::MakeDirectory(std::string const& dirname)
{
  std::lock_guard<std::mutex> lock(Mutex_);
  return cmSystemTools::MakeDirectory(dirname);
}

bool cmQtAutoGenerator::FileSystem::MakeDirectory(GeneratorT genType,
                                                  std::string const& dirname)
{
  if (!MakeDirectory(dirname)) {
    Log()->ErrorFile(genType, dirname, "Could not create directory");
    return false;
  }
  return true;
}

bool cmQtAutoGenerator::FileSystem::MakeParentDirectory(
  std::string const& filename)
{
  bool success = true;
  std::string const dirName = cmSystemTools::GetFilenamePath(filename);
  if (!dirName.empty()) {
    success = MakeDirectory(dirName);
  }
  return success;
}

bool cmQtAutoGenerator::FileSystem::MakeParentDirectory(
  GeneratorT genType, std::string const& filename)
{
  if (!MakeParentDirectory(filename)) {
    Log()->ErrorFile(genType, filename, "Could not create parent directory");
    return false;
  }
  return true;
}

int cmQtAutoGenerator::ReadOnlyProcessT::PipeT::init(uv_loop_t* uv_loop,
                                                     ReadOnlyProcessT* process)
{
  Process_ = process;
  Target_ = nullptr;
  return UVPipe_.init(*uv_loop, 0, this);
}

int cmQtAutoGenerator::ReadOnlyProcessT::PipeT::startRead(std::string* target)
{
  Target_ = target;
  return uv_read_start(uv_stream(), &PipeT::UVAlloc, &PipeT::UVData);
}

void cmQtAutoGenerator::ReadOnlyProcessT::PipeT::reset()
{
  Process_ = nullptr;
  Target_ = nullptr;
  UVPipe_.reset();
  Buffer_.clear();
  Buffer_.shrink_to_fit();
}

void cmQtAutoGenerator::ReadOnlyProcessT::PipeT::UVAlloc(uv_handle_t* handle,
                                                         size_t suggestedSize,
                                                         uv_buf_t* buf)
{
  auto& pipe = *reinterpret_cast<PipeT*>(handle->data);
  pipe.Buffer_.resize(suggestedSize);
  buf->base = &pipe.Buffer_.front();
  buf->len = pipe.Buffer_.size();
}

void cmQtAutoGenerator::ReadOnlyProcessT::PipeT::UVData(uv_stream_t* stream,
                                                        ssize_t nread,
                                                        const uv_buf_t* buf)
{
  auto& pipe = *reinterpret_cast<PipeT*>(stream->data);
  if (nread > 0) {
    // Append data to merged output
    if ((buf->base != nullptr) && (pipe.Target_ != nullptr)) {
      pipe.Target_->append(buf->base, nread);
    }
  } else if (nread < 0) {
    // EOF or error
    auto* proc = pipe.Process_;
    // Check it this an unusual error
    if (nread != UV_EOF) {
      if (!proc->Result()->error()) {
        proc->Result()->ErrorMessage =
          "libuv reading from pipe failed with error code ";
        proc->Result()->ErrorMessage += std::to_string(nread);
      }
    }
    // Clear libuv pipe handle and try to finish
    pipe.reset();
    proc->UVTryFinish();
  }
}

void cmQtAutoGenerator::ProcessResultT::reset()
{
  ExitStatus = 0;
  TermSignal = 0;
  if (!StdOut.empty()) {
    StdOut.clear();
    StdOut.shrink_to_fit();
  }
  if (!StdErr.empty()) {
    StdErr.clear();
    StdErr.shrink_to_fit();
  }
  if (!ErrorMessage.empty()) {
    ErrorMessage.clear();
    ErrorMessage.shrink_to_fit();
  }
}

void cmQtAutoGenerator::ReadOnlyProcessT::setup(
  ProcessResultT* result, bool mergedOutput,
  std::vector<std::string> const& command, std::string const& workingDirectory)
{
  Setup_.WorkingDirectory = workingDirectory;
  Setup_.Command = command;
  Setup_.Result = result;
  Setup_.MergedOutput = mergedOutput;
}

bool cmQtAutoGenerator::ReadOnlyProcessT::start(
  uv_loop_t* uv_loop, std::function<void()>&& finishedCallback)
{
  if (IsStarted() || (Result() == nullptr)) {
    return false;
  }

  // Reset result before the start
  Result()->reset();

  // Fill command string pointers
  if (!Setup().Command.empty()) {
    CommandPtr_.reserve(Setup().Command.size() + 1);
    for (std::string const& arg : Setup().Command) {
      CommandPtr_.push_back(arg.c_str());
    }
    CommandPtr_.push_back(nullptr);
  } else {
    Result()->ErrorMessage = "Empty command";
  }

  if (!Result()->error()) {
    if (UVPipeOut_.init(uv_loop, this) != 0) {
      Result()->ErrorMessage = "libuv stdout pipe initialization failed";
    }
  }
  if (!Result()->error()) {
    if (UVPipeErr_.init(uv_loop, this) != 0) {
      Result()->ErrorMessage = "libuv stderr pipe initialization failed";
    }
  }
  if (!Result()->error()) {
    // -- Setup process stdio options
    // stdin
    UVOptionsStdIO_[0].flags = UV_IGNORE;
    UVOptionsStdIO_[0].data.stream = nullptr;
    // stdout
    UVOptionsStdIO_[1].flags =
      static_cast<uv_stdio_flags>(UV_CREATE_PIPE | UV_WRITABLE_PIPE);
    UVOptionsStdIO_[1].data.stream = UVPipeOut_.uv_stream();
    // stderr
    UVOptionsStdIO_[2].flags =
      static_cast<uv_stdio_flags>(UV_CREATE_PIPE | UV_WRITABLE_PIPE);
    UVOptionsStdIO_[2].data.stream = UVPipeErr_.uv_stream();

    // -- Setup process options
    std::fill_n(reinterpret_cast<char*>(&UVOptions_), sizeof(UVOptions_), 0);
    UVOptions_.exit_cb = &ReadOnlyProcessT::UVExit;
    UVOptions_.file = CommandPtr_[0];
    UVOptions_.args = const_cast<char**>(&CommandPtr_.front());
    UVOptions_.cwd = Setup_.WorkingDirectory.c_str();
    UVOptions_.flags = UV_PROCESS_WINDOWS_HIDE;
    UVOptions_.stdio_count = static_cast<int>(UVOptionsStdIO_.size());
    UVOptions_.stdio = &UVOptionsStdIO_.front();

    // -- Spawn process
    if (UVProcess_.spawn(*uv_loop, UVOptions_, this) != 0) {
      Result()->ErrorMessage = "libuv process spawn failed";
    }
  }
  // -- Start reading from stdio streams
  if (!Result()->error()) {
    if (UVPipeOut_.startRead(&Result()->StdOut) != 0) {
      Result()->ErrorMessage = "libuv start reading from stdout pipe failed";
    }
  }
  if (!Result()->error()) {
    if (UVPipeErr_.startRead(Setup_.MergedOutput ? &Result()->StdOut
                                                 : &Result()->StdErr) != 0) {
      Result()->ErrorMessage = "libuv start reading from stderr pipe failed";
    }
  }

  if (!Result()->error()) {
    IsStarted_ = true;
    FinishedCallback_ = std::move(finishedCallback);
  } else {
    // Clear libuv handles and finish
    UVProcess_.reset();
    UVPipeOut_.reset();
    UVPipeErr_.reset();
    CommandPtr_.clear();
  }

  return IsStarted();
}

void cmQtAutoGenerator::ReadOnlyProcessT::UVExit(uv_process_t* handle,
                                                 int64_t exitStatus,
                                                 int termSignal)
{
  auto& proc = *reinterpret_cast<ReadOnlyProcessT*>(handle->data);
  if (proc.IsStarted() && !proc.IsFinished()) {
    // Set error message on demand
    proc.Result()->ExitStatus = exitStatus;
    proc.Result()->TermSignal = termSignal;
    if (!proc.Result()->error()) {
      if (termSignal != 0) {
        proc.Result()->ErrorMessage = "Process was terminated by signal ";
        proc.Result()->ErrorMessage +=
          std::to_string(proc.Result()->TermSignal);
      } else if (exitStatus != 0) {
        proc.Result()->ErrorMessage = "Process failed with return value ";
        proc.Result()->ErrorMessage +=
          std::to_string(proc.Result()->ExitStatus);
      }
    }

    // Reset process handle and try to finish
    proc.UVProcess_.reset();
    proc.UVTryFinish();
  }
}

void cmQtAutoGenerator::ReadOnlyProcessT::UVTryFinish()
{
  // There still might be data in the pipes after the process has finished.
  // Therefore check if the process is finished AND all pipes are closed
  // before signaling the worker thread to continue.
  if (UVProcess_.get() == nullptr) {
    if (UVPipeOut_.uv_pipe() == nullptr) {
      if (UVPipeErr_.uv_pipe() == nullptr) {
        IsFinished_ = true;
        FinishedCallback_();
      }
    }
  }
}

cmQtAutoGenerator::cmQtAutoGenerator()
  : FileSys_(&Logger_)
{
  // Initialize logger
  Logger_.SetVerbose(cmSystemTools::HasEnv("VERBOSE"));
  {
    std::string colorEnv;
    cmSystemTools::GetEnv("COLOR", colorEnv);
    if (!colorEnv.empty()) {
      Logger_.SetColorOutput(cmSystemTools::IsOn(colorEnv.c_str()));
    } else {
      Logger_.SetColorOutput(true);
    }
  }

  // Initialize libuv loop
  uv_disable_stdio_inheritance();
#ifdef CMAKE_UV_SIGNAL_HACK
  UVHackRAII_ = cm::make_unique<cmUVSignalHackRAII>();
#endif
  UVLoop_ = cm::make_unique<uv_loop_t>();
  uv_loop_init(UVLoop());
}

cmQtAutoGenerator::~cmQtAutoGenerator()
{
  // Close libuv loop
  uv_loop_close(UVLoop());
}

bool cmQtAutoGenerator::Run(std::string const& infoFile,
                            std::string const& config)
{
  // Info settings
  InfoFile_ = infoFile;
  cmSystemTools::ConvertToUnixSlashes(InfoFile_);
  InfoDir_ = cmSystemTools::GetFilenamePath(infoFile);
  InfoConfig_ = config;

  bool success = false;
  {
    cmake cm(cmake::RoleScript);
    cm.SetHomeOutputDirectory(InfoDir());
    cm.SetHomeDirectory(InfoDir());
    cm.GetCurrentSnapshot().SetDefaultDefinitions();
    cmGlobalGenerator gg(&cm);

    cmStateSnapshot snapshot = cm.GetCurrentSnapshot();
    snapshot.GetDirectory().SetCurrentBinary(InfoDir());
    snapshot.GetDirectory().SetCurrentSource(InfoDir());

    auto makefile = cm::make_unique<cmMakefile>(&gg, snapshot);
    // The OLD/WARN behavior for policy CMP0053 caused a speed regression.
    // https://gitlab.kitware.com/cmake/cmake/issues/17570
    makefile->SetPolicyVersion("3.9", std::string());
    gg.SetCurrentMakefile(makefile.get());
    success = this->Init(makefile.get());
  }
  if (success) {
    success = this->Process();
  }
  return success;
}

std::string cmQtAutoGenerator::SettingsFind(std::string const& content,
                                            const char* key)
{
  std::string prefix(key);
  prefix += ':';
  std::string::size_type pos = content.find(prefix);
  if (pos != std::string::npos) {
    pos += prefix.size();
    if (pos < content.size()) {
      std::string::size_type posE = content.find('\n', pos);
      if ((posE != std::string::npos) && (posE != pos)) {
        return content.substr(pos, posE - pos);
      }
    }
  }
  return std::string();
}
