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

#include "cmGeneratedFileStream.h"
#include "cmGeneratorTarget.h"
#include "cmGlobalGenerator.h"
#include "cmLocalGenerator.h"
#include "cmMakefile.h"
#include "cmSourceFile.h"
#include "cmStateTypes.h"
#include "cmSystemTools.h"
#include "cmTarget.h"
#include "cmXMLWriter.h"
#include "cmake.h"

#include "cmsys/Directory.hxx"
#include "cmsys/FStream.hxx"
#include <set>
#include <string.h>
#include <utility>

cmGlobalKdevelopGenerator::cmGlobalKdevelopGenerator()
  : cmExternalMakefileProjectGenerator()
{
}

cmExternalMakefileProjectGeneratorFactory*
cmGlobalKdevelopGenerator::GetFactory()
{
  static cmExternalMakefileProjectGeneratorSimpleFactory<
    cmGlobalKdevelopGenerator>
    factory("KDevelop3", "Generates KDevelop 3 project files.");

  if (factory.GetSupportedGlobalGenerators().empty()) {
    factory.AddSupportedGlobalGenerator("Unix Makefiles");
#ifdef CMAKE_USE_NINJA
    factory.AddSupportedGlobalGenerator("Ninja");
#endif

    factory.Aliases.push_back("KDevelop3");
  }

  return &factory;
}

void cmGlobalKdevelopGenerator::Generate()
{
  // for each sub project in the project create
  // a kdevelop project
  for (auto const& it : this->GlobalGenerator->GetProjectMap()) {
    std::string outputDir = it.second[0]->GetCurrentBinaryDirectory();
    std::string projectDir = it.second[0]->GetSourceDirectory();
    std::string projectName = it.second[0]->GetProjectName();
    std::string cmakeFilePattern("CMakeLists.txt;*.cmake;");
    std::string fileToOpen;
    const std::vector<cmLocalGenerator*>& lgs = it.second;
    // create the project.kdevelop.filelist file
    if (!this->CreateFilelistFile(lgs, outputDir, projectDir, projectName,
                                  cmakeFilePattern, fileToOpen)) {
      cmSystemTools::Error("Can not create filelist file");
      return;
    }
    // try to find the name of an executable so we have something to
    // run from kdevelop for now just pick the first executable found
    std::string executable;
    for (cmLocalGenerator* lg : lgs) {
      std::vector<cmGeneratorTarget*> const& targets =
        lg->GetGeneratorTargets();
      for (cmGeneratorTarget* target : targets) {
        if (target->GetType() == cmStateEnums::EXECUTABLE) {
          executable = target->GetLocation("");
          break;
        }
      }
      if (!executable.empty()) {
        break;
      }
    }

    // now create a project file
    this->CreateProjectFile(outputDir, projectDir, projectName, executable,
                            cmakeFilePattern, fileToOpen);
  }
}

