// has to go first as it violates our poisons
#include "rang.hpp"

#include "absl/strings/escaping.h"
#include "absl/strings/match.h"
#include "absl/strings/str_replace.h"
#include "core/Context.h"
#include "core/Error.h"
#include "core/ErrorQueue.h"
#include "core/GlobalState.h"
#include "core/errors/errors.h"
#include "core/lsp/QueryResponse.h"
#include <algorithm>
#include <sstream>

template class std::unique_ptr<sorbet::core::Error>;
namespace sorbet::core {

using namespace std;

namespace {

constexpr auto ERROR_COLOR = rang::fg::red;
constexpr auto SUCCESS_COLOR = rang::fg::green;
constexpr auto LOW_NOISE_COLOR = rang::fgB::black;
constexpr auto DETAIL_COLOR = rang::fg::yellow;
constexpr auto RESET_COLOR = rang::fg::reset;

constexpr auto INTERPOLATION_COLOR = rang::fg::cyan;

constexpr auto FILE_POS_STYLE = rang::style::underline;
constexpr auto RESET_STYLE = rang::style::reset;

/* used to indicate that previous color should be restored
 * You need to reset color to "previous color that was there", not white.
 * Otherwise you'll cancel color that was wrapping you.
 */
constexpr string_view REVERT_COLOR_SIGIL = "@@<<``\t\v\b\r\aYOU_FORGOT_TO_CALL restoreColors"sv;

string _replaceAll(string_view inWhat, string_view from, string_view to) {
    return absl::StrReplaceAll(inWhat, {{from, to}});
}

string prettyPrintEditReplacement(string_view it) {
    return _replaceAll(absl::StripAsciiWhitespace(it), "\n", "\n    ");
}

} // namespace

string ErrorColors::replaceAll(string_view inWhat, string_view from, string_view to) {
    return _replaceAll(inWhat, from, to);
}

bool Error::isCritical() const {
    return this->what == core::errors::Internal::InternalError;
}

string restoreColors(string_view formatted, rang::fg color) {
    stringstream buf;
    buf << color;
    return _replaceAll(formatted, REVERT_COLOR_SIGIL, buf.str());
}

string ErrorLine::toString(const GlobalState &gs, bool color) const {
    stringstream buf;
    string indent = "  ";
    buf << indent;
    switch (this->displayLoc) {
        case LocDisplay::Shown:
            buf << FILE_POS_STYLE << loc.filePosToString(gs) << RESET_STYLE << ":";
            break;
        case LocDisplay::Hidden:
            break;
    }
    if (!formattedMessage.empty()) {
        switch (this->displayLoc) {
            case LocDisplay::Shown:
                buf << " ";
                break;
            case LocDisplay::Hidden:
                break;
        }
        if (color) {
            buf << DETAIL_COLOR << restoreColors(formattedMessage, DETAIL_COLOR) << RESET_COLOR;
        } else {
            buf << restoreColors(formattedMessage, RESET_COLOR);
        }
    }

    if (loc.exists()) {
        auto fileLength = loc.file().data(gs).source().size();
        if (loc.beginPos() > fileLength || loc.endPos() > fileLength) {
            fatalLogger->error(R"(msg="Bad ErrorLine::toString loc" path="{}" loc="{}" formattedMessage={}")",
                               absl::CEscape(loc.file().data(gs).path()), loc.showRaw(gs), formattedMessage);
            fatalLogger->error("source=\"{}\"", absl::CEscape(loc.file().data(gs).source()));
            ENFORCE(false);
        }
        buf << '\n' << loc.toStringWithTabs(gs, 2);
    }
    return buf.str();
}

string ErrorSection::toString(const GlobalState &gs) const {
    stringstream buf;
    string indent = "  ";
    bool coloredLineHeaders = true;
    bool skipEOL = false;
    if (!this->header.empty()) {
        coloredLineHeaders = false;
        string formattedHeader;
        if (this->isAutocorrectDescription) {
            if (gs.autocorrect) {
                formattedHeader = fmt::format("{} Done", this->header);
            } else {
                formattedHeader = ErrorColors::format("{} Use `{}` to autocorrect", this->header, "-a");
            }
        } else {
            formattedHeader = this->header;
        }
        auto color = gs.autocorrect ? SUCCESS_COLOR : DETAIL_COLOR;
        buf << indent << color << restoreColors(formattedHeader, color) << RESET_COLOR;
    } else {
        skipEOL = true;
    }
    for (auto &line : this->messages) {
        if (!skipEOL) {
            buf << '\n';
        }
        skipEOL = false;
        buf << indent << line.toString(gs, coloredLineHeaders);
    }
    return buf.str();
}

