/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
   file LICENSE.rst or https://cmake.org/licensing for details.  */
/* clang-format off */
#include "cmGeneratorTarget.h"
/* clang-format on */

#include <algorithm>
#include <cassert>
#include <cerrno>
#include <cstddef>
#include <cstdlib>
#include <cstring>
#include <iterator>
#include <map>
#include <set>
#include <sstream>
#include <string>
#include <utility>
#include <vector>

#include <cm/memory>
#include <cmext/algorithm>

#include "cmComputeLinkInformation.h"
#include "cmGeneratorExpression.h"
#include "cmList.h"
#include "cmLocalGenerator.h"
#include "cmMessageType.h"
#include "cmRange.h"
#include "cmStateTypes.h"
#include "cmStringAlgorithms.h"
#include "cmSystemTools.h"
#include "cmValue.h"

namespace {
using UseTo = cmGeneratorTarget::UseTo;
}

cmGeneratorTarget::CompatibleInterfacesBase const&
cmGeneratorTarget::GetCompatibleInterfaces(std::string const& config) const
{
  cmGeneratorTarget::CompatibleInterfaces& compat =
    this->CompatibleInterfacesMap[config];
  if (!compat.Done) {
    compat.Done = true;
    compat.PropsBool.insert("POSITION_INDEPENDENT_CODE");
    compat.PropsString.insert("AUTOUIC_OPTIONS");
    std::vector<cmGeneratorTarget const*> const& deps =
      this->GetLinkImplementationClosure(config, UseTo::Compile);
    for (cmGeneratorTarget const* li : deps) {
#define CM_READ_COMPATIBLE_INTERFACE(X, x)                                    \
  if (cmValue prop = li->GetProperty("COMPATIBLE_INTERFACE_" #X)) {           \
    cmList props(*prop);                                                      \
    compat.Props##x.insert(props.begin(), props.end());                       \
  }
      CM_READ_COMPATIBLE_INTERFACE(BOOL, Bool)
      CM_READ_COMPATIBLE_INTERFACE(STRING, String)
      CM_READ_COMPATIBLE_INTERFACE(NUMBER_MIN, NumberMin)
      CM_READ_COMPATIBLE_INTERFACE(NUMBER_MAX, NumberMax)
#undef CM_READ_COMPATIBLE_INTERFACE
    }
  }
  return compat;
}

bool cmGeneratorTarget::IsLinkInterfaceDependentBoolProperty(
  std::string const& p, std::string const& config) const
{
  if (this->GetType() == cmStateEnums::OBJECT_LIBRARY ||
      this->GetType() == cmStateEnums::INTERFACE_LIBRARY) {
    return false;
  }
  return this->GetCompatibleInterfaces(config).PropsBool.count(p) > 0;
}

bool cmGeneratorTarget::IsLinkInterfaceDependentStringProperty(
  std::string const& p, std::string const& config) const
{
  if (this->GetType() == cmStateEnums::OBJECT_LIBRARY ||
      this->GetType() == cmStateEnums::INTERFACE_LIBRARY) {
    return false;
  }
  return this->GetCompatibleInterfaces(config).PropsString.count(p) > 0;
}

bool cmGeneratorTarget::IsLinkInterfaceDependentNumberMinProperty(
  std::string const& p, std::string const& config) const
{
  if (this->GetType() == cmStateEnums::OBJECT_LIBRARY ||
      this->GetType() == cmStateEnums::INTERFACE_LIBRARY) {
    return false;
  }
  return this->GetCompatibleInterfaces(config).PropsNumberMin.count(p) > 0;
}

bool cmGeneratorTarget::IsLinkInterfaceDependentNumberMaxProperty(
  std::string const& p, std::string const& config) const
{
  if (this->GetType() == cmStateEnums::OBJECT_LIBRARY ||
      this->GetType() == cmStateEnums::INTERFACE_LIBRARY) {
    return false;
  }
  return this->GetCompatibleInterfaces(config).PropsNumberMax.count(p) > 0;
}

enum CompatibleType
{
  BoolType,
  StringType,
  NumberMinType,
  NumberMaxType
};

template <typename PropertyType>
PropertyType getLinkInterfaceDependentProperty(cmGeneratorTarget const* tgt,
                                               std::string const& prop,
                                               std::string const& config,
                                               CompatibleType, PropertyType*);

template <>
bool getLinkInterfaceDependentProperty(cmGeneratorTarget const* tgt,
                                       std::string const& prop,
                                       std::string const& config,
                                       CompatibleType /*unused*/,
                                       bool* /*unused*/)
{
  return tgt->GetLinkInterfaceDependentBoolProperty(prop, config);
}

template <>
char const* getLinkInterfaceDependentProperty(cmGeneratorTarget const* tgt,
                                              std::string const& prop,
                                              std::string const& config,
                                              CompatibleType t,
                                              char const** /*unused*/)
{
  switch (t) {
    case BoolType:
      assert(false &&
             "String compatibility check function called for boolean");
      return nullptr;
    case StringType:
      return tgt->GetLinkInterfaceDependentStringProperty(prop, config);
    case NumberMinType:
      return tgt->GetLinkInterfaceDependentNumberMinProperty(prop, config);
    case NumberMaxType:
      return tgt->GetLinkInterfaceDependentNumberMaxProperty(prop, config);
  }
  assert(false && "Unreachable!");
  return nullptr;
}

template <typename PropertyType>
void checkPropertyConsistency(cmGeneratorTarget const* depender,
                              cmGeneratorTarget const* dependee,
                              std::string const& propName,
                              std::set<std::string>& emitted,
                              std::string const& config, CompatibleType t,
                              PropertyType* /*unused*/)
{
  cmValue prop = dependee->GetProperty(propName);
  if (!prop) {
    return;
  }

  cmList props{ *prop };
  std::string pdir =
    cmStrCat(cmSystemTools::GetCMakeRoot(), "/Help/prop_tgt/");

  for (std::string const& p : props) {
    std::string pname = cmSystemTools::HelpFileName(p);
    std::string pfile = cmStrCat(pdir, pname, ".rst");
    if (cmSystemTools::FileExists(pfile, true)) {
      std::ostringstream e;
      e << "Target \"" << dependee->GetName() << "\" has property \"" << p
        << "\" listed in its " << propName
        << " property.  "
           "This is not allowed.  Only user-defined properties may appear "
           "listed in the "
        << propName << " property.";
      depender->GetLocalGenerator()->IssueMessage(MessageType::FATAL_ERROR,
                                                  e.str());
      return;
    }
    if (emitted.insert(p).second) {
      getLinkInterfaceDependentProperty<PropertyType>(depender, p, config, t,
                                                      nullptr);
      if (cmSystemTools::GetErrorOccurredFlag()) {
        return;
      }
    }
  }
}

namespace {
std::string intersect(std::set<std::string> const& s1,
                      std::set<std::string> const& s2)
{
  std::set<std::string> intersect;
  std::set_intersection(s1.begin(), s1.end(), s2.begin(), s2.end(),
                        std::inserter(intersect, intersect.begin()));
  if (!intersect.empty()) {
    return *intersect.begin();
  }
  return "";
}

std::string intersect(std::set<std::string> const& s1,
                      std::set<std::string> const& s2,
                      std::set<std::string> const& s3)
{
  std::string result;
  result = intersect(s1, s2);
  if (!result.empty()) {
    return result;
  }
  result = intersect(s1, s3);
  if (!result.empty()) {
    return result;
  }
  return intersect(s2, s3);
}

std::string intersect(std::set<std::string> const& s1,
                      std::set<std::string> const& s2,
                      std::set<std::string> const& s3,
                      std::set<std::string> const& s4)
{
  std::string result;
  result = intersect(s1, s2);
  if (!result.empty()) {
    return result;
  }
  result = intersect(s1, s3);
  if (!result.empty()) {
    return result;
  }
  result = intersect(s1, s4);
  if (!result.empty()) {
    return result;
  }
  return intersect(s2, s3, s4);
}
}

void cmGeneratorTarget::CheckPropertyCompatibility(
  cmComputeLinkInformation& info, std::string const& config) const
{
  cmComputeLinkInformation::ItemVector const& deps = info.GetItems();

  std::set<std::string> emittedBools;
  static std::string const strBool = "COMPATIBLE_INTERFACE_BOOL";
  std::set<std::string> emittedStrings;
  static std::string const strString = "COMPATIBLE_INTERFACE_STRING";
  std::set<std::string> emittedMinNumbers;
  static std::string const strNumMin = "COMPATIBLE_INTERFACE_NUMBER_MIN";
  std::set<std::string> emittedMaxNumbers;
  static std::string const strNumMax = "COMPATIBLE_INTERFACE_NUMBER_MAX";

  for (auto const& dep : deps) {
    if (!dep.Target || dep.Target->GetType() == cmStateEnums::OBJECT_LIBRARY) {
      continue;
    }

    checkPropertyConsistency<bool>(this, dep.Target, strBool, emittedBools,
                                   config, BoolType, nullptr);
    if (cmSystemTools::GetErrorOccurredFlag()) {
      return;
    }
    checkPropertyConsistency<char const*>(this, dep.Target, strString,
                                          emittedStrings, config, StringType,
                                          nullptr);
    if (cmSystemTools::GetErrorOccurredFlag()) {
      return;
    }
    checkPropertyConsistency<char const*>(this, dep.Target, strNumMin,
                                          emittedMinNumbers, config,
                                          NumberMinType, nullptr);
    if (cmSystemTools::GetErrorOccurredFlag()) {
      return;
    }
    checkPropertyConsistency<char const*>(this, dep.Target, strNumMax,
                                          emittedMaxNumbers, config,
                                          NumberMaxType, nullptr);
    if (cmSystemTools::GetErrorOccurredFlag()) {
      return;
    }
  }

  std::string prop = intersect(emittedBools, emittedStrings, emittedMinNumbers,
                               emittedMaxNumbers);

  if (!prop.empty()) {
    // Use a sorted std::vector to keep the error message sorted.
    std::vector<std::string> props;
    auto i = emittedBools.find(prop);
    if (i != emittedBools.end()) {
      props.push_back(strBool);
    }
    i = emittedStrings.find(prop);
    if (i != emittedStrings.end()) {
      props.push_back(strString);
    }
    i = emittedMinNumbers.find(prop);
    if (i != emittedMinNumbers.end()) {
      props.push_back(strNumMin);
    }
    i = emittedMaxNumbers.find(prop);
    if (i != emittedMaxNumbers.end()) {
      props.push_back(strNumMax);
    }
    std::sort(props.begin(), props.end());

    std::string propsString = cmStrCat(
      cmJoin(cmMakeRange(props).retreat(1), ", "), " and the ", props.back());

    std::ostringstream e;
    e << "Property \"" << prop << "\" appears in both the " << propsString
      << " property in the dependencies of target \"" << this->GetName()
      << "\".  This is not allowed. A property may only require "
         "compatibility "
         "in a boolean interpretation, a numeric minimum, a numeric maximum "
         "or a "
         "string interpretation, but not a mixture.";
    this->LocalGenerator->IssueMessage(MessageType::FATAL_ERROR, e.str());
  }
}

template <typename PropertyType>
std::string valueAsString(PropertyType);
template <>
std::string valueAsString<bool>(bool value)
{
  return value ? "TRUE" : "FALSE";
}
template <>
std::string valueAsString<char const*>(char const* value)
{
  return value ? value : "(unset)";
}
template <>
std::string valueAsString<std::string>(std::string value)
{
  return value;
}
template <>
std::string valueAsString<cmValue>(cmValue value)
{
  return value ? *value : std::string("(unset)");
}
template <>
std::string valueAsString<std::nullptr_t>(std::nullptr_t /*unused*/)
{
  return "(unset)";
}

static std::string compatibilityType(CompatibleType t)
{
  switch (t) {
    case BoolType:
      return "Boolean compatibility";
    case StringType:
      return "String compatibility";
    case NumberMaxType:
      return "Numeric maximum compatibility";
    case NumberMinType:
      return "Numeric minimum compatibility";
  }
  assert(false && "Unreachable!");
  return "";
}

static std::string compatibilityAgree(CompatibleType t, bool dominant)
{
  switch (t) {
    case BoolType:
    case StringType:
      return dominant ? "(Disagree)\n" : "(Agree)\n";
    case NumberMaxType:
    case NumberMinType:
      return dominant ? "(Dominant)\n" : "(Ignored)\n";
  }
  assert(false && "Unreachable!");
  return "";
}

template <typename PropertyType>
PropertyType getTypedProperty(
  cmGeneratorTarget const* tgt, std::string const& prop,
  cmGeneratorExpressionInterpreter* genexInterpreter = nullptr);

template <>
bool getTypedProperty<bool>(cmGeneratorTarget const* tgt,
                            std::string const& prop,
                            cmGeneratorExpressionInterpreter* genexInterpreter)
{
  if (!genexInterpreter) {
    return tgt->GetPropertyAsBool(prop);
  }

  cmValue value = tgt->GetProperty(prop);
  return cmIsOn(genexInterpreter->Evaluate(value ? *value : "", prop));
}

template <>
char const* getTypedProperty<char const*>(
  cmGeneratorTarget const* tgt, std::string const& prop,
  cmGeneratorExpressionInterpreter* genexInterpreter)
{
  cmValue value = tgt->GetProperty(prop);

  if (!genexInterpreter) {
    return value.GetCStr();
  }

  return genexInterpreter->Evaluate(value ? *value : "", prop).c_str();
}

template <>
std::string getTypedProperty<std::string>(
  cmGeneratorTarget const* tgt, std::string const& prop,
  cmGeneratorExpressionInterpreter* genexInterpreter)
{
  cmValue value = tgt->GetProperty(prop);

  if (!genexInterpreter) {
    return valueAsString(value);
  }

  return genexInterpreter->Evaluate(value ? *value : "", prop);
}

template <typename PropertyType>
PropertyType impliedValue(PropertyType);
template <>
bool impliedValue<bool>(bool /*unused*/)
{
  return false;
}
template <>
char const* impliedValue<char const*>(char const* /*unused*/)
{
  return "";
}
template <>
std::string impliedValue<std::string>(std::string /*unused*/) // NOLINT(*)
{
  return std::string();
}

template <typename PropertyType>
std::pair<bool, PropertyType> consistentProperty(PropertyType lhs,
                                                 PropertyType rhs,
                                                 CompatibleType t);

template <>
std::pair<bool, bool> consistentProperty(bool lhs, bool rhs,
                                         CompatibleType /*unused*/)
{
  return { lhs == rhs, lhs };
}

static std::pair<bool, char const*> consistentStringProperty(char const* lhs,
                                                             char const* rhs)
{
  bool const b = strcmp(lhs, rhs) == 0;
  return { b, b ? lhs : nullptr };
}

static std::pair<bool, std::string> consistentStringProperty(
  std::string const& lhs, std::string const& rhs)
{
  bool const b = lhs == rhs;
  return { b, b ? lhs : valueAsString(nullptr) };
}

static std::pair<bool, char const*> consistentNumberProperty(char const* lhs,
                                                             char const* rhs,
                                                             CompatibleType t)
{
  char* pEnd;

  long lnum = strtol(lhs, &pEnd, 0);
  if (pEnd == lhs || *pEnd != '\0' || errno == ERANGE) {
    return { false, nullptr };
  }

  long rnum = strtol(rhs, &pEnd, 0);
  if (pEnd == rhs || *pEnd != '\0' || errno == ERANGE) {
    return { false, nullptr };
  }

  if (t == NumberMaxType) {
    return { true, std::max(lnum, rnum) == lnum ? lhs : rhs };
  }

  return { true, std::min(lnum, rnum) == lnum ? lhs : rhs };
}

template <>
std::pair<bool, char const*> consistentProperty(char const* lhs,
                                                char const* rhs,
                                                CompatibleType t)
{
  if (!lhs && !rhs) {
    return { true, lhs };
  }
  if (!lhs) {
    return { true, rhs };
  }
  if (!rhs) {
    return { true, lhs };
  }

  switch (t) {
    case BoolType: {
      bool same = cmIsOn(lhs) == cmIsOn(rhs);
      return { same, same ? lhs : nullptr };
    }
    case StringType:
      return consistentStringProperty(lhs, rhs);
    case NumberMinType:
    case NumberMaxType:
      return consistentNumberProperty(lhs, rhs, t);
  }
  assert(false && "Unreachable!");
  return { false, nullptr };
}

static std::pair<bool, std::string> consistentProperty(std::string const& lhs,
                                                       std::string const& rhs,
                                                       CompatibleType t)
{
  std::string const null_ptr = valueAsString(nullptr);

  if (lhs == null_ptr && rhs == null_ptr) {
    return { true, lhs };
  }
  if (lhs == null_ptr) {
    return { true, rhs };
  }
  if (rhs == null_ptr) {
    return { true, lhs };
  }

  switch (t) {
    case BoolType: {
      bool same = cmIsOn(lhs) == cmIsOn(rhs);
      return { same, same ? lhs : null_ptr };
    }
    case StringType:
      return consistentStringProperty(lhs, rhs);
    case NumberMinType:
    case NumberMaxType: {
      auto value = consistentNumberProperty(lhs.c_str(), rhs.c_str(), t);
      return { value.first,
               value.first ? std::string(value.second) : null_ptr };
    }
  }
  assert(false && "Unreachable!");
  return { false, null_ptr };
}

template <typename PropertyType>
PropertyType checkInterfacePropertyCompatibility(cmGeneratorTarget const* tgt,
                                                 std::string const& p,
                                                 std::string const& config,
                                                 char const* defaultValue,
                                                 CompatibleType t,
                                                 PropertyType* /*unused*/)
{
  PropertyType propContent = getTypedProperty<PropertyType>(tgt, p);

  std::vector<std::string> headPropKeys = tgt->GetPropertyKeys();
  bool const explicitlySet = cm::contains(headPropKeys, p);

  bool const impliedByUse = tgt->IsNullImpliedByLinkLibraries(p);
  assert((impliedByUse ^ explicitlySet) || (!impliedByUse && !explicitlySet));

  std::vector<cmGeneratorTarget const*> const& deps =
    tgt->GetLinkImplementationClosure(config, UseTo::Compile);

  if (deps.empty()) {
    return propContent;
  }
  bool propInitialized = explicitlySet;

  std::string report = cmStrCat(" * Target \"", tgt->GetName());
  if (explicitlySet) {
    report += "\" has property content \"";
    report += valueAsString<PropertyType>(propContent);
    report += "\"\n";
  } else if (impliedByUse) {
    report += "\" property is implied by use.\n";
  } else {
    report += "\" property not set.\n";
  }

  std::string interfaceProperty = "INTERFACE_" + p;
  std::unique_ptr<cmGeneratorExpressionInterpreter> genexInterpreter;
  if (p == "POSITION_INDEPENDENT_CODE") {
    // Corresponds to EvaluatingPICExpression.
    genexInterpreter = cm::make_unique<cmGeneratorExpressionInterpreter>(
      tgt->GetLocalGenerator(), config, tgt);
  }

  for (cmGeneratorTarget const* theTarget : deps) {
    // An error should be reported if one dependency
    // has INTERFACE_POSITION_INDEPENDENT_CODE ON and the other
    // has INTERFACE_POSITION_INDEPENDENT_CODE OFF, or if the
    // target itself has a POSITION_INDEPENDENT_CODE which disagrees
    // with a dependency.

    std::vector<std::string> propKeys = theTarget->GetPropertyKeys();

    bool const ifaceIsSet = cm::contains(propKeys, interfaceProperty);
    PropertyType ifacePropContent = getTypedProperty<PropertyType>(
      theTarget, interfaceProperty, genexInterpreter.get());

    std::string reportEntry;
    if (ifaceIsSet) {
      reportEntry += " * Target \"";
      reportEntry += theTarget->GetName();
      reportEntry += "\" property value \"";
      reportEntry += valueAsString<PropertyType>(ifacePropContent);
      reportEntry += "\" ";
    }

    if (explicitlySet) {
      if (ifaceIsSet) {
        std::pair<bool, PropertyType> consistent =
          consistentProperty(propContent, ifacePropContent, t);
        report += reportEntry;
        report += compatibilityAgree(t, propContent != consistent.second);
        if (!consistent.first) {
          std::ostringstream e;
          e << "Property " << p << " on target \"" << tgt->GetName()
            << "\" does\nnot match the "
               "INTERFACE_"
            << p
            << " property requirement\nof "
               "dependency \""
            << theTarget->GetName() << "\".\n";
          cmSystemTools::Error(e.str());
          break;
        }
        propContent = consistent.second;
        continue;
      }
      // Explicitly set on target and not set in iface. Can't disagree.
      continue;
    }
    if (impliedByUse) {
      propContent = impliedValue<PropertyType>(propContent);

      if (ifaceIsSet) {
        std::pair<bool, PropertyType> consistent =
          consistentProperty(propContent, ifacePropContent, t);
        report += reportEntry;
        report += compatibilityAgree(t, propContent != consistent.second);
        if (!consistent.first) {
          std::ostringstream e;
          e << "Property " << p << " on target \"" << tgt->GetName()
            << "\" is\nimplied to be " << defaultValue
            << " because it was used to determine the link libraries\n"
               "already. The INTERFACE_"
            << p << " property on\ndependency \"" << theTarget->GetName()
            << "\" is in conflict.\n";
          cmSystemTools::Error(e.str());
          break;
        }
        propContent = consistent.second;
        continue;
      }
      // Implicitly set on target and not set in iface. Can't disagree.
      continue;
    }
    if (ifaceIsSet) {
      if (propInitialized) {
        std::pair<bool, PropertyType> consistent =
          consistentProperty(propContent, ifacePropContent, t);
        report =
          cmStrCat(std::move(report), reportEntry,
                   compatibilityAgree(t, propContent != consistent.second));
        if (!consistent.first) {
          std::ostringstream e;
          e << "The INTERFACE_" << p << " property of \""
            << theTarget->GetName() << "\" does\nnot agree with the value of "
            << p << " already determined\nfor \"" << tgt->GetName() << "\".\n";
          cmSystemTools::Error(e.str());
          break;
        }
        propContent = consistent.second;
        continue;
      }
      report = cmStrCat(std::move(report), reportEntry, "(Interface set)\n");
      propContent = ifacePropContent;
      propInitialized = true;
    } else {
      // Not set. Nothing to agree on.
      continue;
    }
  }

  tgt->ReportPropertyOrigin(p, valueAsString<PropertyType>(propContent),
                            report, compatibilityType(t));
  return propContent;
}

bool cmGeneratorTarget::GetLinkInterfaceDependentBoolProperty(
  std::string const& p, std::string const& config) const
{
  return checkInterfacePropertyCompatibility<bool>(this, p, config, "FALSE",
                                                   BoolType, nullptr);
}

std::string cmGeneratorTarget::GetLinkInterfaceDependentStringAsBoolProperty(
  std::string const& p, std::string const& config) const
{
  return checkInterfacePropertyCompatibility<std::string>(
    this, p, config, "FALSE", BoolType, nullptr);
}

char const* cmGeneratorTarget::GetLinkInterfaceDependentStringProperty(
  std::string const& p, std::string const& config) const
{
  return checkInterfacePropertyCompatibility<char const*>(
    this, p, config, "empty", StringType, nullptr);
}

char const* cmGeneratorTarget::GetLinkInterfaceDependentNumberMinProperty(
  std::string const& p, std::string const& config) const
{
  return checkInterfacePropertyCompatibility<char const*>(
    this, p, config, "empty", NumberMinType, nullptr);
}

char const* cmGeneratorTarget::GetLinkInterfaceDependentNumberMaxProperty(
  std::string const& p, std::string const& config) const
{
  return checkInterfacePropertyCompatibility<char const*>(
    this, p, config, "empty", NumberMaxType, nullptr);
}
