// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#include "Luau/Module.h"

#include "Luau/Clone.h"
#include "Luau/Common.h"
#include "Luau/ConstraintGenerator.h"
#include "Luau/Normalize.h"
#include "Luau/RecursionCounter.h"
#include "Luau/Scope.h"
#include "Luau/Type.h"
#include "Luau/TypeInfer.h"
#include "Luau/TypePack.h"
#include "Luau/VisitType.h"

#include <algorithm>

LUAU_FASTFLAG(DebugLuauExactTableTypes)
LUAU_FASTFLAGVARIABLE(LuauExportTypecheckTypepacks)
LUAU_FASTFLAGVARIABLE(LuauExportAnnotationBinding)
LUAU_FASTFLAGVARIABLE(LuauReduceTypeFunctionToSyntheticError)

namespace Luau
{

static void defaultLogLuau(std::string_view context, std::string_view input)
{
    // The default is to do nothing because we don't want to mess with
    // the xml parsing done by the dcr script.
}

Luau::LogLuauProc logLuau = &defaultLogLuau;

void setLogLuau(LogLuauProc ll)
{
    logLuau = ll;
}

void resetLogLuauProc()
{
    logLuau = &defaultLogLuau;
}

static bool contains(Position pos, Comment comment)
{
    if (comment.location.contains(pos))
        return true;
    else if (comment.type == Lexeme::BrokenComment && comment.location.begin <= pos) // Broken comments are broken specifically because they don't
                                                                                     // have an end
        return true;
    // comments actually span the whole line - in incremental mode, we could pass a cursor outside of the current parsed comment range span, but it
    // would still be 'within' the comment So, the cursor must be on the same line and the comment itself must come strictly after the `begin`
    else if (comment.type == Lexeme::Comment && comment.location.end.line == pos.line && comment.location.begin <= pos)
        return true;
    else
        return false;
}

bool isWithinComment(const std::vector<Comment>& commentLocations, Position pos)
{
    auto iter = std::lower_bound(
        commentLocations.begin(),
        commentLocations.end(),
        Comment{Lexeme::Comment, Location{pos, pos}},
        [](const Comment& a, const Comment& b)
        {
            if (a.type == Lexeme::Comment)
                return a.location.end.line < b.location.end.line;
            return a.location.end < b.location.end;
        }
    );

    if (iter == commentLocations.end())
        return false;

    if (contains(pos, *iter))
        return true;

    // Due to the nature of std::lower_bound, it is possible that iter points at a comment that ends
    // at pos.  We'll try the next comment, if it exists.
    ++iter;
    if (iter == commentLocations.end())
        return false;

    return contains(pos, *iter);
}

bool isWithinComment(const SourceModule& sourceModule, Position pos)
{
    return isWithinComment(sourceModule.commentLocations, pos);
}

bool isWithinComment(const ParseResult& result, Position pos)
{
    return isWithinComment(result.commentLocations, pos);
}

bool isWithinHotComment(const std::vector<HotComment>& hotComments, Position pos)
{
    for (const HotComment& hotComment : hotComments)
    {
        if (hotComment.location.containsClosed(pos))
            return true;
    }

    return false;
}

bool isWithinHotComment(const SourceModule& sourceModule, Position pos)
{
    return isWithinHotComment(sourceModule.hotcomments, pos);
}

bool isWithinHotComment(const ParseResult& result, Position pos)
{
    return isWithinHotComment(result.hotcomments, pos);
}

struct ClonePublicInterface : Substitution
{
    NotNull<BuiltinTypes> builtinTypes;
    NotNull<Module> module;
    SolverMode solverMode;
    bool internalTypeEscaped = false;

    ClonePublicInterface(const TxnLog* log, NotNull<BuiltinTypes> builtinTypes, Module* module, SolverMode solverMode)
        : Substitution(log, &module->interfaceTypes)
        , builtinTypes(builtinTypes)
        , module(module)
        , solverMode(solverMode)
    {
        LUAU_ASSERT(module);
    }

