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

#include "Luau/RequireNavigator.h"

#include "AliasCycleTracker.h"
#include "PathUtilities.h"

#include "Luau/Config.h"
#include "Luau/LuauConfig.h"

#include <algorithm>
#include <optional>
#include <utility>

namespace Luau::Require
{

using Error = std::optional<std::string>;

static std::string extractAlias(std::string_view path)
{
    // To ignore the '@' alias prefix when processing the alias
    const size_t aliasStartPos = 1;

    // If a directory separator was found, the length of the alias is the
    // distance between the start of the alias and the separator. Otherwise,
    // the whole string after the alias symbol is the alias.
    size_t aliasLen = path.find_first_of('/');
    if (aliasLen != std::string::npos)
        aliasLen -= aliasStartPos;

    return std::string{path.substr(aliasStartPos, aliasLen)};
}

Navigator::Navigator(NavigationContext& navigationContext, ErrorHandler& errorHandler)
    : navigationContext(navigationContext)
    , errorHandler(errorHandler)
{
}

Navigator::Status Navigator::navigate(std::string path)
{
    std::replace(path.begin(), path.end(), '\\', '/');

    if (Error error = navigateImpl(path))
    {
        errorHandler.reportError(*error);
        return Status::ErrorReported;
    }

    return Status::Success;
}

Error Navigator::navigateImpl(std::string_view path)
{
    PathType pathType = getPathType(path);

    if (pathType == PathType::Unsupported)
        return "require path must start with a valid prefix: ./, ../, or @";

    if (pathType == PathType::Aliased)
    {
        std::string alias = extractAlias(path);
        std::transform(
            alias.begin(),
            alias.end(),
            alias.begin(),
            [](unsigned char c)
            {
                return ('A' <= c && c <= 'Z') ? (c + ('a' - 'A')) : c;
            }
        );

        if (auto [error, wasOverridden] = toAliasOverride(alias); error)
        {
            return error;
        }
        else if (wasOverridden)
        {
            if (Error error = navigateThroughPath(path))
                return error;

            return std::nullopt;
        }

        if (Error error = resetToRequirer())
            return error;

        Config config;
        if (Error error = navigateToAndPopulateConfig(alias, config))
            return error;

        if (config.aliases.contains(alias))
        {
            if (Error error = navigateToAlias(alias, config, {}))
                return error;
            if (Error error = navigateThroughPath(path))
                return error;

            return std::nullopt;
        }
        else
        {
            if (alias == "self")
            {
                // If the alias is "@self", we reset to the requirer's context and
                // navigate directly from there.
                if (Error error = resetToRequirer())
                    return error;
                if (Error error = navigateThroughPath(path))
                    return error;

                return std::nullopt;
            }

            if (Error error = toAliasFallback(alias))
                return error;
            if (Error error = navigateThroughPath(path))
                return error;

            return std::nullopt;
        }
    }

    if (pathType == PathType::RelativeToCurrent || pathType == PathType::RelativeToParent)
    {
        if (Error error = resetToRequirer())
            return error;
        if (Error error = navigateToParent(std::nullopt))
            return error;
        if (Error error = navigateThroughPath(path))
            return error;
    }

    return std::nullopt;
}

Error Navigator::navigateThroughPath(std::string_view path)
{
    std::pair<std::string_view, std::string_view> components = splitPath(path);
    if (path.size() >= 1 && path[0] == '@')
    {
        // If the path is aliased, we ignore the alias: this function assumes
        // that navigation to an alias is handled by the caller.
        components = splitPath(components.second);
    }

    std::optional<std::string> previousComponent;
    while (!(components.first.empty() && components.second.empty()))
    {
        if (components.first == "." || components.first.empty())
        {
            components = splitPath(components.second);
            continue;
        }
        else if (components.first == "..")
        {
            if (Error error = navigateToParent(previousComponent))
                return error;
        }
        else
        {
            if (Error error = navigateToChild(std::string{components.first}))
                return error;
        }
        previousComponent = components.first;
        components = splitPath(components.second);
    }

    return std::nullopt;
}

Error Navigator::navigateToAlias(const std::string& alias, const Config& config, AliasCycleTracker cycleTracker)
{
    LUAU_ASSERT(config.aliases.contains(alias));
    std::string value = config.aliases.find(alias)->value;
    PathType pathType = getPathType(value);

    if (pathType == PathType::RelativeToCurrent || pathType == PathType::RelativeToParent)
    {
        if (Error error = navigateThroughPath(value))
            return error;
    }
    else if (pathType == PathType::Aliased)
    {
        if (Error error = cycleTracker.add(alias))
            return error;

        std::string nextAlias = extractAlias(value);

        if (auto [error, wasOverridden] = toAliasOverride(nextAlias); error)
        {
            return error;
        }
        else if (wasOverridden)
        {
            if (Error error = navigateThroughPath(value))
                return error;

            return std::nullopt;
        }

        if (config.aliases.contains(nextAlias))
        {
            if (Error error = navigateToAlias(nextAlias, config, std::move(cycleTracker)))
                return error;
        }
        else
        {
            Config parentConfig;
            if (Error error = navigateToAndPopulateConfig(nextAlias, parentConfig))
                return error;

            if (parentConfig.aliases.contains(nextAlias))
            {
                if (Error error = navigateToAlias(nextAlias, parentConfig, {}))
                    return error;
            }
            else
            {
                if (Error error = toAliasFallback(nextAlias))
                    return error;
            }
        }

        if (Error error = navigateThroughPath(value))
            return error;
    }
    else
    {
        if (Error error = jumpToAlias(value))
            return error;
    }

    return std::nullopt;
}

Error Navigator::navigateToAndPopulateConfig(const std::string& desiredAlias, Config& config)
{
    while (!config.aliases.contains(desiredAlias))
    {
        config = {}; // Clear existing config data.

        NavigationContext::NavigateResult result = navigationContext.toParent();
        if (result == NavigationContext::NavigateResult::Ambiguous)
            return "could not navigate up the ancestry chain during search for alias \"" + desiredAlias + "\" (ambiguous)";
        if (result == NavigationContext::NavigateResult::NotFound)
            break; // Not treated as an error: interpreted as reaching the root.

        NavigationContext::ConfigStatus status = navigationContext.getConfigStatus();
        if (status == NavigationContext::ConfigStatus::Absent)
        {
            continue;
        }
        else if (status == NavigationContext::ConfigStatus::Ambiguous)
        {
            return "could not resolve alias \"" + desiredAlias + "\" (ambiguous configuration file)";
        }
        else
        {
            if (navigationContext.getConfigBehavior() == NavigationContext::ConfigBehavior::GetAlias)
            {
                config.setAlias(desiredAlias, *navigationContext.getAlias(desiredAlias), /* configLocation = */ "unused");
                break;
            }

            std::optional<std::string> configContents = navigationContext.getConfig();
            if (!configContents)
                return "could not get configuration file contents to resolve alias \"" + desiredAlias + "\"";

            Luau::ConfigOptions opts;
            Luau::ConfigOptions::AliasOptions aliasOpts;
            aliasOpts.configLocation = "unused";
            aliasOpts.overwriteAliases = false;
            opts.aliasOptions = std::move(aliasOpts);

            if (status == NavigationContext::ConfigStatus::PresentJson)
            {
                if (Error error = Luau::parseConfig(*configContents, config, opts))
                    return error;
            }
            else if (status == NavigationContext::ConfigStatus::PresentLuau)
            {
                InterruptCallbacks callbacks;
                callbacks.initCallback = navigationContext.luauConfigInit;
                callbacks.interruptCallback = navigationContext.luauConfigInterrupt;

                if (Error error = Luau::extractLuauConfig(*configContents, config, std::move(opts.aliasOptions), std::move(callbacks)))
                    return error;
            }
        }
    };

    return std::nullopt;
}

Error Navigator::resetToRequirer()
{
    NavigationContext::NavigateResult result = navigationContext.resetToRequirer();
    if (result == NavigationContext::NavigateResult::Success)
        return std::nullopt;

    std::string errorMessage = "could not reset to requiring context";
    if (result == NavigationContext::NavigateResult::Ambiguous)
        errorMessage += " (ambiguous)";
    return errorMessage;
}

Error Navigator::jumpToAlias(const std::string& aliasPath)
{
    NavigationContext::NavigateResult result = navigationContext.jumpToAlias(aliasPath);
    if (result == NavigationContext::NavigateResult::Success)
        return std::nullopt;

    std::string errorMessage = "could not jump to alias \"" + aliasPath + "\"";
    if (result == NavigationContext::NavigateResult::Ambiguous)
        errorMessage += " (ambiguous)";
    return errorMessage;
}

Error Navigator::navigateToParent(std::optional<std::string> previousComponent)
{
    NavigationContext::NavigateResult result = navigationContext.toParent();
    if (result == NavigationContext::NavigateResult::Success)
        return std::nullopt;

    std::string errorMessage;
    if (previousComponent)
        errorMessage = "could not get parent of component \"" + *previousComponent + "\"";
    else
        errorMessage = "could not get parent of requiring context";
    if (result == NavigationContext::NavigateResult::Ambiguous)
        errorMessage += " (ambiguous)";
    return errorMessage;
}

Error Navigator::navigateToChild(const std::string& component)
{
    NavigationContext::NavigateResult result = navigationContext.toChild(component);
    if (result == NavigationContext::NavigateResult::Success)
        return std::nullopt;

    std::string errorMessage = "could not resolve child component \"" + component + "\"";
    if (result == NavigationContext::NavigateResult::Ambiguous)
        errorMessage += " (ambiguous)";
    return errorMessage;
}

std::pair<Error, bool> Navigator::toAliasOverride(const std::string& aliasUnprefixed)
{
    std::pair<Error, bool> result;
    switch (navigationContext.toAliasOverride(aliasUnprefixed))
    {
    case NavigationContext::NavigateResult::Success:
        result = {std::nullopt, true};
        break;
    case NavigationContext::NavigateResult::NotFound:
        result = {std::nullopt, false};
        break;
    case NavigationContext::NavigateResult::Ambiguous:
        result = {"@" + aliasUnprefixed + " is not a valid alias (ambiguous)", false};
        break;
    }
    return result;
}

Error Navigator::toAliasFallback(const std::string& aliasUnprefixed)
{
    NavigationContext::NavigateResult result = navigationContext.toAliasFallback(aliasUnprefixed);
    if (result == NavigationContext::NavigateResult::Success)
        return std::nullopt;

    std::string errorMessage = "@" + aliasUnprefixed + " is not a valid alias";
    if (result == NavigationContext::NavigateResult::Ambiguous)
        errorMessage += " (ambiguous)";
    return errorMessage;
}

} // namespace Luau::Require
