// Copyright (c) ZeroC, Inc.

#include "Parser.h"
#include "../Ice/ConsoleUtil.h"
#include "Ice/Ice.h"
#include "IceStormInternal.h"

#include "../Ice/DisableWarnings.h"

#include <algorithm>

#if defined(__APPLE__) || defined(__linux__)
#    include <editline/readline.h>
#    include <unistd.h>
#elif defined(_WIN32)
#    include <windows.h>
#endif

extern FILE* yyin;
extern int yydebug;

int yyparse();

using namespace std;
using namespace Ice;
using namespace IceInternal;
using namespace IceStorm;

namespace IceStorm
{
    Parser* parser;

#ifdef _WIN32
    shared_ptr<Ice::StringConverter> windowsConsoleConverter = nullptr;
#endif
}

namespace
{
    class UnknownManagerException : public std::exception
    {
    public:
        explicit UnknownManagerException(std::string name) : _name(std::move(name)) {}

        [[nodiscard]] const char* what() const noexcept override { return _name.c_str(); }

    private:
        const string _name;
    };
}

Parser::Parser(CommunicatorPtr communicator, TopicManagerPrx admin, map<Ice::Identity, TopicManagerPrx> managers)
    : _communicator(std::move(communicator)),
      _defaultManager(std::move(admin)),
      _managers(std::move(managers))
{
#ifdef _WIN32
    if (!windowsConsoleConverter)
    {
        windowsConsoleConverter = Ice::createWindowsStringConverter(GetConsoleOutputCP());
    }
#endif
}

void
Parser::usage()
{
    consoleOut << "help                     Print this message.\n"
                  "exit, quit               Exit this program.\n"
                  "create TOPICS            Add TOPICS.\n"
                  "destroy TOPICS           Remove TOPICS.\n"
                  "link FROM TO [COST]      Link FROM to TO with the optional given COST.\n"
                  "unlink FROM TO           Unlink TO from FROM.\n"
                  "links [INSTANCE-NAME]    Display all links for the topics in the current topic\n"
                  "                         manager, or in the given INSTANCE-NAME.\n"
                  "topics [INSTANCE-NAME]   Display the names of all topics in the current topic\n"
                  "                         manager, or in the given INSTANCE-NAME.\n"
                  "current [INSTANCE-NAME]  Display the current topic manager, or change it to\n"
                  "                         INSTANCE-NAME.\n"
                  "replica [INSTANCE-NAME]  Display replication information for the given INSTANCE-NAME.\n"
                  "subscribers TOPICS       List TOPICS subscribers.\n";
}

void
Parser::create(const list<string>& args)
{
    if (args.empty())
    {
        error("`create' requires at least one argument (type `help' for more info)");
        return;
    }

    for (const auto& arg : args)
    {
        try
        {
            string topicName;
            auto manager = findManagerById(arg, topicName);
            manager->create(topicName);
        }
        catch (const std::exception&)
        {
            exception(
                current_exception(),
                args.size() > 1); // Print a warning if we're creating multiple topics, an error otherwise.
        }
    }
}

void
Parser::destroy(const list<string>& args)
{
    if (args.empty())
    {
        error("`destroy' requires at least one argument (type `help' for more info)");
        return;
    }

    for (const auto& arg : args)
    {
        try
        {
            findTopic(arg)->destroy();
        }
        catch (const std::exception&)
        {
            exception(
                current_exception(),
                args.size() > 1); // Print a warning if we're destroying multiple topics, an error otherwise.
        }
    }
}

void
Parser::link(const list<string>& args)
{
    if (args.size() != 2 && args.size() != 3)
    {
        error("`link' requires two or three arguments (type `help' for more info)");
        return;
    }

    try
    {
        auto p = args.begin();

        auto fromTopic = findTopic(*p++);
        auto toTopic = findTopic(*p++);
        int32_t cost = p != args.end() ? stoi(*p) : 0;

        fromTopic->link(toTopic, cost);
    }
    catch (const std::exception&)
    {
        exception(current_exception());
    }
}

void
Parser::unlink(const list<string>& args)
{
    if (args.size() != 2)
    {
        error("`unlink' requires exactly two arguments (type `help' for more info)");
        return;
    }

    try
    {
        auto p = args.begin();

        auto fromTopic = findTopic(*p++);
        auto toTopic = findTopic(*p++);

        fromTopic->unlink(toTopic);
    }
    catch (const std::exception&)
    {
        exception(current_exception());
    }
}

void
Parser::links(const list<string>& args)
{
    if (args.size() > 1)
    {
        error("`links' requires at most one argument (type `help' for more info)");
        return;
    }

    try
    {
        TopicManagerPrx manager = args.size() == 0 ? _defaultManager : findManagerByCategory(args.front());

        for (const auto& topic : manager->retrieveAll())
        {
            for (const auto& linkInfo : topic.second->getLinkInfoSeq())
            {
                consoleOut << topic.first << " to " << linkInfo.name << " with cost " << linkInfo.cost << endl;
            }
        }
    }
    catch (const std::exception&)
    {
        exception(current_exception());
    }
}