    bool isNewSolver() const
    {
        return solverMode == SolverMode::New;
    }

    bool isDirty(TypeId ty) override
    {
        if (ty->owningArena == module->internalTypes.get())
            return true;

        if (const FunctionType* ftv = get<FunctionType>(ty))
            return ftv->level.level != 0;
        if (const TableType* ttv = get<TableType>(ty))
            return ttv->level.level != 0;
        return false;
    }

    bool isDirty(TypePackId tp) override
    {
        return tp->owningArena == module->internalTypes.get();
    }

    bool ignoreChildrenVisit(TypeId ty) override
    {
        if (ty->owningArena != module->internalTypes.get())
            return true;

        return false;
    }

    bool ignoreChildrenVisit(TypePackId tp) override
    {
        if (tp->owningArena != module->internalTypes.get())
            return true;

        return false;
    }

    TypeId clean(TypeId ty) override
    {
        TypeId result = clone(ty);

        if (FunctionType* ftv = getMutable<FunctionType>(result))
        {
            if (ftv->generics.empty() && ftv->genericPacks.empty())
            {
                GenericTypeFinder marker;
                marker.traverse(result);

                if (!marker.found)
                    ftv->hasNoFreeOrGenericTypes = true;
            }

            ftv->level = TypeLevel{0, 0};
        }
        else if (TableType* ttv = getMutable<TableType>(result))
        {
            ttv->level = TypeLevel{0, 0};
            if (isNewSolver())
            {
                ttv->scope = nullptr;
                if (FFlag::DebugLuauExactTableTypes)
                {
                    if (ttv->state != TableState::Sealed && ttv->state != TableState::Exact)
                        ttv->state = TableState::Sealed;
                }
                else
                    ttv->state = TableState::Sealed;
            }
        }

        if (isNewSolver())
        {
            if (is<FreeType, BlockedType, PendingExpansionType>(ty))
            {
                internalTypeEscaped = true;
                result = builtinTypes->errorType;
            }
            else if (auto genericty = getMutable<GenericType>(result))
            {
                genericty->scope = nullptr;
            }
            else
            {
                if (auto tfit = get<TypeFunctionInstanceType>(ty))
                {
                    // CLI-230871: Trying to use a synthetic error type here
                    // causes a use-after-free.
                    if (tfit->state == TypeFunctionInstanceState::Stuck)
                    {
                        if (FFlag::LuauReduceTypeFunctionToSyntheticError)
                            result = builtinTypes->errorType;
                        else
                            result = arena->addType(ErrorType{ty});
                    }
                    else
                    {
                        auto resultTfit = getMutable<TypeFunctionInstanceType>(result);
                        LUAU_ASSERT(resultTfit);
                        resultTfit->state = tfit->state;
                    }
                }
            }
        }

        return result;
    }

    TypePackId clean(TypePackId tp) override
    {
        if (isNewSolver())
        {
            if (is<FreeTypePack, BlockedTypePack>(tp))
            {
                internalTypeEscaped = true;
                return builtinTypes->errorTypePack;
            }

            auto clonedTp = clone(tp);
            if (auto gtp = getMutable<GenericTypePack>(clonedTp))
                gtp->scope = nullptr;
            return clonedTp;
        }
        else
        {
            return clone(tp);
        }
    }

    TypeId cloneType(TypeId ty)
    {
        std::optional<TypeId> result = substitute(ty);
        if (result)
        {
            return *result;
        }
        else
        {

            module->errors.emplace_back(module->scopes[0].first, UnificationTooComplex{});
            return builtinTypes->errorType;
        }
    }

    TypePackId cloneTypePack(TypePackId tp)
    {
        std::optional<TypePackId> result = substitute(tp);
        if (result)
        {
            return *result;
        }
        else
        {
            module->errors.emplace_back(module->scopes[0].first, UnificationTooComplex{});
            return builtinTypes->errorTypePack;
        }
    }

