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

#include <cassert>
#include <utility>

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

#include "cmConfigureLog.h"
#include "cmDiagnostics.h"
#include "cmExecutionStatus.h"
#include "cmList.h"
#include "cmMakefile.h"
#include "cmMessageType.h"
#include "cmMessenger.h"
#include "cmPolicies.h"
#include "cmRange.h"
#include "cmStringAlgorithms.h"
#include "cmSystemTools.h"
#include "cmValue.h"
#include "cmake.h"

#ifdef CMake_ENABLE_DEBUGGER
#  include "cmDebuggerAdapter.h"
#endif

namespace {

std::string IndentText(std::string text, cmMakefile& mf)
{
  auto indent =
    cmList{ mf.GetSafeDefinition("CMAKE_MESSAGE_INDENT") }.join("");

  auto const showContext = mf.GetCMakeInstance()->GetShowLogContext() ||
    mf.IsOn("CMAKE_MESSAGE_CONTEXT_SHOW");
  if (showContext) {
    auto context =
      cmList{ mf.GetSafeDefinition("CMAKE_MESSAGE_CONTEXT") }.join(".");
    if (!context.empty()) {
      indent.insert(0u, cmStrCat("["_s, context, "] "_s));
    }
  }

  if (!indent.empty()) {
    cmSystemTools::ReplaceString(text, "\n", "\n" + indent);
    text.insert(0u, indent);
  }
  return text;
}

void ReportCheckResult(cm::string_view what, std::string result,
                       cmMakefile& mf)
{
  if (mf.GetCMakeInstance()->HasCheckInProgress()) {
    auto text = mf.GetCMakeInstance()->GetTopCheckInProgressMessage() + " - " +
      std::move(result);
    mf.DisplayStatus(IndentText(std::move(text), mf), -1);
  } else {
    mf.GetMessenger()->DisplayMessage(
      MessageType::WARNING, cmDiagnostics::CMD_AUTHOR,
      cmStrCat("Ignored "_s, what, " without CHECK_START"_s),
      mf.GetBacktrace());
  }
}

namespace {
#ifndef CMAKE_BOOTSTRAP
void WriteMessageEvent(cmConfigureLog& log, cmMakefile const& mf,
                       std::string const& message)
{
  // Keep in sync with cmFileAPIConfigureLog's DumpEventKindNames.
  static std::vector<unsigned int> const LogVersionsWithMessageV1{ 1 };

  if (log.IsAnyLogVersionEnabled(LogVersionsWithMessageV1)) {
    log.BeginEvent("message-v1", mf);
    log.WriteLiteralTextBlock("message"_s, message);
    log.EndEvent();
  }
}
#endif
}

} // anonymous namespace