void
Parser::topics(const list<string>& args)
{
    if (args.size() > 1)
    {
        error("`topics' requires at most one argument (type `help' for more info)");
        return;
    }

    try
    {
        TopicManagerPrx manager = args.size() == 0 ? _defaultManager : findManagerByCategory(args.front());
        for (const auto& topic : manager->retrieveAll())
        {
            consoleOut << topic.first << endl;
        }
    }
    catch (const std::exception&)
    {
        exception(current_exception());
    }
}

void
Parser::replica(const list<string>& args)
{
    if (args.size() > 1)
    {
        error("`replica' requires at most one argument (type `help' for more info)");
        return;
    }

    try
    {
        auto manager = uncheckedCast<TopicManagerInternalPrx>(
            args.size() == 0 ? _defaultManager : findManagerByCategory(args.front()));
        auto node = manager->getReplicaNode();
        if (!node)
        {
            error("This topic is not replicated");
        }
        auto nodes = node->nodes();
        consoleOut << "replica count: " << nodes.size() << endl;
        for (const auto& n : nodes)
        {
            try
            {
                auto info = n.n->query();
                consoleOut << n.id << ": id:         " << info.id << endl;
                consoleOut << n.id << ": coord:      " << info.coord << endl;
                consoleOut << n.id << ": group name: " << info.group << endl;
                consoleOut << n.id << ": state:      ";
                switch (info.state)
                {
                    case IceStormElection::NodeState::NodeStateInactive:
                        consoleOut << "inactive";
                        break;
                    case IceStormElection::NodeState::NodeStateElection:
                        consoleOut << "election";
                        break;
                    case IceStormElection::NodeState::NodeStateReorganization:
                        consoleOut << "reorganization";
                        break;
                    case IceStormElection::NodeState::NodeStateNormal:
                        consoleOut << "normal";
                        break;
                    default:
                        consoleOut << "unknown";
                }
                consoleOut << endl;
                consoleOut << n.id << ": group:      ";
                for (auto q = info.up.cbegin(); q != info.up.cend(); ++q)
                {
                    if (q != info.up.cbegin())
                    {
                        consoleOut << ",";
                    }
                    consoleOut << q->id;
                }
                consoleOut << endl;
                consoleOut << n.id << ": max:        " << info.max << endl;
            }
            catch (const Exception& ex)
            {
                consoleOut << n.id << ": " << ex.ice_id() << endl;
            }
        }
    }
    catch (const std::exception&)
    {
        exception(current_exception());
    }
}

void
Parser::subscribers(const list<string>& args)
{
    if (args.empty())
    {
        error("subscribers' requires at least one argument (type `help' for more info) ");
        return;
    }
    try
    {
        for (const auto& arg : args)
        {
            auto topic = _defaultManager->retrieve(arg);
            consoleOut << arg << ": subscribers:" << endl;
            for (const auto& subscriber : topic->getSubscribers())
            {
                consoleOut << "\t" << _communicator->identityToString(subscriber) << endl;
            }
        }
    }
    catch (const std::exception&)
    {
        exception(current_exception());
    }
}

void
Parser::current(const list<string>& args)
{
    if (args.empty())
    {
        consoleOut << _communicator->identityToString(_defaultManager->ice_getIdentity()) << endl;
        return;
    }
    else if (args.size() > 1)
    {
        error("`current' requires at most one argument (type `help' for more info)");
        return;
    }

    try
    {
        auto manager = findManagerByCategory(args.front());
        manager->ice_ping();
        _defaultManager = manager;
    }
    catch (const std::exception&)
    {
        exception(current_exception());
    }
}

void
Parser::showBanner()
{
    consoleOut << "Ice " << ICE_STRING_VERSION << "  Copyright (c) ZeroC, Inc." << endl;
}

//
// With older flex version <= 2.5.35 YY_INPUT second
// paramenter is of type int&, in newer versions it
// changes to size_t&
//
void
Parser::getInput(char* buf, int& result, size_t maxSize)
{
    auto r = static_cast<size_t>(result);
    getInput(buf, r, maxSize);
    result = static_cast<int>(r);
}