void ErrorSection::Collector::addErrorDetails(ErrorSection::Collector &&e) {
    children.push_back(std::move(e));
}

void toErrorSectionHelper(const ErrorSection::Collector &e, vector<ErrorLine> &result, int indentLevel) {
    ENFORCE(e.message.length() > 0);
    string message = fmt::format("{}{}", string(indentLevel * 2, ' '), e.message);
    result.push_back(ErrorLine(core::Loc::none(), move(message), ErrorLine::LocDisplay::Hidden));
    for (auto &c : e.children) {
        toErrorSectionHelper(c, result, indentLevel + 1);
    }
}

optional<ErrorSection> ErrorSection::Collector::toErrorSection() const {
    if (children.size() == 0) {
        return nullopt;
    }
    vector<ErrorLine> lines;
    for (auto &c : children) {
        toErrorSectionHelper(c, lines, 0);
    }
    return ErrorSection("Detailed explanation:", move(lines));
}

string Error::toString(const GlobalState &gs) const {
    stringstream buf;
    buf << RESET_STYLE;

    if (gs.packageDB().enabled() && gs.packageDB().packageAttributedErrors()) {
        auto pkgName = "<none>"s;
        if (loc.file().exists() && !loc.file().isPackage(gs)) {
            // If there are errors in `__package.rb` files themselves, it's not clear whether we
            // have a fully-functional package graph or whether we've somehow recovered from an
            // error, giving a false appearance that we do. Let's be defensive, and say that errors
            // in __package.rb files equate to "unpackaged" (`<none>`) errors.
            auto pkg = gs.packageDB().getPackageNameForFile(loc.file());
            if (pkg.exists()) {
                pkgName = pkg.owner.show(gs);
            }
        }

        buf << "[" << pkgName << "] ";
    }

    // Sorbet was called with -a, and this error has autocorrects for it. In that case, let's not render the error
    // header in red, so the user can focus on the errors that don't have an autocorrect associated with them.
    // TODO(neil): We might also want to reorder how the error and autocorrect is presented to make it even more
    // obvious. For example, showing the autocorrect at the top level and nesting the error underneath.
    auto autocorrectApplied = gs.autocorrect && !autocorrects.empty();
    buf << FILE_POS_STYLE << loc.filePosToString(gs) << RESET_STYLE << ": " << ERROR_COLOR
        << restoreColors(header, autocorrectApplied ? RESET_COLOR : ERROR_COLOR) << RESET_COLOR << LOW_NOISE_COLOR
        << " " << gs.errorUrlBase << what.code << RESET_COLOR;
    if (loc.exists()) {
        auto fileLength = loc.file().data(gs).source().size();
        if (loc.beginPos() > fileLength || loc.endPos() > fileLength) {
            fatalLogger->error(R"(msg="Bad Error::toString loc" path="{}" loc="{}" header={}")",
                               absl::CEscape(loc.file().data(gs).path()), loc.showRaw(gs), header);
            fatalLogger->error("source=\"{}\"", absl::CEscape(loc.file().data(gs).source()));
            ENFORCE(false);
        }
        buf << '\n' << loc.toStringWithTabs(gs, 2);
    }

    if (gs.includeErrorSections) {
        for (auto &section : this->sections) {
            buf << '\n' << section.toString(gs);
        }
    }

    return buf.str();
}

ErrorBuilder::ErrorBuilder(const GlobalState &gs, bool willBuild, Loc loc, ErrorClass what)
    : gs(gs), state(willBuild ? State::WillBuild : State::Unreported), loc(loc), what(what) {
    ENFORCE(willBuild || what.minLevel != StrictLevel::Internal);
}

void ErrorBuilder::_setHeader(string &&header) {
    ENFORCE(state == State::WillBuild);
    this->header = move(header);
}

void ErrorBuilder::addErrorSection(optional<ErrorSection> &&section) {
    ENFORCE(state == State::WillBuild);
    if (section.has_value()) {
        this->sections.emplace_back(move(*section));
    }
}