// cmLibraryCommand
bool cmMessageCommand(std::vector<std::string> const& args,
                      cmExecutionStatus& status)
{
  if (args.empty()) {
    status.SetError("called with incorrect number of arguments");
    return false;
  }

  auto& mf = status.GetMakefile();

  auto i = args.cbegin();

  std::unique_ptr<cmMakefile::DiagnosticPushPop> ds;
  auto category = cmDiagnostics::CMD_NONE;
  auto type = MessageType::MESSAGE;
  auto fatal = false;
  auto level = Message::LogLevel::LOG_UNDEFINED;
  auto checkingType = Message::CheckType::UNDEFINED;
  if (*i == "SEND_ERROR") {
    type = MessageType::FATAL_ERROR;
    level = Message::LogLevel::LOG_ERROR;
    ++i;
  } else if (*i == "FATAL_ERROR") {
    fatal = true;
    type = MessageType::FATAL_ERROR;
    level = Message::LogLevel::LOG_ERROR;
    ++i;
  } else if (*i == "WARNING") {
    type = MessageType::WARNING;
    level = Message::LogLevel::LOG_WARNING;
    ++i;
  } else if (*i == "AUTHOR_WARNING") {
    category = cmDiagnostics::CMD_AUTHOR;
    switch (mf.GetDiagnosticAction(category)) {
      case cmDiagnostics::Ignore:
        return true;
      case cmDiagnostics::FatalError:
        fatal = true;
        CM_FALLTHROUGH;
      case cmDiagnostics::SendError:
        type = MessageType::FATAL_ERROR;
        level = Message::LogLevel::LOG_ERROR;
        break;
      default:
        type = MessageType::WARNING;
        level = Message::LogLevel::LOG_WARNING;
        break;
    }
    ++i;
  } else if (*i == "CHECK_START") {
    level = Message::LogLevel::LOG_STATUS;
    checkingType = Message::CheckType::CHECK_START;
    ++i;
  } else if (*i == "CHECK_PASS") {
    level = Message::LogLevel::LOG_STATUS;
    checkingType = Message::CheckType::CHECK_PASS;
    ++i;
  } else if (*i == "CHECK_FAIL") {
    level = Message::LogLevel::LOG_STATUS;
    checkingType = Message::CheckType::CHECK_FAIL;
    ++i;
  } else if (*i == "CONFIGURE_LOG") {
#ifndef CMAKE_BOOTSTRAP
    if (cmConfigureLog* log = mf.GetCMakeInstance()->GetConfigureLog()) {
      ++i;
      WriteMessageEvent(*log, mf, cmJoin(cmMakeRange(i, args.cend()), ""_s));
    }
#endif
    return true;
  } else if (*i == "STATUS") {
    level = Message::LogLevel::LOG_STATUS;
    ++i;
  } else if (*i == "VERBOSE") {
    level = Message::LogLevel::LOG_VERBOSE;
    ++i;
  } else if (*i == "DEBUG") {
    level = Message::LogLevel::LOG_DEBUG;
    ++i;
  } else if (*i == "TRACE") {
    level = Message::LogLevel::LOG_TRACE;
    ++i;
  } else if (*i == "DEPRECATION") {
    category = cmDiagnostics::CMD_DEPRECATED;

    cmPolicies::PolicyStatus const cmp0218 =
      mf.GetPolicyStatus(cmPolicies::CMP0218);
    if (cmp0218 != cmPolicies::NEW) {
      std::unique_ptr<cmMakefile::PolicyPushPop> ps;

      if (cmp0218 != cmPolicies::OLD) {
        // Suppress warnings about using old variables.
        ps = cm::make_unique<cmMakefile::PolicyPushPop>(&mf);
        mf.SetPolicy(cmPolicies::CMP0218, cmPolicies::OLD);
      }

      ds = cm::make_unique<cmMakefile::DiagnosticPushPop>(&mf);

      // Use old variables to determine diagnostic action.
      if (mf.IsOn("CMAKE_ERROR_DEPRECATED")) {
        mf.SetDiagnostic(category, cmDiagnostics::FatalError);
      } else {
        cmValue const warn = mf.GetDefinition("CMAKE_WARN_DEPRECATED");
        if (warn.IsSet() && !warn.IsOn()) {
          mf.SetDiagnostic(category, cmDiagnostics::Ignore);
        } else {
          mf.SetDiagnostic(category, cmDiagnostics::Warn);
        }
      }
    }

    switch (mf.GetDiagnosticAction(category)) {
      case cmDiagnostics::Ignore:
        return true;
      case cmDiagnostics::FatalError:
        fatal = true;
        CM_FALLTHROUGH;
      case cmDiagnostics::SendError:
        type = MessageType::FATAL_ERROR;
        level = Message::LogLevel::LOG_ERROR;
        break;
      default:
        type = MessageType::WARNING;
        level = Message::LogLevel::LOG_WARNING;
        break;
    }
    ++i;
  } else if (*i == "NOTICE") {
    // `NOTICE` message type is going to be output to stderr
    level = Message::LogLevel::LOG_NOTICE;
    ++i;
  } else {
    // Messages w/o any type are `NOTICE`s
    level = Message::LogLevel::LOG_NOTICE;
  }
  assert("Message log level expected to be set" &&
         level != Message::LogLevel::LOG_UNDEFINED);

  Message::LogLevel desiredLevel = mf.GetCurrentLogLevel();

  if (desiredLevel < level) {
    // Suppress the message
    return true;
  }

  auto message = cmJoin(cmMakeRange(i, args.cend()), "");

  switch (level) {
    case Message::LogLevel::LOG_ERROR:
    case Message::LogLevel::LOG_WARNING:
      // we've overridden the message type, above, so display it directly
      mf.GetMessenger()->DisplayMessage(type, category, message,
                                        mf.GetBacktrace());
      break;

    case Message::LogLevel::LOG_NOTICE:
      cmSystemTools::Message(IndentText(message, mf));
#ifdef CMake_ENABLE_DEBUGGER
      if (mf.GetCMakeInstance()->GetDebugAdapter()) {
        mf.GetCMakeInstance()->GetDebugAdapter()->OnMessageOutput(type,
                                                                  message);
      }
#endif
      break;

    case Message::LogLevel::LOG_STATUS:
      switch (checkingType) {
        case Message::CheckType::CHECK_START:
          mf.DisplayStatus(IndentText(message, mf), -1);
          mf.GetCMakeInstance()->PushCheckInProgressMessage(message);
          break;

        case Message::CheckType::CHECK_PASS:
          ReportCheckResult("CHECK_PASS"_s, message, mf);
          break;

        case Message::CheckType::CHECK_FAIL:
          ReportCheckResult("CHECK_FAIL"_s, message, mf);
          break;

        default:
          mf.DisplayStatus(IndentText(message, mf), -1);
          break;
      }
      break;

    case Message::LogLevel::LOG_VERBOSE:
    case Message::LogLevel::LOG_DEBUG:
    case Message::LogLevel::LOG_TRACE:
      mf.DisplayStatus(IndentText(message, mf), -1);
      break;

    default:
      assert("Unexpected log level! Review the `cmMessageCommand.cxx`." &&
             false);
      break;
  }

  if (fatal) {
    cmSystemTools::SetFatalErrorOccurred();
  }
  return true;
}
