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

#include <cassert>

#include <cm/string_view>

#include "cmGlobalGenerator.h"
#include "cmMakefile.h"
#include "cmMessageType.h"
#include "cmStringAlgorithms.h"
#include "cmSystemTools.h"
#include "cmake.h"

cmSourceFileLocation::cmSourceFileLocation() = default;

cmSourceFileLocation::cmSourceFileLocation(const cmSourceFileLocation& loc)
  : Makefile(loc.Makefile)
{
  AmbiguousDirectory = loc.AmbiguousDirectory;
  AmbiguousExtension = loc.AmbiguousExtension;
  Directory = loc.Directory;
  Name = loc.Name;
}

cmSourceFileLocation::cmSourceFileLocation(cmMakefile const* mf,
                                           const std::string& name,
                                           cmSourceFileLocationKind kind)
  : Makefile(mf)
{
  AmbiguousDirectory = !cmSystemTools::FileIsFullPath(name);
  AmbiguousExtension = true;
  Directory = cmSystemTools::GetFilenamePath(name);
  if (cmSystemTools::FileIsFullPath(Directory)) {
    Directory =
      cmSystemTools::CollapseFullPath(Directory, mf->GetHomeOutputDirectory());
  }
  Name = cmSystemTools::GetFilenameName(name);
  if (kind == cmSourceFileLocationKind::Known) {
    DirectoryUseSource();
    AmbiguousExtension = false;
  } else {
    UpdateExtension(name);
  }
}

std::string cmSourceFileLocation::GetFullPath() const
{
  std::string path = GetDirectory();
  if (!path.empty()) {
    path += '/';
  }
  path += GetName();
  return path;
}

void cmSourceFileLocation::Update(cmSourceFileLocation const& loc)
{
  if (AmbiguousDirectory && !loc.AmbiguousDirectory) {
    Directory = loc.Directory;
    AmbiguousDirectory = false;
  }
  if (AmbiguousExtension && !loc.AmbiguousExtension) {
    Name = loc.Name;
    AmbiguousExtension = false;
  }
}

void cmSourceFileLocation::DirectoryUseSource()
{
  assert(Makefile);
  if (AmbiguousDirectory) {
    Directory = cmSystemTools::CollapseFullPath(
      Directory, Makefile->GetCurrentSourceDirectory());
    AmbiguousDirectory = false;
  }
}

void cmSourceFileLocation::DirectoryUseBinary()
{
  assert(Makefile);
  if (AmbiguousDirectory) {
    Directory = cmSystemTools::CollapseFullPath(
      Directory, Makefile->GetCurrentBinaryDirectory());
    AmbiguousDirectory = false;
  }
}

void cmSourceFileLocation::UpdateExtension(const std::string& name)
{
  assert(Makefile);
  // Check the extension.
  std::string ext = cmSystemTools::GetFilenameLastExtension(name);
  if (!ext.empty()) {
    ext = ext.substr(1);
  }

  // The global generator checks extensions of enabled languages.
  cmGlobalGenerator* gg = Makefile->GetGlobalGenerator();
  cmMakefile const* mf = Makefile;
  auto cm = mf->GetCMakeInstance();
  if (!gg->GetLanguageFromExtension(ext.c_str()).empty() ||
      cm->IsAKnownExtension(ext)) {
    // This is a known extension.  Use the given filename with extension.
    Name = cmSystemTools::GetFilenameName(name);
    AmbiguousExtension = false;
  } else {
    // This is not a known extension.  See if the file exists on disk as
    // named.
    std::string tryPath;
    if (AmbiguousDirectory) {
      // Check the source tree only because a file in the build tree should
      // be specified by full path at least once.  We do not want this
      // detection to depend on whether the project has already been built.
      tryPath = cmStrCat(Makefile->GetCurrentSourceDirectory(), '/');
    }
    if (!Directory.empty()) {
      tryPath += Directory;
      tryPath += "/";
    }
    tryPath += Name;
    if (cmSystemTools::FileExists(tryPath, true)) {
      // We found a source file named by the user on disk.  Trust it's
      // extension.
      Name = cmSystemTools::GetFilenameName(name);
      AmbiguousExtension = false;

      // If the directory was ambiguous, it isn't anymore.
      if (AmbiguousDirectory) {
        DirectoryUseSource();
      }
    }
  }
}