void ErrorBuilder::addErrorSections(const ErrorSection::Collector &&errorDetailsCollector) {
    if (auto errorSection = errorDetailsCollector.toErrorSection()) {
        addErrorSection(move(errorSection.value()));
    }
}

void ErrorBuilder::addAutocorrect(AutocorrectSuggestion &&autocorrect) {
    ENFORCE(state == State::WillBuild);
    string sectionTitle;
    if (gs.autocorrect) {
        sectionTitle = "Autocorrect:";
    } else if (autocorrect.isDidYouMean && autocorrect.edits.size() == 1) {
        sectionTitle = ErrorColors::format("Did you mean `{}`?", autocorrect.edits[0].replacement);
    } else {
        sectionTitle = "Autocorrect:";
    }

    vector<ErrorLine> messages;
    if (!autocorrect.hideEdit) {
        for (auto &edit : autocorrect.edits) {
            auto isDeletion = edit.replacement == "";
            uint32_t n = edit.loc.length();
            auto locWithoutLeadingNewLine = edit.loc;
            auto source = locWithoutLeadingNewLine.source(gs);
            uint32_t leadingNewLines = 0;
            if (source.has_value()) {
                auto sv = source.value();
                while (leadingNewLines < sv.size() && sv[leadingNewLines] == '\n') {
                    leadingNewLines++;
                }
            }
            if (leadingNewLines > 0) {
                locWithoutLeadingNewLine = locWithoutLeadingNewLine.adjust(gs, leadingNewLines, 0);
            }
            if (gs.autocorrect) {
                auto line = isDeletion ? ErrorLine::from(locWithoutLeadingNewLine, "Deleted")
                                       : ErrorLine::from(locWithoutLeadingNewLine, "{} `{}`",
                                                         n == 0 ? "Inserted" : "Replaced with",
                                                         prettyPrintEditReplacement(edit.replacement));

                messages.emplace_back(std::move(line));
            } else {
                auto line = isDeletion ? ErrorLine::from(locWithoutLeadingNewLine, "Delete")
                                       : ErrorLine::from(locWithoutLeadingNewLine, "{} `{}`",
                                                         n == 0 ? "Insert" : "Replace with",
                                                         prettyPrintEditReplacement(edit.replacement));

                messages.emplace_back(std::move(line));
            }
        }
    }
    auto errorSection = ErrorSection{sectionTitle, std::move(messages)};
    errorSection.isAutocorrectDescription = true;
    errorSection.isDidYouMean = autocorrect.isDidYouMean;
    addErrorSection(move(errorSection));
    this->autocorrects.emplace_back(move(autocorrect));
}

void ErrorBuilder::maybeAddAutocorrect(std::optional<AutocorrectSuggestion> &&ma) {
    if (!ma.has_value()) {
        return;
    }

    addAutocorrect(std::move(ma.value()));
}

void ErrorBuilder::didYouMean(const string &replacement, Loc loc) {
    if (!gs.didYouMean) {
        return;
    }

    string formatted = fmt::format("Replace with `{}`", replacement);
    auto isDidYouMean = true;
    addAutocorrect(
        AutocorrectSuggestion{move(formatted), {AutocorrectSuggestion::Edit{loc, replacement}}, isDidYouMean});
}

// This will sometimes be bypassed in lieu of just calling build() so put your
// logic in build() instead.
ErrorBuilder::~ErrorBuilder() {
    if (state == State::DidBuild) {
        return;
    }
    this->gs._error(build());
}

unique_ptr<Error> ErrorBuilder::build() {
    ENFORCE(state != State::DidBuild);
    bool isSilenced = state == State::Unreported;
    state = State::DidBuild;

    unique_ptr<Error> err = make_unique<Error>(this->loc, this->what, move(this->header), move(this->sections),
                                               move(this->autocorrects), isSilenced);
    return err;
}

string ErrorColors::coloredPatternReplace = string(coloredPatternSigil);

void ErrorColors::enableColors() {
    rang::setControlMode(rang::control::Force);
    stringstream buf;
    buf << INTERPOLATION_COLOR << "{}" << REVERT_COLOR_SIGIL;
    coloredPatternReplace = buf.str();
}
void ErrorColors::disableColors() {
    coloredPatternReplace = coloredPatternSigil;
    rang::setControlMode(rang::control::Off);
}

} // namespace sorbet::core