    TypeFun cloneTypeFun(const TypeFun& tf)
    {
        std::vector<GenericTypeDefinition> typeParams;
        std::vector<GenericTypePackDefinition> typePackParams;

        for (GenericTypeDefinition typeParam : tf.typeParams)
        {
            TypeId ty = cloneType(typeParam.ty);
            std::optional<TypeId> defaultValue;

            if (typeParam.defaultValue)
                defaultValue = cloneType(*typeParam.defaultValue);

            typeParams.push_back(GenericTypeDefinition{ty, defaultValue});
        }

        for (GenericTypePackDefinition typePackParam : tf.typePackParams)
        {
            TypePackId tp = cloneTypePack(typePackParam.tp);
            std::optional<TypePackId> defaultValue;

            if (typePackParam.defaultValue)
                defaultValue = cloneTypePack(*typePackParam.defaultValue);

            typePackParams.push_back(GenericTypePackDefinition{tp, defaultValue});
        }

        TypeId type = cloneType(tf.type);

        return TypeFun{std::move(typeParams), std::move(typePackParams), type, tf.definitionLocation};
    }
};

Module::~Module()
{
    unfreeze(interfaceTypes);
    if (internalTypes)
        unfreeze(*internalTypes);
}

void Module::clonePublicInterface(NotNull<BuiltinTypes> builtinTypes, InternalErrorReporter& ice, SolverMode mode)
{
    CloneState cloneState{builtinTypes};

    ScopePtr moduleScope = getModuleScope();

    TypePackId returnType = moduleScope->returnType;
    std::optional<TypePackId> varargPack = mode == SolverMode::New ? std::nullopt : moduleScope->varargPack;

    TxnLog log;
    ClonePublicInterface clonePublicInterface{&log, builtinTypes, this, mode};

    returnType = clonePublicInterface.cloneTypePack(returnType);

    moduleScope->returnType = returnType;
    if (varargPack)
    {
        varargPack = clonePublicInterface.cloneTypePack(*varargPack);
        moduleScope->varargPack = varargPack;
    }

    for (auto& [name, tf] : moduleScope->exportedTypeBindings)
    {
        tf = clonePublicInterface.cloneTypeFun(tf);
    }

    for (auto& [name, ty] : declaredGlobals)
    {
        ty = clonePublicInterface.cloneType(ty);
    }

    for (auto& tf : typeFunctionAliases)
    {
        *tf = clonePublicInterface.cloneTypeFun(*tf);
    }

    if (clonePublicInterface.internalTypeEscaped)
    {
        errors.emplace_back(
            Location{}, // Not amazing but the best we can do.
            name,
            InternalError{"An internal type is escaping this module; please report this bug at "
                          "https://github.com/luau-lang/luau/issues"}
        );
    }

    // Copy external stuff over to Module itself
    this->returnType = moduleScope->returnType;
    this->exportedTypeBindings = moduleScope->exportedTypeBindings;
}

bool Module::hasModuleScope() const
{
    return !scopes.empty();
}

ScopePtr Module::getModuleScope() const
{
    LUAU_ASSERT(hasModuleScope());
    return scopes.front().second;
}

void synthesizeExportReturn(NotNull<BuiltinTypes> builtinTypes, NotNull<Module> module)
{
    LUAU_ASSERT(module->root);

    ScopePtr moduleScope = module->getModuleScope();
    TableType::Props props;

    auto lookupExportedBindingType = [&](AstLocal* local) -> TypeId
    {
        NotNull<Scope> scope = moduleScope->findNarrowestScopeContaining(local->location);

        if (std::optional<std::pair<Binding*, Scope*>> binding = scope->lookupEx(Symbol{local}))
            return follow(binding->first->typeId);

        return builtinTypes->errorType;
    };

    auto lookupExprType = [&](AstExpr* expr) -> TypeId
    {
        if (TypeId* ty = module->astTypes.find(expr))
            return follow(*ty);

        // type-packs may not be in astTypes (require causes this), so we check and assign the first value here
        if (FFlag::LuauExportTypecheckTypepacks)
        {
            if (TypePackId* tp = module->astTypePacks.find(expr))
            {
                if (std::optional<TypeId> ty = first(*tp))
                    return follow(*ty);
            }
        }

        return builtinTypes->errorType;
    };

    DenseHashSet<AstLocal*> exportedLocals;

    for (AstStat* statement : module->root->body)
    {
        if (AstStatLocal* localStat = statement->as<AstStatLocal>())
        {
            if (!localStat->isExported)
                continue;

            for (size_t i = 0; i < localStat->vars.size; ++i)
            {
                AstLocal* local = localStat->vars.data[i];
                exportedLocals.insert(local);

                if (FFlag::LuauExportAnnotationBinding)
                {
                    if (localStat->vars.size != localStat->values.size || i >= localStat->values.size || local->annotation)
                    {
                        props[local->name.value] = Property::readonly(lookupExportedBindingType(local));
                    }
                    else
                    {
                        props[local->name.value] = Property::readonly(lookupExprType(localStat->values.data[i]));
                    }
                }
                else
                {
                    if (localStat->vars.size != localStat->values.size || i >= localStat->values.size)
                    {
                        props[local->name.value] = lookupExportedBindingType(local);
                    }
                    else
                    {
                        props[local->name.value] = Property::readonly(lookupExprType(localStat->values.data[i]));
                    }
                }

                props[local->name.value].location = local->location;
            }
        }
        else if (AstStatLocalFunction* localFunction = statement->as<AstStatLocalFunction>())
        {
            if (!localFunction->name->isExported)
                continue;

            props[localFunction->name->name.value] = Property::readonly(lookupExportedBindingType(localFunction->name));
            props[localFunction->name->name.value].location = localFunction->name->location;
        }
        else if (AstStatAssign* assign = statement->as<AstStatAssign>())
        {
            for (size_t i = 0; i < assign->vars.size; ++i)
            {
                AstExprLocal* exprLocal = assign->vars.data[i]->as<AstExprLocal>();
                if (!exprLocal || !exportedLocals.contains(exprLocal->local))
                    continue;

                if (assign->vars.size != assign->values.size || i >= assign->values.size)
                {
                    props[exprLocal->local->name.value] = lookupExportedBindingType(exprLocal->local);
                }
                else
                {
                    props[exprLocal->local->name.value] = Property::readonly(lookupExprType(assign->values.data[i]));
                }

                props[exprLocal->local->name.value].location = exprLocal->local->location;
            }
        }
        else if (AstStatFunction* funcStat = statement->as<AstStatFunction>())
        {
            AstExprLocal* exprLocal = funcStat->name->as<AstExprLocal>();
            if (exprLocal && exportedLocals.contains(exprLocal->local))
            {
                props[exprLocal->local->name.value] = Property::readonly(lookupExprType(funcStat->func));
                props[exprLocal->local->name.value].location = exprLocal->local->location;
            }
        }
        else if (FFlag::DebugLuauUserDefinedClasses)
        {
            if (AstStatClass* classStat = statement->as<AstStatClass>())
            {
                if (!classStat->exported)
                    continue;

                props[classStat->name->name.value] = Property::readonly(lookupExportedBindingType(classStat->name));
                props[classStat->name->name.value].location = classStat->name->location;
            }
        }
    }

    if (props.empty())
        return;

    const TableState state = FFlag::DebugLuauExactTableTypes ? TableState::Exact : TableState::Sealed;
    TableType tbl{props, std::nullopt, moduleScope->level, state};
    tbl.definitionModuleName = module->name;
    TypeId exports = module->internalTypes->addType(std::move(tbl));
    moduleScope->returnType = module->internalTypes->addTypePack({exports});
}

} // namespace Luau
