/*
 *  The ManaPlus Client
 *  Copyright (C) 2004-2009  The Mana World Development Team
 *  Copyright (C) 2009-2010  The Mana Developers
 *  Copyright (C) 2011-2013  The ManaPlus Developers
 *
 *  This file is part of The ManaPlus Client.
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#include "gui/serverdialog.h"

#include "chatlogger.h"
#include "client.h"
#include "configuration.h"
#include "keydata.h"
#include "keyevent.h"
#include "main.h"

#include "gui/editserverdialog.h"
#include "gui/gui.h"
#include "gui/logindialog.h"
#include "gui/okdialog.h"
#include "gui/sdlfont.h"
#include "gui/sdlinput.h"

#include "gui/widgets/button.h"
#include "gui/widgets/dropdown.h"
#include "gui/widgets/label.h"
#include "gui/widgets/layout.h"
#include "gui/widgets/listbox.h"
#include "gui/widgets/scrollarea.h"
#include "gui/widgets/textfield.h"

#include "net/net.h"

#include "utils/gettext.h"
#include "utils/langs.h"

#include <guichan/font.hpp>

#include <cstdlib>
#include <iostream>
#include <string>

#include "debug.h"

static const int MAX_SERVERLIST = 15;

static std::string serverTypeToString(const ServerInfo::Type type)
{
    switch (type)
    {
        case ServerInfo::TMWATHENA:
            return "TmwAthena";
        case ServerInfo::EVOL:
            return "Evol";
#ifdef EATHENA_SUPPORT
        case ServerInfo::EATHENA:
            return "eAthena";
#endif
#ifdef MANASERV_SUPPORT
        case ServerInfo::MANASERV:
            return "ManaServ";
#else
        case ServerInfo::MANASERV:
#endif
#ifndef EATHENA_SUPPORT
        case ServerInfo::EATHENA:
#endif
        default:
        case ServerInfo::UNKNOWN:
            return "";
    }
}

static unsigned short defaultPortForServerType(const ServerInfo::Type type)
{
    switch (type)
    {
        default:
        case ServerInfo::EATHENA:
#ifdef EATHENA_SUPPORT
            return 6900;
#else
            return 6901;
#endif
        case ServerInfo::UNKNOWN:
        case ServerInfo::TMWATHENA:
        case ServerInfo::EVOL:
#ifdef MANASERV_SUPPORT
            return 6901;
        case ServerInfo::MANASERV:
            return 9601;
#else
        case ServerInfo::MANASERV:
            return 6901;
#endif
    }
}

ServersListModel::ServersListModel(ServerInfos *const servers,
                                   ServerDialog *const parent) :
        mServers(servers),
        mVersionStrings(servers->size(), VersionString(0, "")),
        mParent(parent)
{
}

int ServersListModel::getNumberOfElements()
{
    MutexLocker lock = mParent->lock();
    return static_cast<int>(mServers->size());
}

std::string ServersListModel::getElementAt(int elementIndex)
{
    MutexLocker lock = mParent->lock();
    const ServerInfo &server = mServers->at(elementIndex);
    std::string myServer;
    myServer.append(server.hostname);
//    myServer += ":";
//    myServer += toString(server.port);
    return myServer;
}

void ServersListModel::setVersionString(const int index,
                                        const std::string &version)
{
    if (index >= static_cast<int>(mVersionStrings.size()))
        return;

    if (version.empty())
    {
        mVersionStrings[index] = VersionString(0, "");
    }
    else
    {
        mVersionStrings[index] = VersionString(
            gui->getFont()->getWidth(version), version);
    }
}

class ServersListBox final : public ListBox
{
public:
    ServersListBox(const Widget2 *const widget,
                   ServersListModel *const model) :
        ListBox(widget, model, "serverslistbox.xml"),
        mHighlightColor(getThemeColor(Theme::HIGHLIGHT)),
        mNotSupportedColor(getThemeColor(Theme::SERVER_VERSION_NOT_SUPPORTED))
    {
    }

    void draw(gcn::Graphics *graphics)
    {
        if (!mListModel)
            return;

        ServersListModel *const model = static_cast<ServersListModel *const>(
            mListModel);

        updateAlpha();

        mHighlightColor.a = static_cast<int>(mAlpha * 255.0f);
        graphics->setColor(mHighlightColor);

        const int height = getRowHeight();
        mNotSupportedColor.a = static_cast<int>(mAlpha * 255.0f);

        // Draw filled rectangle around the selected list element
        if (mSelected >= 0)
        {
            graphics->fillRectangle(gcn::Rectangle(mPadding,
                height * mSelected + mPadding, getWidth() - 2 * mPadding,
                height));
        }

        gcn::Font *const font1 = boldFont;
        gcn::Font *const font2 = getFont();
        const int fontHeight = font1->getHeight();
        const int pad1 = fontHeight + mPadding;
        const int pad2 = height / 4 + mPadding;
        const int width = getWidth();
        // Draw the list elements
        for (int i = 0, y = 0; i < model->getNumberOfElements();
             ++i, y += height)
        {
            ServerInfo info = model->getServer(i);

            if (mSelected == i)
                graphics->setColor(mForegroundSelectedColor);
            else
                graphics->setColor(mForegroundColor);

            int top;
            int x = mPadding;

            if (!info.name.empty())
            {
                x += font1->getWidth(info.name) + 15;
                font1->drawString(graphics, info.name, mPadding, y + mPadding);
                top = y + pad1;
            }
            else
            {
                top = y + pad2;
            }

            if (!info.description.empty())
                font2->drawString(graphics, info.description, x, y + mPadding);
            font2->drawString(graphics, model->getElementAt(i), mPadding, top);

            if (info.version.first > 0)
            {
                graphics->setColor(mNotSupportedColor);
                font2->drawString(graphics, info.version.second,
                    width - info.version.first - mPadding, top);
            }
        }
    }

    unsigned int getRowHeight() const
    {
        return 2 * getFont()->getHeight() + 5;
    }
private:
    gcn::Color mHighlightColor;
    gcn::Color mNotSupportedColor;
};


ServerDialog::ServerDialog(ServerInfo *const serverInfo,
                           const std::string &dir) :
    Window(_("Choose Your Server"), false, nullptr, "server.xml"),
    gcn::ActionListener(),
    gcn::KeyListener(),
    gcn::SelectionListener(),
    mDescription(new Label(this, std::string())),
    mQuitButton(new Button(this, _("Quit"), "quit", this)),
    mConnectButton(new Button(this, _("Connect"), "connect", this)),
    mAddEntryButton(new Button(this, _("Add"), "addEntry", this)),
    mEditEntryButton(new Button(this, _("Edit"), "editEntry", this)),
    mDeleteButton(new Button(this, _("Delete"), "remove", this)),
    mLoadButton(new Button(this, _("Load"), "load", this)),
    mServers(ServerInfos()),
    mServersListModel(new ServersListModel(&mServers, this)),
    mServersList(new ServersListBox(this, mServersListModel)),
    mDir(dir),
    mDownloadStatus(DOWNLOADING_UNKNOWN),
    mDownload(nullptr),
    mDownloadProgress(-1.0f),
    mServerInfo(serverInfo),
    mPersistentIPCheckBox(nullptr)
{
    if (isSafeMode)
        setCaption(_("Choose Your Server  *** SAFE MODE ***"));

    setWindowName("ServerDialog");

    setCloseButton(true);

    mPersistentIPCheckBox = new CheckBox(this,
        _("Use same ip for game sub servers"),
        config.getBoolValue("usePersistentIP"),
        this, "persitent ip");

    loadCustomServers();

    mServersList->addMouseListener(this);

    ScrollArea *const usedScroll = new ScrollArea(mServersList,
        getOptionBool("showbackground"), "server_background.xml");
    usedScroll->setHorizontalScrollPolicy(gcn::ScrollArea::SHOW_NEVER);

    mServersList->addSelectionListener(this);
    usedScroll->setVerticalScrollAmount(0);

    const int n = 0;
    place(0, 0 + n, usedScroll, 7, 5).setPadding(3);
    place(0, 5 + n, mDescription, 7);
    place(0, 6 + n, mPersistentIPCheckBox, 7);
    place(0, 7 + n, mAddEntryButton);
    place(1, 7 + n, mEditEntryButton);
    place(2, 7 + n, mLoadButton);
    place(3, 7 + n, mDeleteButton);
    place(5, 7 + n, mQuitButton);
    place(6, 7 + n, mConnectButton);

    // Make sure the list has enough height
    getLayout().setRowHeight(0, 80);

    // Do this manually instead of calling reflowLayout so we can enforce a
    // minimum width.
    int width = 500;
    int height = 350;

    getLayout().reflow(width, height);
    setContentSize(width, height);

    setMinWidth(310);
    setMinHeight(220);
    setDefaultSize(getWidth(), getHeight(), ImageRect::CENTER);

    setResizable(true);
    addKeyListener(this);

    loadWindowState();

    setVisible(true);

    mConnectButton->requestFocus();

    loadServers(true);

    mServersList->setSelected(0); // Do this after for the Delete button

    if (needUpdateServers())
        downloadServerList();
}

ServerDialog::~ServerDialog()
{
    if (mDownload)
    {
        mDownload->cancel();
        delete mDownload;
        mDownload = nullptr;
    }
    delete mServersListModel;
    mServersListModel = nullptr;
}

void ServerDialog::connectToSelectedServer()
{
    if (Client::getState() == STATE_CONNECT_SERVER)
        return;

    const int index = mServersList->getSelected();
    if (index < 0)
        return;

    if (mDownload)
        mDownload->cancel();

    mQuitButton->setEnabled(false);
    mConnectButton->setEnabled(false);
    mLoadButton->setEnabled(false);

    ServerInfo server = mServers.at(index);
    mServerInfo->hostname = server.hostname;
    mServerInfo->port = server.port;
    mServerInfo->type = server.type;
    mServerInfo->name = server.name;
    mServerInfo->description = server.description;
    mServerInfo->registerUrl = server.registerUrl;
    mServerInfo->save = true;

    if (chatLogger)
        chatLogger->setServerName(mServerInfo->hostname);

    saveCustomServers(*mServerInfo);

    if (!LoginDialog::savedPasswordKey.empty())
    {
        if (mServerInfo->hostname != LoginDialog::savedPasswordKey)
            LoginDialog::savedPassword.clear();
    }

    config.setValue("usePersistentIP",
        mPersistentIPCheckBox->isSelected());
    Client::setState(STATE_CONNECT_SERVER);
}

void ServerDialog::action(const gcn::ActionEvent &event)
{
    const std::string &eventId = event.getId();
    if (eventId == "connect")
    {
        connectToSelectedServer();
    }
    else if (eventId == "quit")
    {
        close();
    }
    else if (eventId == "load")
    {
        downloadServerList();
    }
    else if (eventId == "addEntry")
    {
        new EditServerDialog(this, ServerInfo(), -1);
    }
    else if (eventId == "editEntry")
    {
        const int index = mServersList->getSelected();
        if (index >= 0)
            new EditServerDialog(this, mServers.at(index), index);
    }
    else if (eventId == "remove")
    {
        const int index = mServersList->getSelected();
        if (index >= 0)
        {
            mServersList->setSelected(0);
            mServers.erase(mServers.begin() + index);
            saveCustomServers();
        }
    }
}

void ServerDialog::keyPressed(gcn::KeyEvent &keyEvent)
{
    switch (static_cast<KeyEvent*>(&keyEvent)->getActionId())
    {
        case Input::KEY_GUI_CANCEL:
            keyEvent.consume();
            Client::setState(STATE_EXIT);
            return;

        case Input::KEY_GUI_SELECT:
        case Input::KEY_GUI_SELECT2:
            keyEvent.consume();
            action(gcn::ActionEvent(nullptr,
                mConnectButton->getActionEventId()));
            return;

        case Input::KEY_GUI_INSERT:
            new EditServerDialog(this, ServerInfo(), -1);
            return;

        case Input::KEY_GUI_DELETE:
        {
            const int index = mServersList->getSelected();
            if (index >= 0)
            {
                mServersList->setSelected(0);
                mServers.erase(mServers.begin() + index);
                saveCustomServers();
            }
            return;
        }

        case Input::KEY_GUI_BACKSPACE:
        {
            const int index = mServersList->getSelected();
            if (index >= 0)
                new EditServerDialog(this, mServers.at(index), index);
            return;
        }

        default:
            break;
    }
    if (!keyEvent.isConsumed())
        mServersList->keyPressed(keyEvent);
}

void ServerDialog::valueChanged(const gcn::SelectionEvent &)
{
    const int index = mServersList->getSelected();
    if (index == -1)
    {
        mDeleteButton->setEnabled(false);
        return;
    }
    mDeleteButton->setEnabled(true);
}

void ServerDialog::mouseClicked(gcn::MouseEvent &mouseEvent)
{
    if (mouseEvent.getClickCount() == 2 &&
        mouseEvent.getSource() == mServersList)
    {
        action(gcn::ActionEvent(mConnectButton,
                                mConnectButton->getActionEventId()));
    }
}

void ServerDialog::logic()
{
    BLOCK_START("ServerDialog::logic")
    {
        MutexLocker tempLock(&mMutex);
        if (mDownloadStatus == DOWNLOADING_COMPLETE)
        {
            mDownloadStatus = DOWNLOADING_OVER;

            mDescription->setCaption(std::string());
        }
        else if (mDownloadStatus == DOWNLOADING_IN_PROGRESS)
        {
            mDescription->setCaption(strprintf(_("Downloading server list..."
                "%2.2f%%"), static_cast<double>(mDownloadProgress * 100)));
        }
        else if (mDownloadStatus == DOWNLOADING_IDLE)
        {
            mDescription->setCaption(_("Waiting for server..."));
        }
        else if (mDownloadStatus == DOWNLOADING_PREPARING)
        {
            mDescription->setCaption(_("Preparing download"));
        }
        else if (mDownloadStatus == DOWNLOADING_ERROR)
        {
            mDescription->setCaption(_("Error retreiving server list!"));
        }
    }

    Window::logic();
    BLOCK_END("ServerDialog::logic")
}

void ServerDialog::downloadServerList()
{
    // Try to load the configuration value for the onlineServerList
    std::string listFile = branding.getStringValue("onlineServerList");

    if (listFile.empty())
        listFile = config.getStringValue("onlineServerList");

    // Fall back to manaplus.evolonline.org when neither branding nor config set it
    if (listFile.empty())
        listFile = "http://manaplus.evolonline.org/serverlist.xml";

    if (mDownload)
    {
        mDownload->cancel();
        delete mDownload;
        mDownload = nullptr;
    }

    mDownload = new Net::Download(this, listFile, &downloadUpdate);
    mDownload->setFile(std::string(mDir).append("/").append(
        branding.getStringValue("onlineServerFile")));
    mDownload->start();

    config.setValue("serverslistupdate", getDateString());
}

void ServerDialog::loadServers(const bool addNew)
{
    XML::Document doc(std::string(mDir).append("/").append(
        branding.getStringValue("onlineServerFile")), false);
    const XmlNodePtr rootNode = doc.rootNode();

    if (!rootNode || !xmlNameEqual(rootNode, "serverlist"))
    {
        logger->log1("Error loading server list!");
        return;
    }

    const int ver = XML::getProperty(rootNode, "version", 0);
    if (ver != 1)
    {
        logger->log("Error: unsupported online server list version: %d",
                    ver);
        return;
    }

    const std::string lang = getLangShort();
    const std::string description2 = "description_" + lang;

    for_each_xml_child_node(serverNode, rootNode)
    {
        if (!xmlNameEqual(serverNode, "server"))
            continue;

        ServerInfo server;

        std::string type = XML::getProperty(serverNode, "type", "unknown");

        server.type = ServerInfo::parseType(type);

        // Ignore unknown server types
        if (server.type == ServerInfo::UNKNOWN)
        {
            logger->log("Ignoring server entry with unknown type: %s",
                        type.c_str());
            continue;
        }

        server.name = XML::getProperty(serverNode, "name", std::string());

        std::string version = XML::getProperty(serverNode, "minimumVersion",
                                               std::string());

        const bool meetsMinimumVersion = (compareStrI(version, SMALL_VERSION)
                                   <= 0);

        // For display in the list
        if (meetsMinimumVersion)
            version.clear();
        else if (version.empty())
            version = _("requires a newer version");
        else
            version = strprintf(_("requires v%s"), version.c_str());

        gcn::Font *font = gui->getFont();

        for_each_xml_child_node(subNode, serverNode)
        {
            if (xmlNameEqual(subNode, "connection"))
            {
                server.hostname = XML::getProperty(subNode, "hostname", "");
                server.port = static_cast<short unsigned>(
                    XML::getProperty(subNode, "port", 0));

                if (server.port == 0)
                {
                    // If no port is given, use the default for the given type
                    server.port = defaultPortForServerType(server.type);
                }
            }
            else if ((xmlNameEqual(subNode, "description")
                     && server.description.empty()) || (!lang.empty()
                     && xmlNameEqual(subNode, description2.c_str())))
            {
                server.description = reinterpret_cast<const char*>(
                    subNode->xmlChildrenNode->content);
            }
            else if (xmlNameEqual(subNode, "registerurl"))
            {
                server.registerUrl = reinterpret_cast<const char*>(
                    subNode->xmlChildrenNode->content);
            }
        }

        server.version.first = font->getWidth(version);
        server.version.second = version;

        MutexLocker tempLock(&mMutex);
        // Add the server to the local list if it's not already present
        bool found = false;
        for (unsigned int i = 0, sz = static_cast<unsigned int>(
             mServers.size()); i < sz; i++)
        {
            if (mServers[i] == server)
            {
                // Use the name listed in the server list
                mServers[i].name = server.name;
                mServers[i].version = server.version;
                mServers[i].description = server.description;
                mServers[i].registerUrl = server.registerUrl;
                mServersListModel->setVersionString(i, version);
                found = true;
                break;
            }
        }
        if (!found && addNew)
            mServers.push_back(server);
    }
    if (mServersList->getSelected() < 0)
        mServersList->setSelected(0);
}

void ServerDialog::loadCustomServers()
{
    for (int i = 0; i < MAX_SERVERLIST; ++i)
    {
        const std::string index = toString(i);
        const std::string nameKey = "MostUsedServerDescName" + index;
        const std::string descKey = "MostUsedServerDescription" + index;
        const std::string hostKey = "MostUsedServerName" + index;
        const std::string typeKey = "MostUsedServerType" + index;
        const std::string portKey = "MostUsedServerPort" + index;

        ServerInfo server;
        server.name = config.getValue(nameKey, "");
        server.description = config.getValue(descKey, "");
        server.hostname = config.getValue(hostKey, "");
        server.type = ServerInfo::parseType(config.getValue(typeKey, ""));

        const int defaultPort = defaultPortForServerType(server.type);
        server.port = static_cast<unsigned short>(
            config.getValue(portKey, defaultPort));

        // skip invalid server
        if (!server.isValid())
            continue;

        server.save = true;
        mServers.push_back(server);
    }
}

void ServerDialog::saveCustomServers(const ServerInfo &currentServer,
                                     const int index)
{
    // Make sure the current server is mentioned first
    if (currentServer.isValid())
    {
        if (index >= 0 && static_cast<unsigned>(index) < mServers.size())
        {
            mServers[index] = currentServer;
        }
        else
        {
            FOR_EACH (ServerInfos::iterator, i, mServers)
            {
                if (*i == currentServer)
                {
                    mServers.erase(i);
                    break;
                }
            }
            mServers.insert(mServers.begin(), currentServer);
        }
    }

    int savedServerCount = 0;

    for (unsigned i = 0, sz = static_cast<unsigned>(mServers.size());
         i < sz && savedServerCount < MAX_SERVERLIST; ++ i)
    {
        const ServerInfo &server = mServers.at(i);

        // Only save servers that were loaded from settings
        if (!(server.save && server.isValid()))
            continue;

        const std::string num = toString(savedServerCount);
        const std::string nameKey = "MostUsedServerDescName" + num;
        const std::string descKey = "MostUsedServerDescription" + num;
        const std::string hostKey = "MostUsedServerName" + num;
        const std::string typeKey = "MostUsedServerType" + num;
        const std::string portKey = "MostUsedServerPort" + num;

        config.setValue(nameKey, toString(server.name));
        config.setValue(descKey, toString(server.description));
        config.setValue(hostKey, toString(server.hostname));
        config.setValue(typeKey, serverTypeToString(server.type));
        config.setValue(portKey, toString(server.port));
        ++ savedServerCount;
    }

    // Insert an invalid entry at the end to make the loading stop there
    if (savedServerCount < MAX_SERVERLIST)
        config.setValue("MostUsedServerName" + toString(savedServerCount), "");
}

int ServerDialog::downloadUpdate(void *ptr, DownloadStatus status,
                                 size_t total, size_t remaining)
{
    if (!ptr || status == DOWNLOAD_STATUS_CANCELLED)
        return -1;

    ServerDialog *const sd = reinterpret_cast<ServerDialog*>(ptr);
    bool finished = false;

    if (!sd->mDownload)
        return -1;

    if (status == DOWNLOAD_STATUS_COMPLETE)
    {
        finished = true;
    }
    else if (status < 0)
    {
        logger->log("Error retreiving server list: %s\n",
                    sd->mDownload->getError());
        sd->mDownloadStatus = DOWNLOADING_ERROR;
    }
    else
    {
        float progress = static_cast<float>(remaining);
        if (total)
            progress /= static_cast<float>(total);

        if (progress != progress)
        {
            progress = 0.0f; // check for NaN
        }
        else if (progress < 0.0f)
        {
            progress = 0.0f; // no idea how this could ever happen,
                             // but why not check for it anyway.
        }
        else if (progress > 1.0f)
        {
            progress = 1.0f;
        }

        MutexLocker lock(&sd->mMutex);
        sd->mDownloadStatus = DOWNLOADING_IN_PROGRESS;
        sd->mDownloadProgress = progress;
    }

    if (finished)
    {
        sd->loadServers();

        MutexLocker lock(&sd->mMutex);
        sd->mDownloadStatus = DOWNLOADING_COMPLETE;
    }

    return 0;
}

void ServerDialog::updateServer(ServerInfo server, const int index)
{
    saveCustomServers(server, index);
}

bool ServerDialog::needUpdateServers() const
{
    if (mServers.empty() || config.getStringValue("serverslistupdate")
        != getDateString())
    {
        return true;
    }

    return false;
}

void ServerDialog::close()
{
    if (mDownload)
        mDownload->cancel();
    Client::setState(STATE_FORCE_QUIT);
    Window::close();
}