bool cmGlobalKdevelopGenerator::CreateFilelistFile(
  const std::vector<cmLocalGenerator*>& lgs, const std::string& outputDir,
  const std::string& projectDirIn, const std::string& projectname,
  std::string& cmakeFilePattern, std::string& fileToOpen)
{
  std::string projectDir = projectDirIn + "/";
  std::string filename = outputDir + "/" + projectname + ".kdevelop.filelist";

  std::set<std::string> files;
  std::string tmp;

  std::vector<std::string> const& hdrExts =
    this->GlobalGenerator->GetCMakeInstance()->GetHeaderExtensions();

  for (cmLocalGenerator* lg : lgs) {
    cmMakefile* makefile = lg->GetMakefile();
    const std::vector<std::string>& listFiles = makefile->GetListFiles();
    for (std::string const& listFile : listFiles) {
      tmp = listFile;
      cmSystemTools::ReplaceString(tmp, projectDir.c_str(), "");
      // make sure the file is part of this source tree
      if ((tmp[0] != '/') &&
          (strstr(tmp.c_str(), cmake::GetCMakeFilesDirectoryPostSlash()) ==
           nullptr)) {
        files.insert(tmp);
        tmp = cmSystemTools::GetFilenameName(tmp);
        // add all files which don't match the default
        // */CMakeLists.txt;*cmake; to the file pattern
        if ((tmp != "CMakeLists.txt") &&
            (strstr(tmp.c_str(), ".cmake") == nullptr)) {
          cmakeFilePattern += tmp + ";";
        }
      }
    }

    // get all sources
    const std::vector<cmGeneratorTarget*>& targets = lg->GetGeneratorTargets();
    for (cmGeneratorTarget* gt : targets) {
      std::vector<cmSourceFile*> sources;
      gt->GetSourceFiles(sources, gt->Target->GetMakefile()->GetSafeDefinition(
                                    "CMAKE_BUILD_TYPE"));
      for (cmSourceFile* sf : sources) {
        tmp = sf->GetFullPath();
        std::string headerBasename = cmSystemTools::GetFilenamePath(tmp);
        headerBasename += "/";
        headerBasename += cmSystemTools::GetFilenameWithoutExtension(tmp);

        cmSystemTools::ReplaceString(tmp, projectDir.c_str(), "");

        if ((tmp[0] != '/') &&
            (strstr(tmp.c_str(), cmake::GetCMakeFilesDirectoryPostSlash()) ==
             nullptr) &&
            (cmSystemTools::GetFilenameExtension(tmp) != ".moc")) {
          files.insert(tmp);

          // check if there's a matching header around
          for (std::string const& hdrExt : hdrExts) {
            std::string hname = headerBasename;
            hname += ".";
            hname += hdrExt;
            if (cmSystemTools::FileExists(hname.c_str())) {
              cmSystemTools::ReplaceString(hname, projectDir.c_str(), "");
              files.insert(hname);
              break;
            }
          }
        }
      }
      for (std::string const& listFile : listFiles) {
        tmp = listFile;
        cmSystemTools::ReplaceString(tmp, projectDir.c_str(), "");
        if ((tmp[0] != '/') &&
            (strstr(tmp.c_str(), cmake::GetCMakeFilesDirectoryPostSlash()) ==
             nullptr)) {
          files.insert(tmp);
        }
      }
    }
  }

  // check if the output file already exists and read it
  // insert all files which exist into the set of files
  cmsys::ifstream oldFilelist(filename.c_str());
  if (oldFilelist) {
    while (cmSystemTools::GetLineFromStream(oldFilelist, tmp)) {
      if (tmp[0] == '/') {
        continue;
      }
      std::string completePath = projectDir + tmp;
      if (cmSystemTools::FileExists(completePath.c_str())) {
        files.insert(tmp);
      }
    }
    oldFilelist.close();
  }

  // now write the new filename
  cmGeneratedFileStream fout(filename.c_str());
  if (!fout) {
    return false;
  }

  fileToOpen = "";
  for (std::string const& file : files) {
    // get the full path to the file
    tmp = cmSystemTools::CollapseFullPath(file, projectDir.c_str());
    // just select the first source file
    if (fileToOpen.empty()) {
      std::string ext = cmSystemTools::GetFilenameExtension(tmp);
      if ((ext == ".c") || (ext == ".cc") || (ext == ".cpp") ||
          (ext == ".cxx") || (ext == ".C") || (ext == ".h") ||
          (ext == ".hpp")) {
        fileToOpen = tmp;
      }
    }
    // make it relative to the project dir
    cmSystemTools::ReplaceString(tmp, projectDir.c_str(), "");
    // only put relative paths
    if (!tmp.empty() && tmp[0] != '/') {
      fout << tmp << "\n";
    }
  }
  return true;
}