bool cmSourceFileLocation::MatchesAmbiguousExtension(
  cmSourceFileLocation const& loc) const
{
  assert(Makefile);
  // This location's extension is not ambiguous but loc's extension
  // is.  See if the names match as-is.
  if (Name == loc.Name) {
    return true;
  }

  // Check if loc's name could possibly be extended to our name by
  // adding an extension.
  if (!(Name.size() > loc.Name.size() && Name[loc.Name.size()] == '.' &&
        cmHasPrefix(Name, loc.Name))) {
    return false;
  }

  // Only a fixed set of extensions will be tried to match a file on
  // disk.  One of these must match if loc refers to this source file.
  auto ext = cm::string_view(Name).substr(loc.Name.size() + 1);
  cmMakefile const* mf = Makefile;
  auto cm = mf->GetCMakeInstance();
  return cm->IsAKnownExtension(ext);
}

bool cmSourceFileLocation::Matches(cmSourceFileLocation const& loc)
{
  assert(Makefile);
  if (AmbiguousExtension == loc.AmbiguousExtension) {
    // Both extensions are similarly ambiguous.  Since only the old fixed set
    // of extensions will be tried, the names must match at this point to be
    // the same file.
    if (Name.size() != loc.Name.size() ||
        !cmSystemTools::ComparePath(Name, loc.Name)) {
      return false;
    }
  } else {
    const cmSourceFileLocation* loc1;
    const cmSourceFileLocation* loc2;
    if (AmbiguousExtension) {
      // Only "this" extension is ambiguous.
      loc1 = &loc;
      loc2 = this;
    } else {
      // Only "loc" extension is ambiguous.
      loc1 = this;
      loc2 = &loc;
    }
    if (!loc1->MatchesAmbiguousExtension(*loc2)) {
      return false;
    }
  }

  if (!AmbiguousDirectory && !loc.AmbiguousDirectory) {
    // Both sides have absolute directories.
    if (Directory != loc.Directory) {
      return false;
    }
  } else if (AmbiguousDirectory && loc.AmbiguousDirectory) {
    if (Makefile == loc.Makefile) {
      // Both sides have directories relative to the same location.
      if (Directory != loc.Directory) {
        return false;
      }
    } else {
      // Each side has a directory relative to a different location.
      // This can occur when referencing a source file from a different
      // directory.  This is not yet allowed.
      Makefile->IssueMessage(
        MessageType::INTERNAL_ERROR,
        "Matches error: Each side has a directory relative to a different "
        "location. This can occur when referencing a source file from a "
        "different directory.  This is not yet allowed.");
      return false;
    }
  } else if (AmbiguousDirectory) {
    // Compare possible directory combinations.
    std::string const srcDir = cmSystemTools::CollapseFullPath(
      Directory, Makefile->GetCurrentSourceDirectory());
    std::string const binDir = cmSystemTools::CollapseFullPath(
      Directory, Makefile->GetCurrentBinaryDirectory());
    if (srcDir != loc.Directory && binDir != loc.Directory) {
      return false;
    }
  } else if (loc.AmbiguousDirectory) {
    // Compare possible directory combinations.
    std::string const srcDir = cmSystemTools::CollapseFullPath(
      loc.Directory, loc.Makefile->GetCurrentSourceDirectory());
    std::string const binDir = cmSystemTools::CollapseFullPath(
      loc.Directory, loc.Makefile->GetCurrentBinaryDirectory());
    if (srcDir != Directory && binDir != Directory) {
      return false;
    }
  }

  // File locations match.
  Update(loc);
  return true;
}