void
Parser::getInput(char* buf, size_t& result, size_t maxSize)
{
    if (!_commands.empty())
    {
        if (_commands == ";")
        {
            result = 0;
        }
        else
        {
            result = min(maxSize, _commands.length());
            strncpy(buf, _commands.c_str(), result);
            _commands.erase(0, result);
            if (_commands.empty())
            {
                _commands = ";";
            }
        }
    }
    else
    {
#if defined(__APPLE__) || defined(__linux__)
        if (isatty(fileno(stdin)) == 1)
        {
            const char* prompt = parser->getPrompt();
            char* line = readline(const_cast<char*>(prompt));
            if (!line)
            {
                result = 0;
            }
            else
            {
                if (*line)
                {
                    add_history(line);
                }

                result = strlen(line) + 1;
                if (result > maxSize)
                {
                    free(line);
                    error("input line too long");
                    result = 0;
                }
                else
                {
                    // NOLINTBEGIN(clang-analyzer-security.insecureAPI.strcpy)
                    strcpy(buf, line);
                    strcat(buf, "\n");
                    // NOLINTEND(clang-analyzer-security.insecureAPI.strcpy)
                    free(line);
                }
            }
        }
        else
        {
#endif
            consoleOut << parser->getPrompt() << flush;

            string line;
            while (true)
            {
                int c = getc(yyin);
                if (c == EOF)
                {
                    if (line.size())
                    {
                        line += '\n';
                    }
                    break;
                }

                line += static_cast<char>(c);
                if (c == '\n')
                {
                    break;
                }
            }
#ifdef _WIN32
            if (windowsConsoleConverter)
            {
                line = nativeToUTF8(line, windowsConsoleConverter);
            }
#endif
            result = line.length();
            if (result > maxSize)
            {
                error("input line too long");
                buf[0] = EOF;
                result = 1;
            }
            else
            {
                strcpy(buf, line.c_str()); // NOLINT(clang-analyzer-security.insecureAPI.strcpy)
            }
#if defined(__APPLE__) || defined(__linux__)
        }
#endif
    }
}

void
Parser::continueLine()
{
    _continue = true;
}

const char*
Parser::getPrompt()
{
    assert(_commands.empty());

    if (_continue)
    {
        _continue = false;
        return "(cont) ";
    }
    else
    {
        return ">>> ";
    }
}

void
Parser::error(const char* s)
{
    consoleErr << "error: " << s << endl;
    _errors++;
}

void
Parser::error(const string& s)
{
    error(s.c_str());
}

void
Parser::warning(const char* s)
{
    consoleErr << "warning: " << s << endl;
}

void
Parser::warning(const string& s)
{
    warning(s.c_str());
}

void
Parser::invalidCommand(const string& s)
{
    consoleErr << s << endl;
}

int
Parser::parse(FILE* file, bool debug)
{
    yydebug = debug ? 1 : 0;

    assert(!parser);
    parser = this;

    _errors = 0;
    _commands.clear();
    yyin = file;
    assert(yyin);

    _continue = false;

    int status = yyparse();
    if (_errors)
    {
        status = EXIT_FAILURE;
    }

    parser = nullptr;
    return status;
}

int
Parser::parse(const std::string& commands, bool debug)
{
    yydebug = debug ? 1 : 0;

    assert(!parser);
    parser = this;

    _errors = 0;
    _commands = commands;
    assert(!_commands.empty());
    yyin = nullptr;

    _continue = false;

    int status = yyparse();
    if (_errors)
    {
        status = EXIT_FAILURE;
    }

    parser = nullptr;
    return status;
}

TopicManagerPrx
Parser::findManagerById(const string& full, string& arg) const
{
    auto id = Ice::stringToIdentity(full);
    arg = id.name;
    if (id.category.empty())
    {
        return _defaultManager;
    }
    id.name = "TopicManager";
    auto p = _managers.find(id);
    if (p == _managers.end())
    {
        throw UnknownManagerException(id.category);
    }
    return p->second;
}

TopicManagerPrx
Parser::findManagerByCategory(const string& full) const
{
    Ice::Identity id = {"TopicManager", full};
    auto p = _managers.find(id);
    if (p == _managers.end())
    {
        throw UnknownManagerException(id.category);
    }
    return p->second;
}

optional<TopicPrx>
Parser::findTopic(const string& full) const
{
    string topicName;
    auto manager = findManagerById(full, topicName);
    return manager->retrieve(topicName);
}

void
Parser::exception(exception_ptr pex, bool warn)
{
    ostringstream os;
    try
    {
        rethrow_exception(pex);
    }
    catch (const LinkExists& ex)
    {
        os << "link '" << ex.name << "' already exists";
    }
    catch (const NoSuchLink& ex)
    {
        os << "couldn't find link '" << ex.name << "'";
    }
    catch (const TopicExists& ex)
    {
        os << "topic '" << ex.name << "' exists";
    }
    catch (const NoSuchTopic& ex)
    {
        os << "couldn't find topic '" << ex.name << "'";
    }
    catch (const UnknownManagerException& ex)
    {
        os << "couldn't find IceStorm service '" << ex.what() << "'";
    }
    catch (const ParseException& ex)
    {
        os << "parse exception:\n" << ex;
    }
    catch (const Ice::LocalException& ex)
    {
        os << "couldn't reach IceStorm service:\n" << ex;
    }
    catch (const Ice::Exception& ex)
    {
        os << ex;
    }
    catch (const std::exception& ex)
    {
        os << ex.what();
    }

    if (warn)
    {
        warning(os.str());
    }
    else
    {
        error(os.str());
    }
}
