/*
 *  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 "net/ea/network.h"

#include "configuration.h"
#include "logger.h"

#include "net/messagehandler.h"

#include "net/eathena/protocol.h"

#include "utils/gettext.h"

#include <assert.h>
#include <sstream>

#include "debug.h"

namespace Ea
{

const unsigned int BUFFER_SIZE = 1000000;
const unsigned int BUFFER_LIMIT = 930000;

int networkThread(void *data)
{
    Network *const network = static_cast<Network *const>(data);

    if (!network || !network->realConnect())
        return -1;

    network->receive();

    return 0;
}

Network::Network() :
    mSocket(nullptr),
    mServer(),
    mInBuffer(new char[BUFFER_SIZE]),
    mOutBuffer(new char[BUFFER_SIZE]),
    mInSize(0),
    mOutSize(0),
    mToSkip(0),
    mState(IDLE),
    mError(),
    mWorkerThread(nullptr),
    mMutex(SDL_CreateMutex()),
    mSleep(config.getIntValue("networksleep"))
{
    TcpNet::init();
}

Network::~Network()
{
    if (mState != IDLE && mState != NET_ERROR)
        disconnect();

    SDL_DestroyMutex(mMutex);
    mMutex = nullptr;

    delete []mInBuffer;
    delete []mOutBuffer;

    TcpNet::quit();
}

bool Network::connect(ServerInfo server)
{
    if (mState != IDLE && mState != NET_ERROR)
    {
        logger->log1("Tried to connect an already connected socket!");
        assert(false);
        return false;
    }

    if (server.hostname.empty())
    {
        setError(_("Empty address given to Network::connect()!"));
        return false;
    }

    logger->log("Network::Connecting to %s:%i",
        server.hostname.c_str(), server.port);

    mServer.hostname = server.hostname;
    mServer.port = server.port;

    // Reset to sane values
    mOutSize = 0;
    mInSize = 0;
    mToSkip = 0;

    mState = CONNECTING;
    mWorkerThread = SDL_CreateThread(networkThread, this);
    if (!mWorkerThread)
    {
        setError("Unable to create network worker thread");
        return false;
    }

    return true;
}

void Network::disconnect()
{
    mState = IDLE;

    if (mWorkerThread && SDL_GetThreadID(mWorkerThread))
    {
        SDL_WaitThread(mWorkerThread, nullptr);
        mWorkerThread = nullptr;
    }

    if (mSocket)
    {
        TcpNet::closeSocket(mSocket);
        mSocket = nullptr;
        if (mSleep > 0)
            SDL_Delay(mSleep);
    }
}

void Network::flush()
{
    if (!mOutSize || mState != CONNECTED)
        return;

    SDL_mutexP(mMutex);
    const int ret = TcpNet::send(mSocket, mOutBuffer, mOutSize);
    DEBUGLOG(std::string("Send ").append(toString(mOutSize)).append(" bytes"));
    if (ret < static_cast<int>(mOutSize))
    {
        setError("Error in TcpNet::send(): " +
            std::string(TcpNet::getError()));
    }
    mOutSize = 0;
    SDL_mutexV(mMutex);
}

void Network::skip(const int len)
{
    SDL_mutexP(mMutex);
    mToSkip += len;
    if (!mInSize)
    {
        SDL_mutexV(mMutex);
        return;
    }

    if (mInSize >= mToSkip)
    {
        mInSize -= mToSkip;
        memmove(mInBuffer, mInBuffer + mToSkip, mInSize);
        mToSkip = 0;
    }
    else
    {
        mToSkip -= mInSize;
        mInSize = 0;
    }
    SDL_mutexV(mMutex);
}

bool Network::realConnect()
{
    IPaddress ipAddress;

    if (TcpNet::resolveHost(&ipAddress, mServer.hostname.c_str(),
        mServer.port) == -1)
    {
        const std::string errorMessage = std::string(
            _("Unable to resolve host \"")).append(
            mServer.hostname).append("\"");
        setError(errorMessage);
        logger->log("TcpNet::ResolveHost: %s", errorMessage.c_str());
        return false;
    }

    mState = CONNECTING;

    mSocket = TcpNet::open(&ipAddress);
    if (!mSocket)
    {
        logger->log("Error in TcpNet::open(): %s", TcpNet::getError());
        setError(TcpNet::getError());
        return false;
    }

    logger->log("Network::Started session with %s:%i",
        ipToString(ipAddress.host), ipAddress.port);

    mState = CONNECTED;
    return true;
}

void Network::receive()
{
    TcpNet::SocketSet set;

    if (!(set = TcpNet::allocSocketSet(1)))
    {
        setError("Error in TcpNet::allocSocketSet(): " +
            std::string(TcpNet::getError()));
        return;
    }

    if (TcpNet::addSocket(set, mSocket) == -1)
    {
        setError("Error in TcpNet::addSocket(): " +
            std::string(TcpNet::getError()));
    }

    while (mState == CONNECTED)
    {
        // TODO Try to get this to block all the time while still being able
        // to escape the loop
        const int numReady = TcpNet::checkSockets(
            set, (static_cast<uint32_t>(500)));
        switch (numReady)
        {
            case -1:
                logger->log1("Error: TcpNet::checkSockets");
                // FALLTHROUGH
            case 0:
                break;

            case 1:
            {
                // Receive data from the socket
                SDL_mutexP(mMutex);
                if (mInSize > BUFFER_LIMIT)
                {
                    SDL_mutexV(mMutex);
                    SDL_Delay(100);
                    continue;
                }

                const int ret = TcpNet::recv(mSocket, mInBuffer + mInSize,
                    BUFFER_SIZE - mInSize);

                if (!ret)
                {
                    // We got disconnected
                    mState = IDLE;
                    logger->log1("Disconnected.");
                }
                else if (ret < 0)
                {
                    setError(_("Connection to server terminated. ") +
                             std::string(TcpNet::getError()));
                }
                else
                {
//                    DEBUGLOG("Receive " + toString(ret) + " bytes");
                    mInSize += ret;
                    if (mToSkip)
                    {
                        if (mInSize >= mToSkip)
                        {
                            mInSize -= mToSkip;
                            memmove(mInBuffer, mInBuffer + mToSkip, mInSize);
                            mToSkip = 0;
                        }
                        else
                        {
                            mToSkip -= mInSize;
                            mInSize = 0;
                        }
                    }
                }
                SDL_mutexV(mMutex);
                break;
            }

            default:
                // more than one socket is ready..
                // this should not happen since we only listen once socket.
                std::stringstream errorStream;
                errorStream << "Error in TcpNet::recv(), " << numReady
                            << " sockets are ready: " << TcpNet::getError();
                setError(errorStream.str());
                break;
        }
    }

    if (TcpNet::delSocket(set, mSocket) == -1)
        logger->log("Error in TcpNet::delSocket(): %s", TcpNet::getError());

    TcpNet::freeSocketSet(set);
}

void Network::setError(const std::string &error)
{
    logger->log("Network error: %s", error.c_str());
    mError = error;
    mState = NET_ERROR;
}

uint16_t Network::readWord(const int pos) const
{
#if SDL_BYTEORDER == SDL_BIG_ENDIAN
    return SDL_Swap16(*reinterpret_cast<uint16_t*>(mInBuffer + (pos)));
#else
    return (*reinterpret_cast<uint16_t*>(mInBuffer + (pos)));
#endif
}

void Network::fixSendBuffer()
{
    if (mOutSize > BUFFER_LIMIT)
    {
        if (mState != CONNECTED)
            mOutSize = 0;
        else
            flush();
    }
}

}  // namespace Ea