//===-- lib/Parser/message.cpp --------------------------------------------===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//

#include "flang/Parser/message.h"
#include "flang/Common/idioms.h"
#include "flang/Parser/char-set.h"
#include "llvm/Support/raw_ostream.h"
#include <algorithm>
#include <cstdarg>
#include <cstddef>
#include <cstdio>
#include <cstring>
#include <string>
#include <vector>

namespace Fortran::parser {

llvm::raw_ostream &operator<<(llvm::raw_ostream &o, const MessageFixedText &t) {
  std::size_t n{t.text().size()};
  for (std::size_t j{0}; j < n; ++j) {
    o << t.text()[j];
  }
  return o;
}

void MessageFormattedText::Format(const MessageFixedText *text, ...) {
  const char *p{text->text().begin()};
  std::string asString;
  if (*text->text().end() != '\0') {
    // not NUL-terminated
    asString = text->text().NULTerminatedToString();
    p = asString.c_str();
  }
  va_list ap;
  va_start(ap, text);
#ifdef _MSC_VER
  // Microsoft has a separate function for "positional arguments", which is
  // used in some messages.
  int need{_vsprintf_p(nullptr, 0, p, ap)};
#else
  int need{vsnprintf(nullptr, 0, p, ap)};
#endif

  CHECK(need >= 0);
  char *buffer{
      static_cast<char *>(std::malloc(static_cast<std::size_t>(need) + 1))};
  CHECK(buffer);
  va_end(ap);
  va_start(ap, text);
#ifdef _MSC_VER
  // Use positional argument variant of printf.
  int need2{_vsprintf_p(buffer, need + 1, p, ap)};
#else
  int need2{vsnprintf(buffer, need + 1, p, ap)};
#endif
  CHECK(need2 == need);
  va_end(ap);
  string_ = buffer;
  std::free(buffer);
  conversions_.clear();
}

const char *MessageFormattedText::Convert(const std::string &s) {
  conversions_.emplace_front(s);
  return conversions_.front().c_str();
}

const char *MessageFormattedText::Convert(std::string &&s) {
  conversions_.emplace_front(std::move(s));
  return conversions_.front().c_str();
}

const char *MessageFormattedText::Convert(const std::string_view &s) {
  conversions_.emplace_front(s);
  return conversions_.front().c_str();
}

const char *MessageFormattedText::Convert(std::string_view &&s) {
  conversions_.emplace_front(s);
  return conversions_.front().c_str();
}

const char *MessageFormattedText::Convert(CharBlock x) {
  return Convert(x.ToString());
}

std::string MessageExpectedText::ToString() const {
  return common::visit(
      common::visitors{
          [](CharBlock cb) {
            return MessageFormattedText("expected '%s'"_err_en_US, cb)
                .MoveString();
          },
          [](const SetOfChars &set) {
            SetOfChars expect{set};
            if (expect.Has('\n')) {
              expect = expect.Difference('\n');
              if (expect.empty()) {
                return "expected end of line"_err_en_US.text().ToString();
              } else {
                std::string s{expect.ToString()};
                if (s.size() == 1) {
                  return MessageFormattedText(
                      "expected end of line or '%s'"_err_en_US, s)
                      .MoveString();
                } else {
                  return MessageFormattedText(
                      "expected end of line or one of '%s'"_err_en_US, s)
                      .MoveString();
                }
              }
            }
            std::string s{expect.ToString()};
            if (s.size() != 1) {
              return MessageFormattedText("expected one of '%s'"_err_en_US, s)
                  .MoveString();
            } else {
              return MessageFormattedText("expected '%s'"_err_en_US, s)
                  .MoveString();
            }
          },
      },
      u_);
}

bool MessageExpectedText::Merge(const MessageExpectedText &that) {
  return common::visit(common::visitors{
                           [](SetOfChars &s1, const SetOfChars &s2) {
                             s1 = s1.Union(s2);
                             return true;
                           },
                           [](const auto &, const auto &) { return false; },
                       },
      u_, that.u_);
}

bool Message::SortBefore(const Message &that) const {
  // Messages from prescanning have ProvenanceRange values for their locations,
  // while messages from later phases have CharBlock values, since the
  // conversion of cooked source stream locations to provenances is not
  // free and needs to be deferred, and many messages created during parsing
  // are speculative.  Messages with ProvenanceRange locations are ordered
  // before others for sorting.
  return common::visit(
      common::visitors{
          [](CharBlock cb1, CharBlock cb2) {
            return cb1.begin() < cb2.begin();
          },
          [](CharBlock, const ProvenanceRange &) { return false; },
          [](const ProvenanceRange &pr1, const ProvenanceRange &pr2) {
            return pr1.start() < pr2.start();
          },
          [](const ProvenanceRange &, CharBlock) { return true; },
      },
      location_, that.location_);
}

bool Message::IsFatal() const {
  return severity() == Severity::Error || severity() == Severity::Todo;
}

Severity Message::severity() const {
  return common::visit(
      common::visitors{
          [](const MessageExpectedText &) { return Severity::Error; },
          [](const MessageFixedText &x) { return x.severity(); },
          [](const MessageFormattedText &x) { return x.severity(); },
      },
      text_);
}

Message &Message::set_severity(Severity severity) {
  common::visit(
      common::visitors{
          [](const MessageExpectedText &) {},
          [severity](MessageFixedText &x) { x.set_severity(severity); },
          [severity](MessageFormattedText &x) { x.set_severity(severity); },
      },
      text_);
  return *this;
}

std::string Message::ToString() const {
  return common::visit(
      common::visitors{
          [](const MessageFixedText &t) {
            return t.text().NULTerminatedToString();
          },
          [](const MessageFormattedText &t) { return t.string(); },
          [](const MessageExpectedText &e) { return e.ToString(); },
      },
      text_);
}

void Message::ResolveProvenances(const AllCookedSources &allCooked) {
  if (CharBlock * cb{std::get_if<CharBlock>(&location_)}) {
    if (std::optional<ProvenanceRange> resolved{
            allCooked.GetProvenanceRange(*cb)}) {
      location_ = *resolved;
    }
  }
  if (Message * attachment{attachment_.get()}) {
    attachment->ResolveProvenances(allCooked);
  }
}

std::optional<ProvenanceRange> Message::GetProvenanceRange(
    const AllCookedSources &allCooked) const {
  return common::visit(
      common::visitors{
          [&](CharBlock cb) { return allCooked.GetProvenanceRange(cb); },
          [](const ProvenanceRange &pr) { return std::make_optional(pr); },
      },
      location_);
}

static std::string Prefix(Severity severity) {
  switch (severity) {
  case Severity::Error:
    return "error: ";
  case Severity::Warning:
    return "warning: ";
  case Severity::Portability:
    return "portability: ";
  case Severity::Because:
    return "because: ";
  case Severity::Context:
    return "in the context: ";
  case Severity::Todo:
    return "error: not yet implemented: ";
  case Severity::None:
    break;
  }
  return "";
}

static llvm::raw_ostream::Colors PrefixColor(Severity severity) {
  switch (severity) {
  case Severity::Error:
  case Severity::Todo:
    return llvm::raw_ostream::RED;
  case Severity::Warning:
  case Severity::Portability:
    return llvm::raw_ostream::MAGENTA;
  default:
    // TODO: Set the color.
    break;
  }
  return llvm::raw_ostream::SAVEDCOLOR;
}

void Message::Emit(llvm::raw_ostream &o, const AllCookedSources &allCooked,
    bool echoSourceLine) const {
  std::optional<ProvenanceRange> provenanceRange{GetProvenanceRange(allCooked)};
  const AllSources &sources{allCooked.allSources()};
  sources.EmitMessage(o, provenanceRange, ToString(), Prefix(severity()),
      PrefixColor(severity()), echoSourceLine);
  bool isContext{attachmentIsContext_};
  for (const Message *attachment{attachment_.get()}; attachment;
       attachment = attachment->attachment_.get()) {
    Severity severity = isContext ? Severity::Context : attachment->severity();
    sources.EmitMessage(o, attachment->GetProvenanceRange(allCooked),
        attachment->ToString(), Prefix(severity), PrefixColor(severity),
        echoSourceLine);
  }
}

// Messages are equal if they're for the same location and text, and the user
// visible aspects of their attachments are the same
bool Message::operator==(const Message &that) const {
  if (!AtSameLocation(that) || ToString() != that.ToString() ||
      severity() != that.severity() ||
      attachmentIsContext_ != that.attachmentIsContext_) {
    return false;
  }
  const Message *thatAttachment{that.attachment_.get()};
  for (const Message *attachment{attachment_.get()}; attachment;
       attachment = attachment->attachment_.get()) {
    if (!thatAttachment || !attachment->AtSameLocation(*thatAttachment) ||
        attachment->ToString() != thatAttachment->ToString() ||
        attachment->severity() != thatAttachment->severity()) {
      return false;
    }
    thatAttachment = thatAttachment->attachment_.get();
  }
  return !thatAttachment;
}

bool Message::Merge(const Message &that) {
  return AtSameLocation(that) &&
      (!that.attachment_.get() ||
          attachment_.get() == that.attachment_.get()) &&
      common::visit(
          common::visitors{
              [](MessageExpectedText &e1, const MessageExpectedText &e2) {
                return e1.Merge(e2);
              },
              [](const auto &, const auto &) { return false; },
          },
          text_, that.text_);
}

Message &Message::Attach(Message *m) {
  if (!attachment_) {
    attachment_ = m;
  } else {
    if (attachment_->references() > 1) {
      // Don't attach to a shared context attachment; copy it first.
      attachment_ = new Message{*attachment_};
    }
    attachment_->Attach(m);
  }
  return *this;
}

Message &Message::Attach(std::unique_ptr<Message> &&m) {
  return Attach(m.release());
}

bool Message::AtSameLocation(const Message &that) const {
  return common::visit(
      common::visitors{
          [](CharBlock cb1, CharBlock cb2) {
            return cb1.begin() == cb2.begin();
          },
          [](const ProvenanceRange &pr1, const ProvenanceRange &pr2) {
            return pr1.start() == pr2.start();
          },
          [](const auto &, const auto &) { return false; },
      },
      location_, that.location_);
}

bool Messages::Merge(const Message &msg) {
  if (msg.IsMergeable()) {
    for (auto &m : messages_) {
      if (m.Merge(msg)) {
        return true;
      }
    }
  }
  return false;
}

void Messages::Merge(Messages &&that) {
  if (messages_.empty()) {
    *this = std::move(that);
  } else {
    while (!that.messages_.empty()) {
      if (Merge(that.messages_.front())) {
        that.messages_.pop_front();
      } else {
        auto next{that.messages_.begin()};
        ++next;
        messages_.splice(
            messages_.end(), that.messages_, that.messages_.begin(), next);
      }
    }
  }
}

void Messages::Copy(const Messages &that) {
  for (const Message &m : that.messages_) {
    Message copy{m};
    Say(std::move(copy));
  }
}

void Messages::ResolveProvenances(const AllCookedSources &allCooked) {
  for (Message &m : messages_) {
    m.ResolveProvenances(allCooked);
  }
}

void Messages::Emit(llvm::raw_ostream &o, const AllCookedSources &allCooked,
    bool echoSourceLines) const {
  std::vector<const Message *> sorted;
  for (const auto &msg : messages_) {
    sorted.push_back(&msg);
  }
  std::stable_sort(sorted.begin(), sorted.end(),
      [](const Message *x, const Message *y) { return x->SortBefore(*y); });
  const Message *lastMsg{nullptr};
  for (const Message *msg : sorted) {
    if (lastMsg && *msg == *lastMsg) {
      // Don't emit two identical messages for the same location
      continue;
    }
    msg->Emit(o, allCooked, echoSourceLines);
    lastMsg = msg;
  }
}

void Messages::AttachTo(Message &msg, std::optional<Severity> severity) {
  for (Message &m : messages_) {
    Message m2{std::move(m)};
    if (severity) {
      m2.set_severity(*severity);
    }
    msg.Attach(std::move(m2));
  }
  messages_.clear();
}

bool Messages::AnyFatalError() const {
  for (const auto &msg : messages_) {
    if (msg.IsFatal()) {
      return true;
    }
  }
  return false;
}
} // namespace Fortran::parser
