#include "core/Names.h"
#include "core/Context.h"
#include "core/GlobalState.h"
#include "core/NameSubstitution.h"
#include "core/Names.h"
#include "core/hashing/hashing.h"
#include <numeric> // accumulate

#include "absl/strings/escaping.h"
#include "absl/strings/str_cat.h"
#include "absl/strings/str_join.h"
#include "absl/strings/str_split.h"

using namespace std;

namespace sorbet::core {

static_assert(is_nothrow_constructible_v<NameRef>);
static_assert(is_nothrow_default_constructible_v<NameRef>);
static_assert(is_nothrow_copy_constructible_v<NameRef>);
static_assert(is_nothrow_move_constructible_v<NameRef>);
static_assert(is_nothrow_assignable_v<NameRef, NameRef>);
static_assert(is_nothrow_copy_assignable_v<NameRef>);
static_assert(is_nothrow_move_assignable_v<NameRef>);
static_assert(is_nothrow_destructible_v<NameRef>);
static_assert(is_nothrow_swappable_v<NameRef>);

NameRef::NameRef(const GlobalState &gs, NameKind kind, uint32_t id)
    : DebugOnlyCheck(gs, kind, id), _id{(id << KIND_BITS) | static_cast<uint32_t>(kind)} {
    // If this fails, the symbol table is too big :(
    ENFORCE_NO_TIMER(id <= MAX_ID);
}

NameRef::NameRef(const GlobalState &gs, NameRef ref)
    : DebugOnlyCheck(gs, ref.kind(), ref.unsafeTableIndex()), _id(ref.rawId()) {
    // If this fails, the symbol table is too big :(
    ENFORCE_NO_TIMER(this->unsafeTableIndex() <= MAX_ID);
}

string NameRef::showRaw(const GlobalState &gs) const {
    switch (kind()) {
        case NameKind::UTF8: {
            auto raw = dataUtf8(gs);
            return fmt::format("<U {}>", string(raw->utf8.begin(), raw->utf8.end()));
        }
        case NameKind::UNIQUE: {
            auto unique = dataUnique(gs);
            string kind;
            switch (unique->uniqueNameKind) {
                case UniqueNameKind::Desugar:
                    kind = "D";
                    break;
                case UniqueNameKind::Namer:
                    kind = "N";
                    break;
                case UniqueNameKind::MangleRename:
                    kind = "M";
                    break;
                case UniqueNameKind::Singleton:
                    kind = "S";
                    break;
                case UniqueNameKind::Overload:
                    kind = "O";
                    break;
                case UniqueNameKind::TypeVarName:
                    kind = "T";
                    break;
                case UniqueNameKind::PositionalArg:
                    kind = "A";
                    break;
                case UniqueNameKind::MangledKeywordArg:
                    kind = "K";
                    break;
                case UniqueNameKind::ResolverMissingClass:
                    kind = "R";
                    break;
                case UniqueNameKind::TEnum:
                    kind = "E";
                    break;
                case UniqueNameKind::Struct:
                    kind = "U";
                    break;
                case UniqueNameKind::Packager:
                    kind = "G";
                    break;
                case UniqueNameKind::DesugarCsend:
                    kind = "&";
                    break;
            }
            if (gs.censorForSnapshotTests && unique->uniqueNameKind == UniqueNameKind::Namer &&
                unique->original == core::Names::staticInit()) {
                return fmt::format("<{} {} ${}>", kind, unique->original.showRaw(gs), "CENSORED");
            } else {
                return fmt::format("<{} {} ${}>", kind, unique->original.showRaw(gs), unique->num);
            }
        }
        case NameKind::CONSTANT:
            return fmt::format("<C {}>", dataCnst(gs)->original.showRaw(gs));
    }
}

string NameRef::toString(const GlobalState &gs) const {
    switch (kind()) {
        case NameKind::UTF8: {
            auto raw = dataUtf8(gs);
            return string(raw->utf8.begin(), raw->utf8.end());
        }
        case NameKind::UNIQUE: {
            auto unique = dataUnique(gs);
            if (unique->uniqueNameKind == UniqueNameKind::Singleton) {
                return fmt::format("<Class:{}>", unique->original.show(gs));
            } else if (unique->uniqueNameKind == UniqueNameKind::Overload) {
                return absl::StrCat(unique->original.show(gs), " (overload.", unique->num, ")");
            } else if (unique->uniqueNameKind == UniqueNameKind::DesugarCsend) {
                return fmt::format("<&{}>", unique->original.show(gs));
            }
            if (gs.censorForSnapshotTests && unique->uniqueNameKind == UniqueNameKind::Namer &&
                unique->original == core::Names::staticInit()) {
                return fmt::format("{}${}", unique->original.show(gs), "CENSORED");
            } else {
                return fmt::format("{}${}", unique->original.show(gs), unique->num);
            }
        }
        case NameKind::CONSTANT:
            return fmt::format("<C {}>", dataCnst(gs)->original.toString(gs));
    }
}

string NameRef::show(const GlobalState &gs) const {
    switch (kind()) {
        case NameKind::UTF8: {
            auto raw = dataUtf8(gs);
            return string(raw->utf8.begin(), raw->utf8.end());
        }
        case NameKind::UNIQUE: {
            auto unique = dataUnique(gs);
            if (unique->uniqueNameKind == UniqueNameKind::Singleton) {
                return fmt::format("<Class:{}>", unique->original.show(gs));
            } else if (unique->uniqueNameKind == UniqueNameKind::Overload) {
                return absl::StrCat(unique->original.show(gs), " (overload.", unique->num, ")");
            } else if (unique->uniqueNameKind == UniqueNameKind::MangleRename) {
                return unique->original.show(gs);
            } else if (unique->uniqueNameKind == UniqueNameKind::TEnum) {
                // The entire goal of UniqueNameKind::TEnum is to have Name::show print the name as if on the
                // original name, so that our T::Enum DSL-synthesized class names are kept as an implementation detail.
                // Thus, we fall through.
            }
            return unique->original.show(gs);
        }
        case NameKind::CONSTANT:
            return dataCnst(gs)->original.show(gs);
    }
}

string NameRef::showAsSymbolLiteral(const GlobalState &gs) const {
    auto shown = this->show(gs);
    if (absl::StrContains(shown, " ")) {
        return fmt::format(":\"{}\"", absl::Utf8SafeCEscape(shown));
    } else {
        return fmt::format(":{}", shown);
    }
}

string_view NameRef::shortName(const GlobalState &gs) const {
    switch (kind()) {
        case NameKind::UTF8: {
            auto raw = dataUtf8(gs);
            return string_view(raw->utf8.begin(), raw->utf8.end() - raw->utf8.begin());
        }
        case NameKind::UNIQUE:
            return dataUnique(gs)->original.shortName(gs);
        case NameKind::CONSTANT:
            return dataCnst(gs)->original.shortName(gs);
    }
}

void NameRef::sanityCheck(const GlobalState &gs) const {
    if constexpr (!debug_mode) {
        return;
    }
    switch (kind()) {
        case NameKind::UTF8:
            ENFORCE_NO_TIMER(*this == const_cast<GlobalState &>(gs).enterNameUTF8(dataUtf8(gs)->utf8),
                             "Name table corrupted, re-entering UTF8 name gives different id");
            break;
        case NameKind::UNIQUE: {
            auto unique = dataUnique(gs);
            ENFORCE_NO_TIMER(unique->num > 0, "unique num == 0");
            NameRef current2 =
                const_cast<GlobalState &>(gs).freshNameUnique(unique->uniqueNameKind, unique->original, unique->num);
            ENFORCE_NO_TIMER(*this == current2, "Name table corrupted, re-entering UNIQUE name gives different id");
            break;
        }
        case NameKind::CONSTANT:
            ENFORCE_NO_TIMER(*this == const_cast<GlobalState &>(gs).enterNameConstant(dataCnst(gs)->original),
                             "Name table corrupted, re-entering CONSTANT name gives different id");
            break;
    }
}

bool NameRef::isOverloadName(const GlobalState &gs) const {
    if (kind() != NameKind::UNIQUE) {
        return false;
    }

    return dataUnique(gs)->uniqueNameKind == UniqueNameKind::Overload;
}

bool NameRef::isClassName(const GlobalState &gs) const {
    switch (kind()) {
        case NameKind::UTF8:
            return false;
        case NameKind::UNIQUE:
            switch (dataUnique(gs)->uniqueNameKind) {
                case UniqueNameKind::Singleton:
                case UniqueNameKind::MangleRename:
                case UniqueNameKind::TEnum:
                case UniqueNameKind::Struct:
                    return dataUnique(gs)->original.isClassName(gs);
                case UniqueNameKind::ResolverMissingClass:
                case UniqueNameKind::Packager:
                case UniqueNameKind::Desugar:
                case UniqueNameKind::Namer:
                case UniqueNameKind::Overload:
                case UniqueNameKind::TypeVarName:
                case UniqueNameKind::PositionalArg:
                case UniqueNameKind::MangledKeywordArg:
                    return false;
                case UniqueNameKind::DesugarCsend:
                    Exception::raise("UniqueNameKind::DesugarCsend should only be used in Extract to Variable");
                    return false;
            }
        case NameKind::CONSTANT:
            ENFORCE_NO_TIMER(dataCnst(gs)->original.isValidConstantName(gs));
            return true;
    }
}

bool NameRef::isTEnumName(const GlobalState &gs) const {
    if (kind() != NameKind::CONSTANT) {
        return false;
    }
    auto original = dataCnst(gs)->original;
    return original.kind() == NameKind::UNIQUE && original.dataUnique(gs)->uniqueNameKind == UniqueNameKind::TEnum;
}

bool NameRef::isValidConstantName(const GlobalState &gs) const {
    switch (kind()) {
        case NameKind::UTF8:
            return true;
        case NameKind::UNIQUE:
            switch (dataUnique(gs)->uniqueNameKind) {
                case UniqueNameKind::ResolverMissingClass:
                case UniqueNameKind::TEnum:
                case UniqueNameKind::Struct:
                case UniqueNameKind::Packager:
                    return true;
                case UniqueNameKind::Desugar:
                case UniqueNameKind::Namer:
                case UniqueNameKind::MangleRename:
                case UniqueNameKind::Singleton:
                case UniqueNameKind::Overload:
                case UniqueNameKind::TypeVarName:
                case UniqueNameKind::PositionalArg:
                case UniqueNameKind::MangledKeywordArg:
                    return false;
                case UniqueNameKind::DesugarCsend:
                    Exception::raise("UniqueNameKind::DesugarCsend should only be used in Extract to Variable");
                    return false;
            }
        case NameKind::CONSTANT:
            return false;
    }
}

bool NameRef::isAnyStaticInitName(const GlobalState &gs) const {
    if (*this == core::Names::staticInit()) {
        return true;
    } else if (this->kind() != core::NameKind::UNIQUE) {
        return false;
    } else {
        return this->dataUnique(gs)->original == core::Names::staticInit();
    }
}

bool NameRef::isUniqueNameOf(const GlobalState &gs, NameRef name) const {
    return this->kind() == NameKind::UNIQUE && this->dataUnique(gs)->original == name;
}

bool NameRef::hasUniqueNameKind(const GlobalState &gs, UniqueNameKind uniqueNameKind) const {
    if (this->kind() != NameKind::UNIQUE) {
        return false;
    }

    auto &data = this->dataUnique(gs);
    return data->uniqueNameKind == uniqueNameKind;
}

bool NameRef::isUpdateKnowledgeName() const {
    switch (this->rawId()) {
        case Names::bang().rawId():
        case Names::blank_p().rawId():
        case Names::eqeq().rawId():
        case Names::equal_p().rawId():
        case Names::isA_p().rawId():
        case Names::kindOf_p().rawId():
        case Names::instanceOf_p().rawId():
        case Names::leq().rawId():
        case Names::lessThan().rawId():
        case Names::neq().rawId():
        case Names::nil_p().rawId():
        case Names::present_p().rawId():
        case Names::tripleEq().rawId():
        case Names::checkMatchArray().rawId():
            return true;
        default:
            return false;
    }
}

bool NameRef::isMethodDefModifierName() const {
    switch (this->rawId()) {
        // Instance method visibility modifiers
        case Names::private_().rawId():
        case Names::protected_().rawId():
        case Names::public_().rawId():
        case Names::packagePrivate().rawId():

        // Class method visibility modifiers
        case Names::privateClassMethod().rawId():
        case Names::publicClassMethod().rawId():
        case Names::packagePrivateClassMethod().rawId():
            return true;

        default:
            return false;
    }
}

NameRefDebugCheck::NameRefDebugCheck(const GlobalState &gs, NameKind kind, uint32_t index) {
    // store the globalStateId of the creating global state to allow sharing refs between siblings
    // when the ref refers to a name in the common ancestor
    globalStateId = gs.globalStateId;
    for (const auto &deepCloneInfo : gs.deepCloneHistory) {
        switch (kind) {
            case NameKind::UTF8:
                if (index < deepCloneInfo.lastUTF8NameKnownByParentGlobalState) {
                    globalStateId = deepCloneInfo.globalStateId;
                    return;
                }
                break;
            case NameKind::CONSTANT:
                if (index < deepCloneInfo.lastConstantNameKnownByParentGlobalState) {
                    globalStateId = deepCloneInfo.globalStateId;
                    return;
                }
                break;
            case NameKind::UNIQUE:
                if (index < deepCloneInfo.lastUniqueNameKnownByParentGlobalState) {
                    globalStateId = deepCloneInfo.globalStateId;
                    return;
                }
                break;
        }
    }
}

void NameRefDebugCheck::check(const GlobalState &gs, NameKind kind, uint32_t index) const {
    if (globalStateId == -1) {
        return;
    }
    if (kind == NameKind::UTF8 && index <= Names::LAST_WELL_KNOWN_UTF8_NAME) {
        return;
    }
    if (kind == NameKind::CONSTANT && index <= Names::LAST_WELL_KNOWN_CONSTANT_NAME) {
        return;
    }
    if (globalStateId == gs.globalStateId) {
        return;
    }
    for (const auto &deepCloneInfo : gs.deepCloneHistory) {
        if (globalStateId == deepCloneInfo.globalStateId) {
            switch (kind) {
                case NameKind::UTF8:
                    if (index < deepCloneInfo.lastUTF8NameKnownByParentGlobalState) {
                        return;
                    }
                    break;
                case NameKind::CONSTANT:
                    if (index < deepCloneInfo.lastConstantNameKnownByParentGlobalState) {
                        return;
                    }
                    break;
                case NameKind::UNIQUE:
                    if (index < deepCloneInfo.lastUniqueNameKnownByParentGlobalState) {
                        return;
                    }
                    break;
            }
        }
    }
    ENFORCE(false, "NameRef not owned by correct GlobalState");
}

void NameRefDebugCheck::check(const NameSubstitution &subst) const {
    ENFORCE(globalStateId != subst.toGlobalStateId, "substituting a name twice!");
}

void NameRef::enforceCorrectGlobalState(const GlobalState &gs) const {
    runDebugOnlyCheck(gs, kind(), unsafeTableIndex());
}

void NameRef::sanityCheckSubstitution(const NameSubstitution &subst) const {
    runDebugOnlyCheck(subst);
}

const UniqueNameData NameRef::dataUnique(const GlobalState &gs) const {
    ENFORCE_NO_TIMER(kind() == NameKind::UNIQUE);
    ENFORCE_NO_TIMER(uniqueIndex() < gs.uniqueNames.size(), "unique name {} id out of bounds {}", uniqueIndex(),
                     gs.uniqueNames.size());
    return UniqueNameData(gs.uniqueNames[uniqueIndex()], gs);
}

const UTF8NameData NameRef::dataUtf8(const GlobalState &gs) const {
    ENFORCE_NO_TIMER(kind() == NameKind::UTF8);
    ENFORCE_NO_TIMER(utf8Index() < gs.utf8Names.size(), "utf8 name {} id out of bounds {}", utf8Index(),
                     gs.utf8Names.size());
    return UTF8NameData(gs.utf8Names[utf8Index()], gs);
}

const ConstantNameData NameRef::dataCnst(const GlobalState &gs) const {
    ENFORCE_NO_TIMER(kind() == NameKind::CONSTANT);
    ENFORCE_NO_TIMER(constantIndex() < gs.constantNames.size(), "constant name {} id out of bounds {}", constantIndex(),
                     gs.constantNames.size());
    return ConstantNameData(gs.constantNames[constantIndex()], gs);
}

NameRef NameRef::addEq(GlobalState &gs) const {
    auto name = this->dataUtf8(gs);
    string nameEq = absl::StrCat(name->utf8, "=");
    return gs.enterNameUTF8(nameEq);
}

NameRef NameRef::lookupWithEq(const GlobalState &gs) const {
    auto name = this->dataUtf8(gs);
    string nameEq = absl::StrCat(name->utf8, "=");
    return gs.lookupNameUTF8(nameEq);
}

bool NameRef::isSetter(const GlobalState &gs) const {
    if (this->kind() != NameKind::UTF8) {
        return false;
    }
    const string_view rawName = this->dataUtf8(gs)->utf8;
    if (rawName.size() < 2) {
        return false;
    }
    if (rawName.back() == '=') {
        return !(*this == Names::leq() || *this == Names::geq() || *this == Names::tripleEq() ||
                 *this == Names::eqeq() || *this == Names::neq());
    }
    return false;
}

NameRef NameRef::addQuestion(GlobalState &gs) const {
    auto name = this->dataUtf8(gs);
    string nameEq = absl::StrCat(name->utf8, "?");
    return gs.enterNameUTF8(nameEq);
}

NameRef NameRef::lookupWithAt(const GlobalState &gs) const {
    auto name = this->dataUtf8(gs);
    string nameEq = absl::StrCat("@", name->utf8);
    return gs.lookupNameUTF8(nameEq);
}

NameRef NameRef::addAt(GlobalState &gs) const {
    auto name = this->dataUtf8(gs);
    string nameEq = absl::StrCat("@", name->utf8);
    return gs.enterNameUTF8(nameEq);
}

NameRef NameRef::prepend(GlobalState &gs, string_view s) const {
    auto name = this->dataUtf8(gs);
    string nameEq = absl::StrCat(s, name->utf8);
    return gs.enterNameUTF8(nameEq);
}

UTF8Name UTF8Name::deepCopy(const GlobalState &to) const {
    return UTF8Name{this->utf8};
}

ConstantName ConstantName::deepCopy(const GlobalState &to) const {
    return ConstantName{NameRef(to, this->original)};
}

UniqueName UniqueName::deepCopy(const GlobalState &to) const {
    return UniqueName{NameRef(to, this->original), this->num, this->uniqueNameKind};
}

UniqueNameData::UniqueNameData(const UniqueName &ref, const GlobalState &gs) : DebugOnlyCheck(gs), name(ref) {}
ConstantNameData::ConstantNameData(const ConstantName &ref, const GlobalState &gs) : DebugOnlyCheck(gs), name(ref) {}
UTF8NameData::UTF8NameData(const UTF8Name &ref, const GlobalState &gs) : DebugOnlyCheck(gs), name(ref) {}

NameDataDebugCheck::NameDataDebugCheck(const GlobalState &gs) : gs(gs), nameCountAtCreation(gs.namesUsedTotal()) {}

void NameDataDebugCheck::check() const {
    ENFORCE_NO_TIMER(nameCountAtCreation == gs.namesUsedTotal());
}

const UniqueName *UniqueNameData::operator->() const {
    runDebugOnlyCheck();
    return &name;
};

const ConstantName *ConstantNameData::operator->() const {
    runDebugOnlyCheck();
    return &name;
};

const UTF8Name *UTF8NameData::operator->() const {
    runDebugOnlyCheck();
    return &name;
};

} // namespace sorbet::core