/* create the project file, if it already exists, merge it with the
existing one, otherwise create a new one */
void cmGlobalKdevelopGenerator::CreateProjectFile(
  const std::string& outputDir, const std::string& projectDir,
  const std::string& projectname, const std::string& executable,
  const std::string& cmakeFilePattern, const std::string& fileToOpen)
{
  this->Blacklist.clear();

  std::string filename = outputDir + "/";
  filename += projectname + ".kdevelop";
  std::string sessionFilename = outputDir + "/";
  sessionFilename += projectname + ".kdevses";

  if (cmSystemTools::FileExists(filename.c_str())) {
    this->MergeProjectFiles(outputDir, projectDir, filename, executable,
                            cmakeFilePattern, fileToOpen, sessionFilename);
  } else {
    // add all subdirectories which are cmake build directories to the
    // kdevelop blacklist so they are not monitored for added or removed files
    // since this is handled by adding files to the cmake files
    cmsys::Directory d;
    if (d.Load(projectDir)) {
      size_t numf = d.GetNumberOfFiles();
      for (unsigned int i = 0; i < numf; i++) {
        std::string nextFile = d.GetFile(i);
        if ((nextFile != ".") && (nextFile != "..")) {
          std::string tmp = projectDir;
          tmp += "/";
          tmp += nextFile;
          if (cmSystemTools::FileIsDirectory(tmp)) {
            tmp += "/CMakeCache.txt";
            if ((nextFile == "CMakeFiles") ||
                (cmSystemTools::FileExists(tmp.c_str()))) {
              this->Blacklist.push_back(nextFile);
            }
          }
        }
      }
    }
    this->CreateNewProjectFile(outputDir, projectDir, filename, executable,
                               cmakeFilePattern, fileToOpen, sessionFilename);
  }
}

void cmGlobalKdevelopGenerator::MergeProjectFiles(
  const std::string& outputDir, const std::string& projectDir,
  const std::string& filename, const std::string& executable,
  const std::string& cmakeFilePattern, const std::string& fileToOpen,
  const std::string& sessionFilename)
{
  cmsys::ifstream oldProjectFile(filename.c_str());
  if (!oldProjectFile) {
    this->CreateNewProjectFile(outputDir, projectDir, filename, executable,
                               cmakeFilePattern, fileToOpen, sessionFilename);
    return;
  }

  /* Read the existing project file (line by line), copy all lines
    into the new project file, except the ones which can be reliably
    set from contents of the CMakeLists.txt */
  std::string tmp;
  std::vector<std::string> lines;
  while (cmSystemTools::GetLineFromStream(oldProjectFile, tmp)) {
    lines.push_back(tmp);
  }
  oldProjectFile.close();

  cmGeneratedFileStream fout(filename.c_str());
  if (!fout) {
    return;
  }

  for (std::string const& l : lines) {
    const char* line = l.c_str();
    // skip these tags as they are always replaced
    if ((strstr(line, "<projectdirectory>") != nullptr) ||
        (strstr(line, "<projectmanagement>") != nullptr) ||
        (strstr(line, "<absoluteprojectpath>") != nullptr) ||
        (strstr(line, "<filelistdirectory>") != nullptr) ||
        (strstr(line, "<buildtool>") != nullptr) ||
        (strstr(line, "<builddir>") != nullptr)) {
      continue;
    }

    // output the line from the file if it is not one of the above tags
    fout << l << "\n";
    // if this is the <general> tag output the stuff that goes in the
    // general tag
    if (strstr(line, "<general>")) {
      fout << "  <projectmanagement>KDevCustomProject</projectmanagement>\n";
      fout << "  <projectdirectory>" << projectDir
           << "</projectdirectory>\n"; // this one is important
      fout << "  <absoluteprojectpath>true</absoluteprojectpath>\n";
      // and this one
    }
    // inside kdevcustomproject the <filelistdirectory> must be put
    if (strstr(line, "<kdevcustomproject>")) {
      fout << "    <filelistdirectory>" << outputDir
           << "</filelistdirectory>\n";
    }
    // buildtool and builddir go inside <build>
    if (strstr(line, "<build>")) {
      fout << "      <buildtool>make</buildtool>\n";
      fout << "      <builddir>" << outputDir << "</builddir>\n";
    }
  }
}

