#include "Instructions.h"

#include "cfg/CFG.h"
#include "common/strings/formatting.h"
#include "common/typecase.h"
#include "core/Names.h"
#include "core/TypeConstraint.h"
#include <memory>
#include <utility>

using namespace std;

namespace sorbet::cfg {

namespace {
string spacesForTabLevel(int tabs) {
    fmt::memory_buffer ss;
    for (int i = 0; i < tabs; i++) {
        fmt::format_to(std::back_inserter(ss), "&nbsp;");
    }
    return to_string(ss);
}
} // namespace

#define CASE_STATEMENT(CASE_BODY, T) \
    case Tag::T: {                   \
        CASE_BODY(T)                 \
        break;                       \
    }

#define GENERATE_TAG_SWITCH(tag, body)            \
    switch (tag) {                                \
        CASE_STATEMENT(body, Ident)               \
        CASE_STATEMENT(body, Alias)               \
        CASE_STATEMENT(body, SolveConstraint)     \
        CASE_STATEMENT(body, Send)                \
        CASE_STATEMENT(body, Return)              \
        CASE_STATEMENT(body, BlockReturn)         \
        CASE_STATEMENT(body, LoadSelf)            \
        CASE_STATEMENT(body, Literal)             \
        CASE_STATEMENT(body, GetCurrentException) \
        CASE_STATEMENT(body, LoadArg)             \
        CASE_STATEMENT(body, ArgPresent)          \
        CASE_STATEMENT(body, LoadYieldParams)     \
        CASE_STATEMENT(body, YieldParamPresent)   \
        CASE_STATEMENT(body, YieldLoadArg)        \
        CASE_STATEMENT(body, Cast)                \
        CASE_STATEMENT(body, TAbsurd)             \
        CASE_STATEMENT(body, KeepAlive)           \
    }

string InstructionPtr::toString(const core::GlobalState &gs, const CFG &cfg) const {
    auto *ptr = get();

#define TO_STRING(name) return static_cast<const name *>(ptr)->toString(gs, cfg);
    GENERATE_TAG_SWITCH(tag(), TO_STRING)
#undef TO_STRING
}

string InstructionPtr::showRaw(const core::GlobalState &gs, const CFG &cfg, int tabs) const {
    auto *ptr = get();

#define SHOW_RAW(name) return static_cast<const name *>(ptr)->showRaw(gs, cfg, tabs);
    GENERATE_TAG_SWITCH(tag(), SHOW_RAW)
#undef SHOW_RAW
}

void InstructionPtr::deleteTagged(Tag tag, void *expr) noexcept {
    ENFORCE(expr != nullptr);

#define DELETE_INSN(name) delete static_cast<name *>(expr);
    GENERATE_TAG_SWITCH(tag, DELETE_INSN)
#undef DELETE_INSN
}

Return::Return(LocalRef what, core::LocOffsets whatLoc) : what(what), whatLoc(whatLoc) {
    categoryCounterInc("cfg", "return");
}

string SolveConstraint::toString(const core::GlobalState &gs, const CFG &cfg) const {
    return fmt::format("Solve<{}, {}>", this->send.toString(gs, cfg), this->link.data(cfg)->fun.toString(gs));
}

string SolveConstraint::showRaw(const core::GlobalState &gs, const CFG &cfg, int tabs) const {
    return fmt::format("Solve {{ send = {}, link = {} }}", this->send.toString(gs, cfg),
                       this->link.data(cfg)->fun.showRaw(gs));
}

string Return::toString(const core::GlobalState &gs, const CFG &cfg) const {
    return fmt::format("return {}", this->what.toString(gs, cfg));
}

string Return::showRaw(const core::GlobalState &gs, const CFG &cfg, int tabs) const {
    return fmt::format("Return {{\n{0}&nbsp;what = {1},\n{0}}}", spacesForTabLevel(tabs),
                       this->what.showRaw(gs, cfg, tabs + 1));
}

BlockReturn::BlockReturn(LinkRef link, LocalRef what) : link(link), what(what) {
    categoryCounterInc("cfg", "blockreturn");
}

string BlockReturn::toString(const core::GlobalState &gs, const CFG &cfg) const {
    return fmt::format("blockreturn<{}> {}", this->link.data(cfg)->fun.toString(gs), this->what.toString(gs, cfg));
}

string BlockReturn::showRaw(const core::GlobalState &gs, const CFG &cfg, int tabs) const {
    return fmt::format("BlockReturn {{\n{0}&nbsp;link = {1},\n{0}&nbsp;what = {2},\n{0}}}", spacesForTabLevel(tabs),
                       this->link.data(cfg)->fun.showRaw(gs), this->what.showRaw(gs, cfg, tabs + 1));
}

LoadSelf::LoadSelf(LinkRef link, LocalRef fallback) : fallback(fallback), link(link) {
    categoryCounterInc("cfg", "loadself");
}

string LoadSelf::toString(const core::GlobalState &gs, const CFG &cfg) const {
    return fmt::format("loadSelf({})", this->link.data(cfg)->fun.toString(gs));
}

string LoadSelf::showRaw(const core::GlobalState &gs, const CFG &cfg, int tabs) const {
    return fmt::format("LoadSelf {{ link = {} }}", this->link.data(cfg)->fun.showRaw(gs));
}

Send::SendInitializer::SendInitializer(Send *snd) : snd(snd), refs(snd->argRefs()), locs(snd->argLocs()) {}

Send::SendInitializer Send::make(LocalRef recv, core::LocOffsets receiverLoc, core::NameRef fun,
                                 core::LocOffsets funLoc, uint16_t numPosArgs, bool isPrivateOk, uint32_t numArgs) {
    size_t totalSize = Parent::totalSizeToAlloc<LocalRef, core::TypePtr, core::LocOffsets>(numArgs, numArgs, numArgs);
    void *p = ::operator new(totalSize);
    Send *snd = new (p) Send(recv, receiverLoc, fun, funLoc, numPosArgs, isPrivateOk, numArgs);
    // Go ahead and initialize all the TypePtrs; we will assign real values in infer.
    for (auto &t : snd->argTypes()) {
        new (&t) core::TypePtr();
    }
    return SendInitializer(snd);
}

Send::Send(LocalRef recv, core::LocOffsets receiverLoc, core::NameRef fun, core::LocOffsets funLoc, uint16_t numPosArgs,
           bool isPrivateOk, uint32_t numArgs)
    : isPrivateOk(isPrivateOk), numPosArgs(numPosArgs), fun(fun), recv(recv), funLoc(funLoc), receiverLoc(receiverLoc),
      numArgs(numArgs) {
    ENFORCE(numPosArgs <= numArgs, "Expected {} positional arguments, but only have {} args", numPosArgs, numArgs);

    categoryCounterInc("cfg", "send");
    histogramInc("cfg.send.args", numArgs);
}

namespace {
template <typename T> void destroySpan(absl::Span<T> span) {
    destroy_n(span.begin(), span.size());
}
} // namespace

Send::~Send() {
    destroySpan(argRefs());
    destroySpan(argTypes());
    destroySpan(argLocs());
}

core::LocOffsets Send::locWithoutBlock(core::LocOffsets bindLoc) {
    if (!this->link.exists()) {
        // This location is slightly better, because it will include the last `)` if that exists,
        // which means that queries for things like
        //
        //     foo()
        //     #   ^ completion: ...
        //
        // will match.
        return bindLoc;
    }

    if (!this->argLocs().empty()) {
        // For sig, the arg will often be a reference to an implicit self,
        // so the back of argLocs is a zero width loc.
        return this->receiverLoc.join(this->funLoc).join(this->argLocs().back());
    }

    return this->receiverLoc.join(this->funLoc);
}

Literal::Literal(const core::TypePtr &value) : value(move(value)) {
    categoryCounterInc("cfg", "literal");
}

string Literal::toString(const core::GlobalState &gs, const CFG &cfg) const {
    string res;
    typecase(
        this->value, [&](const core::NamedLiteralType &l) { res = l.showValue(gs); },
        [&](const core::IntegerLiteralType &i) { res = i.showValue(gs); },
        [&](const core::FloatLiteralType &i) { res = i.showValue(gs); },
        [&](const core::ClassType &l) {
            if (l.symbol == core::Symbols::NilClass()) {
                res = "nil";
            } else if (l.symbol == core::Symbols::FalseClass()) {
                res = "false";
            } else if (l.symbol == core::Symbols::TrueClass()) {
                res = "true";
            } else {
                res = fmt::format("literal({})", this->value.toStringWithTabs(gs, 0));
            }
        },
        [&](const core::TypePtr &t) { res = fmt::format("literal({})", this->value.toStringWithTabs(gs, 0)); });
    return res;
}

string Literal::showRaw(const core::GlobalState &gs, const CFG &cfg, int tabs) const {
    return fmt::format("Literal {{ value = {} }}", this->value.show(gs));
}

Ident::Ident(LocalRef what) : what(what) {
    categoryCounterInc("cfg", "ident");
}

Alias::Alias(core::SymbolRef what, core::NameRef name) : what(what), name(name) {
    // what == undeclaredFieldStub -> name.exists()
    ENFORCE(what != core::Symbols::Magic_undeclaredFieldStub() || name.exists(),
            "Missing name for undeclared field alias!");

    // name.exists() -> what == undeclaredFieldStub()
    ENFORCE(!name.exists() || what == core::Symbols::Magic_undeclaredFieldStub(), "Name provided for known alias!");
    categoryCounterInc("cfg", "alias");
}

string Ident::toString(const core::GlobalState &gs, const CFG &cfg) const {
    return this->what.toString(gs, cfg);
}

string Ident::showRaw(const core::GlobalState &gs, const CFG &cfg, int tabs) const {
    return fmt::format("Ident {{\n{0}&nbsp;what = {1},\n{0}}}", spacesForTabLevel(tabs), this->what.showRaw(gs, cfg));
}

string Alias::toString(const core::GlobalState &gs, const CFG &cfg) const {
    if (name.exists()) {
        return fmt::format("alias {} ({})", this->what.name(gs).toString(gs), name.toString(gs));
    } else {
        return fmt::format("alias {}", this->what.name(gs).toString(gs));
    }
}

string Alias::showRaw(const core::GlobalState &gs, const CFG &cfg, int tabs) const {
    return fmt::format("Alias {{ what = {} }}", this->what.show(gs));
}

string Send::toString(const core::GlobalState &gs, const CFG &cfg) const {
    return fmt::format("{}.{}({})", this->recv.toString(gs, cfg), this->fun.toString(gs),
                       fmt::map_join(this->argSpan(), ", ", [&](const auto &arg) -> string {
                           VariableUseSite site(arg.key, arg.value);
                           return site.toString(gs, cfg);
                       }));
}

string Send::showRaw(const core::GlobalState &gs, const CFG &cfg, int tabs) const {
    return fmt::format("Send {{\n{0}&nbsp;recv = {1},\n{0}&nbsp;fun = {2},\n{0}&nbsp;args = ({3}),\n{0}}}",
                       spacesForTabLevel(tabs), this->recv.toString(gs, cfg), this->fun.showRaw(gs),
                       fmt::map_join(this->argSpan(), ", ", [&](const auto &arg) -> string {
                           VariableUseSite site(arg.key, arg.value);
                           return site.showRaw(gs, cfg, tabs + 1);
                       }));
}

string LoadArg::toString(const core::GlobalState &gs, const CFG &cfg) const {
    return fmt::format("load_arg({})", this->argument(gs).parameterName(gs));
}

string LoadArg::showRaw(const core::GlobalState &gs, const CFG &cfg, int tabs) const {
    return fmt::format("LoadArg {{ argument = {} }}", this->argument(gs).parameterName(gs));
}

const core::ParamInfo &LoadArg::argument(const core::GlobalState &gs) const {
    return this->method.data(gs)->parameters[this->argId];
}

string ArgPresent::toString(const core::GlobalState &gs, const CFG &cfg) const {
    return fmt::format("arg_present({})", this->argument(gs).parameterName(gs));
}

string ArgPresent::showRaw(const core::GlobalState &gs, const CFG &cfg, int tabs) const {
    return fmt::format("ArgPresent {{ argument = {} }}", this->argument(gs).parameterName(gs));
}

const core::ParamInfo &ArgPresent::argument(const core::GlobalState &gs) const {
    return this->method.data(gs)->parameters[this->argId];
}

string LoadYieldParams::toString(const core::GlobalState &gs, const CFG &cfg) const {
    return fmt::format("load_yield_params({})", this->link.data(cfg)->fun.toString(gs));
}

string LoadYieldParams::showRaw(const core::GlobalState &gs, const CFG &cfg, int tabs) const {
    return fmt::format("LoadYieldParams {{ link = {0} }}", this->link.data(cfg)->fun.showRaw(gs));
}

string YieldParamPresent::toString(const core::GlobalState &gs, const CFG &cfg) const {
    return fmt::format("yield_param_present({})", this->argId);
}

string YieldParamPresent::showRaw(const core::GlobalState &gs, const CFG &cfg, int tabs) const {
    return fmt::format("YieldParamPresent {{ argId = {} }}", this->argId);
}

string YieldLoadArg::toString(const core::GlobalState &gs, const CFG &cfg) const {
    return fmt::format("yield_load_arg({}, {})", this->argId, this->yieldParam.toString(gs, cfg));
}

string YieldLoadArg::showRaw(const core::GlobalState &gs, const CFG &cfg, int tabs) const {
    return fmt::format("YieldLoadArg {{ argId = {}, yieldParam = {} }}", this->argId,
                       this->yieldParam.showRaw(gs, cfg));
}

string GetCurrentException::toString(const core::GlobalState &gs, const CFG &cfg) const {
    return "<get-current-exception>";
}

string GetCurrentException::showRaw(const core::GlobalState &gs, const CFG &cfg, int tabs) const {
    return fmt::format("GetCurrentException {{}}", spacesForTabLevel(tabs));
}

string Cast::toString(const core::GlobalState &gs, const CFG &cfg) const {
    return fmt::format("cast({}, {});", this->value.toString(gs, cfg), this->type.show(gs));
}

string Cast::showRaw(const core::GlobalState &gs, const CFG &cfg, int tabs) const {
    return fmt::format("Cast {{\n{0}&nbsp;cast = T.{1},\n{0}&nbsp;value = {2},\n{0}&nbsp;type = {3},\n{0}}}",
                       spacesForTabLevel(tabs), this->cast.show(gs), this->value.showRaw(gs, cfg, tabs + 1),
                       this->type.show(gs));
}

string TAbsurd::toString(const core::GlobalState &gs, const CFG &cfg) const {
    return fmt::format("T.absurd({})", this->what.toString(gs, cfg));
}

string TAbsurd::showRaw(const core::GlobalState &gs, const CFG &cfg, int tabs) const {
    return fmt::format("TAbsurd {{\n{0}&nbsp;what = {1},\n{0}}}", spacesForTabLevel(tabs),
                       this->what.showRaw(gs, cfg, tabs + 1));
}

string KeepAlive::toString(const core::GlobalState &gs, const CFG &cfg) const {
    return fmt::format("<keep-alive> {}", this->what.toString(gs, cfg));
}

string KeepAlive::showRaw(const core::GlobalState &gs, const CFG &cfg, int tabs) const {
    return fmt::format("KeepAlive {{\n{0}&nbsp;what = {1},\n{0}}}", spacesForTabLevel(tabs),
                       this->what.showRaw(gs, cfg));
}

string VariableUseSite::toString(const core::GlobalState &gs, const CFG &cfg) const {
    if (this->variable == LocalRef::unconditional() || this->type == nullptr) {
        return this->variable.toString(gs, cfg);
    } else {
        return fmt::format("{}: {}", this->variable.toString(gs, cfg), this->type.show(gs));
    }
}

string VariableUseSite::showRaw(const core::GlobalState &gs, const CFG &cfg, int tabs) const {
    if (this->variable == LocalRef::unconditional() || this->type == nullptr) {
        return fmt::format("VariableUseSite {{ variable = {} }}", this->variable.showRaw(gs, cfg));
    } else {
        return fmt::format("VariableUseSite {{\n{0}&nbsp;variable = {1},\n{0}&nbsp;type = {2},\n{0}}}",
                           spacesForTabLevel(tabs), this->variable.showRaw(gs, cfg), this->type.show(gs));
    }
}

} // namespace sorbet::cfg