void cmGlobalKdevelopGenerator::CreateNewProjectFile(
  const std::string& outputDir, const std::string& projectDir,
  const std::string& filename, const std::string& executable,
  const std::string& cmakeFilePattern, const std::string& fileToOpen,
  const std::string& sessionFilename)
{
  cmGeneratedFileStream fout(filename.c_str());
  if (!fout) {
    return;
  }
  cmXMLWriter xml(fout);

  // check for a version control system
  bool hasSvn = cmSystemTools::FileExists((projectDir + "/.svn").c_str());
  bool hasCvs = cmSystemTools::FileExists((projectDir + "/CVS").c_str());

  bool enableCxx = (this->GlobalGenerator->GetLanguageEnabled("C") ||
                    this->GlobalGenerator->GetLanguageEnabled("CXX"));
  bool enableFortran = this->GlobalGenerator->GetLanguageEnabled("Fortran");
  std::string primaryLanguage = "C++";
  if (enableFortran && !enableCxx) {
    primaryLanguage = "Fortran77";
  }

  xml.StartDocument();
  xml.StartElement("kdevelop");
  xml.StartElement("general");

  xml.Element("author", "");
  xml.Element("email", "");
  xml.Element("version", "$VERSION$");
  xml.Element("projectmanagement", "KDevCustomProject");
  xml.Element("primarylanguage", primaryLanguage);
  xml.Element("ignoreparts");
  xml.Element("projectdirectory", projectDir); // this one is important
  xml.Element("absoluteprojectpath", "true");  // and this one

  // setup additional languages
  xml.StartElement("secondaryLanguages");
  if (enableFortran && enableCxx) {
    xml.Element("language", "Fortran");
  }
  if (enableCxx) {
    xml.Element("language", "C");
  }
  xml.EndElement();

  if (hasSvn) {
    xml.Element("versioncontrol", "kdevsubversion");
  } else if (hasCvs) {
    xml.Element("versioncontrol", "kdevcvsservice");
  }

  xml.EndElement(); // general
  xml.StartElement("kdevcustomproject");

  xml.Element("filelistdirectory", outputDir);

  xml.StartElement("run");
  xml.Element("mainprogram", executable);
  xml.Element("directoryradio", "custom");
  xml.Element("customdirectory", outputDir);
  xml.Element("programargs", "");
  xml.Element("terminal", "false");
  xml.Element("autocompile", "true");
  xml.Element("envvars");
  xml.EndElement();

  xml.StartElement("build");
  xml.Element("buildtool", "make");   // this one is important
  xml.Element("builddir", outputDir); // and this one
  xml.EndElement();

  xml.StartElement("make");
  xml.Element("abortonerror", "false");
  xml.Element("numberofjobs", 1);
  xml.Element("dontact", "false");
  xml.Element("makebin", this->GlobalGenerator->GetLocalGenerators()[0]
                           ->GetMakefile()
                           ->GetRequiredDefinition("CMAKE_MAKE_PROGRAM"));
  xml.Element("selectedenvironment", "default");

  xml.StartElement("environments");
  xml.StartElement("default");

  xml.StartElement("envvar");
  xml.Attribute("value", 1);
  xml.Attribute("name", "VERBOSE");
  xml.EndElement();

  xml.StartElement("envvar");
  xml.Attribute("value", 1);
  xml.Attribute("name", "CMAKE_NO_VERBOSE");
  xml.EndElement();

  xml.EndElement(); // default
  xml.EndElement(); // environments
  xml.EndElement(); // make

  xml.StartElement("blacklist");
  for (std::string const& dir : this->Blacklist) {
    xml.Element("path", dir);
  }
  xml.EndElement();

  xml.EndElement(); // kdevcustomproject

  xml.StartElement("kdevfilecreate");
  xml.Element("filetypes");
  xml.StartElement("useglobaltypes");

  xml.StartElement("type");
  xml.Attribute("ext", "ui");
  xml.EndElement();

  xml.StartElement("type");
  xml.Attribute("ext", "cpp");
  xml.EndElement();

  xml.StartElement("type");
  xml.Attribute("ext", "h");
  xml.EndElement();

  xml.EndElement(); // useglobaltypes
  xml.EndElement(); // kdevfilecreate

  xml.StartElement("kdevdoctreeview");
  xml.StartElement("projectdoc");
  xml.Element("userdocDir", "html/");
  xml.Element("apidocDir", "html/");
  xml.EndElement(); // projectdoc
  xml.Element("ignoreqt_xml");
  xml.Element("ignoredoxygen");
  xml.Element("ignorekdocs");
  xml.Element("ignoretocs");
  xml.Element("ignoredevhelp");
  xml.EndElement(); // kdevdoctreeview;

  if (enableCxx) {
    xml.StartElement("cppsupportpart");
    xml.StartElement("filetemplates");
    xml.Element("interfacesuffix", ".h");
    xml.Element("implementationsuffix", ".cpp");
    xml.EndElement(); // filetemplates
    xml.EndElement(); // cppsupportpart

    xml.StartElement("kdevcppsupport");
    xml.StartElement("codecompletion");
    xml.Element("includeGlobalFunctions", "true");
    xml.Element("includeTypes", "true");
    xml.Element("includeEnums", "true");
    xml.Element("includeTypedefs", "false");
    xml.Element("automaticCodeCompletion", "true");
    xml.Element("automaticArgumentsHint", "true");
    xml.Element("automaticHeaderCompletion", "true");
    xml.Element("codeCompletionDelay", 250);
    xml.Element("argumentsHintDelay", 400);
    xml.Element("headerCompletionDelay", 250);
    xml.EndElement(); // codecompletion
    xml.Element("references");
    xml.EndElement(); // kdevcppsupport;
  }

  if (enableFortran) {
    xml.StartElement("kdevfortransupport");
    xml.StartElement("ftnchek");
    xml.Element("division", "false");
    xml.Element("extern", "false");
    xml.Element("declare", "false");
    xml.Element("pure", "false");
    xml.Element("argumentsall", "false");
    xml.Element("commonall", "false");
    xml.Element("truncationall", "false");
    xml.Element("usageall", "false");
    xml.Element("f77all", "false");
    xml.Element("portabilityall", "false");
    xml.Element("argumentsonly");
    xml.Element("commononly");
    xml.Element("truncationonly");
    xml.Element("usageonly");
    xml.Element("f77only");
    xml.Element("portabilityonly");
    xml.EndElement(); // ftnchek
    xml.EndElement(); // kdevfortransupport;
  }

  // set up file groups. maybe this can be used with the CMake SOURCE_GROUP()
  // command
  xml.StartElement("kdevfileview");
  xml.StartElement("groups");

  xml.StartElement("group");
  xml.Attribute("pattern", cmakeFilePattern);
  xml.Attribute("name", "CMake");
  xml.EndElement();

  if (enableCxx) {
    xml.StartElement("group");
    xml.Attribute("pattern", "*.h;*.hxx;*.hpp");
    xml.Attribute("name", "Header");
    xml.EndElement();

    xml.StartElement("group");
    xml.Attribute("pattern", "*.c");
    xml.Attribute("name", "C Sources");
    xml.EndElement();

    xml.StartElement("group");
    xml.Attribute("pattern", "*.cpp;*.C;*.cxx;*.cc");
    xml.Attribute("name", "C++ Sources");
    xml.EndElement();
  }

  if (enableFortran) {
    xml.StartElement("group");
    xml.Attribute("pattern",
                  "*.f;*.F;*.f77;*.F77;*.f90;*.F90;*.for;*.f95;*.F95");
    xml.Attribute("name", "Fortran Sources");
    xml.EndElement();
  }

  xml.StartElement("group");
  xml.Attribute("pattern", "*.ui");
  xml.Attribute("name", "Qt Designer files");
  xml.EndElement();

  xml.Element("hidenonprojectfiles", "true");
  xml.EndElement(); // groups

  xml.StartElement("tree");
  xml.Element("hidepatterns", "*.o,*.lo,CVS,*~,cmake*");
  xml.Element("hidenonprojectfiles", "true");
  xml.EndElement(); // tree

  xml.EndElement(); // kdevfileview
  xml.EndElement(); // kdevelop;
  xml.EndDocument();

  if (sessionFilename.empty()) {
    return;
  }

  // and a session file, so that kdevelop opens a file if it opens the
  // project the first time
  cmGeneratedFileStream devses(sessionFilename.c_str());
  if (!devses) {
    return;
  }
  cmXMLWriter sesxml(devses);
  sesxml.StartDocument("UTF-8");
  sesxml.Doctype("KDevPrjSession");
  sesxml.StartElement("KDevPrjSession");

  sesxml.StartElement("DocsAndViews");
  sesxml.Attribute("NumberOfDocuments", 1);

  sesxml.StartElement("Doc0");
  sesxml.Attribute("NumberOfViews", 1);
  sesxml.Attribute("URL", "file://" + fileToOpen);

  sesxml.StartElement("View0");
  sesxml.Attribute("line", 0);
  sesxml.Attribute("Type", "Source");
  sesxml.EndElement(); // View0

  sesxml.EndElement(); // Doc0
  sesxml.EndElement(); // DocsAndViews
  sesxml.EndElement(); // KDevPrjSession;
}
