/*  -*- C++ -*-
 *  Copyright (C) 2003 Thiago Macieira <thiago@kde.org>
 *
 *
 *  Permission is hereby granted, free of charge, to any person obtaining
 *  a copy of this software and associated documentation files (the
 *  "Software"), to deal in the Software without restriction, including
 *  without limitation the rights to use, copy, modify, merge, publish,
 *  distribute, sublicense, and/or sell copies of the Software, and to
 *  permit persons to whom the Software is furnished to do so, subject to
 *  the following conditions:
 *
 *  The above copyright notice and this permission notice shall be included
 *  in all copies or substantial portions of the Software.
 *
 *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 *  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 *  MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 *  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
 *  LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
 *  OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
 *  WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

#include "k3streamsocket.h"

#include <config-network.h>

#include <QSocketNotifier>
#include <QDateTime>
#include <QTimer>
#include <QPointer>

#include "k3socketaddress.h"
#include "k3resolver.h"
#include "k3socketdevice.h"

using namespace KNetwork;

class KNetwork::KStreamSocketPrivate
{
public:
    KResolverResults::ConstIterator local, peer;
    QTime startTime;
    QTimer timer;

    int timeout;

    inline KStreamSocketPrivate()
        : timeout(0)
    { }
};

KStreamSocket::KStreamSocket(const QString &node, const QString &service,
                             QObject *parent)
    : KClientSocketBase(parent), d(new KStreamSocketPrivate)
{
    peerResolver().setNodeName(node);
    peerResolver().setServiceName(service);
    peerResolver().setFamily(KResolver::KnownFamily);
    localResolver().setFamily(KResolver::KnownFamily);

    setSocketOptions(socketOptions() & ~Blocking);

    QObject::connect(&d->timer, SIGNAL(timeout()), this, SLOT(timeoutSlot()));
}

KStreamSocket::~KStreamSocket()
{
    delete d;
    // KClientSocketBase's destructor closes the socket
}

int KStreamSocket::timeout() const
{
    return d->timeout;
}

int KStreamSocket::remainingTimeout() const
{
    if (state() != Connecting) {
        return timeout();
    }
    if (timeout() <= 0) {
        return 0;
    }

    return timeout() - d->startTime.elapsed();
}

void KStreamSocket::setTimeout(int msecs)
{
    d->timeout = msecs;

    if (state() == Connecting) {
        d->timer.start(msecs);
    }
}

bool KStreamSocket::bind(const QString &node, const QString &service)
{
    if (state() != Idle) {
        return false;
    }

    if (!node.isNull()) {
        localResolver().setNodeName(node);
    }
    if (!service.isNull()) {
        localResolver().setServiceName(service);
    }
    return true;
}

bool KStreamSocket::bind(const KResolverEntry &entry)
{
    return KClientSocketBase::bind(entry);
}

bool KStreamSocket::connect(const QString &node, const QString &service,
                            OpenMode mode)
{
    Q_UNUSED(mode);
    if (state() == Connected) {
        return true;    // already connected
    }

    if (state() > Connected) {
        return false;    // can't do much here
    }

    if (!node.isNull()) {
        peerResolver().setNodeName(node);
    }
    if (!service.isNull()) {
        peerResolver().setServiceName(service);
    }

    if (state() == Connecting && !blocking()) {
        setError(InProgress);
        emit gotError(InProgress);
        return true;      // we're already connecting
    }

    if (state() < HostFound) {
        // connection hasn't started yet
        if (!blocking()) {
            QObject::connect(this, SIGNAL(hostFound()), SLOT(hostFoundSlot()));
            return lookup();
        }

        // blocking mode
        if (!lookup()) {
            return false;    // lookup failure
        }
    }

    /*
     * lookup results are available here
     */

    if (timeout() > 0) {
        if (!blocking() && !d->timer.isActive()) {
            d->timer.setSingleShot(true);
            d->timer.start(timeout());
        } else {
            // blocking connection with timeout
            // this must be handled as a special case because it requires a
            // non-blocking socket

            d->timer.stop();  // no need for a timer here

            socketDevice()->setBlocking(false);
            while (true) {
                connectionEvent();
                if (state() < Connecting) {
                    return false;    // error connecting
                }
                if (state() == Connected) {
                    return true;    // connected!
                }

                if (remainingTimeout() <= 0) {
                    // we've timed out
                    timeoutSlot();
                    return false;
                }

                if (socketDevice()->error() == InProgress) {
                    bool timedout;
                    socketDevice()->poll(remainingTimeout(), &timedout);
                    if (timedout) {
                        timeoutSlot();
                        return false;
                    }
                }
            }
        }
    }

    connectionEvent();
    return error() == NoError;
}

bool KStreamSocket::connect(const KResolverEntry &entry, OpenMode mode)
{
    return KClientSocketBase::connect(entry, mode);
}

void KStreamSocket::hostFoundSlot()
{
    QObject::disconnect(this, SLOT(hostFoundSlot()));
    if (timeout() > 0) {
        d->timer.setSingleShot(true);
        d->timer.start(timeout());
    }
    QTimer::singleShot(0, this, SLOT(connectionEvent()));
}

void KStreamSocket::connectionEvent()
{
    if (state() != HostFound && state() != Connecting) {
        return;    // nothing to do
    }

    const KResolverResults &peer = peerResults();
    if (state() == HostFound) {
        d->startTime.start();

        setState(Connecting);
        emit stateChanged(Connecting);
        d->peer = peer.begin();
        d->local = localResults().begin(); // just to be on the safe side
    }

    while (d->peer != peer.end()) {
        const KResolverEntry &r = *d->peer;

        if (socketDevice()->socket() != -1) {
            // we have an existing file descriptor
            // this means that we've got activity in it (connection result)
            if (socketDevice()->connect(r) && socketDevice()->error() == NoError) {
                // yes, it did connect!
                connectionSucceeded(r);
                return;
            } else if (socketDevice()->error() == InProgress)
                // nope, still in progress
            {
                return;
            }

            // no, the socket failed to connect
            copyError();
            socketDevice()->close();
            ++d->peer;
            continue;
        }

        // try to bind
        if (!bindLocallyFor(r)) {
            // could not find a matching family
            ++d->peer;
            continue;
        }

        {
            bool skip = false;
            emit aboutToConnect(r, skip);
            if (skip) {
                ++d->peer;
                continue;
            }
        }

        if (socketDevice()->connect(r) || socketDevice()->error() == InProgress) {
            // socket is attempting to connect
            if (socketDevice()->error() == InProgress) {
                QSocketNotifier *n = socketDevice()->readNotifier();
                QObject::connect(n, SIGNAL(activated(int)),
                                 this, SLOT(connectionEvent()));
                n->setEnabled(true);

                n = socketDevice()->writeNotifier();
                QObject::connect(n, SIGNAL(activated(int)),
                                 this, SLOT(connectionEvent()));
                n->setEnabled(true);

                return;       // wait for activity
            }

            // socket has connected
            connectionSucceeded(r);
            return;
        }

        // connection failed
        // try next
        copyError();
        socketDevice()->close();
        ++d->peer;
    }

    // that was the last item
    socketDevice()->setSocketOptions(socketOptions());
    setState(Idle);
    emit stateChanged(Idle);
    emit gotError(error());
    return;
}

void KStreamSocket::timeoutSlot()
{
    if (state() != Connecting) {
        return;
    }

    // halt the connections
    socketDevice()->close();  // this also kills the notifiers

    setError(Timeout);
    setState(HostFound);
    emit stateChanged(HostFound);

    QPointer<KStreamSocket> that = this;
    emit gotError(Timeout);

    if (!that.isNull()) {
        emit timedOut();
    }
}

bool KStreamSocket::bindLocallyFor(const KResolverEntry &peer)
{
    const KResolverResults &local = localResults();

    if (local.isEmpty())
        // user doesn't want to bind to any specific local address
    {
        return true;
    }

    bool foundone = false;
    // scan the local resolution for a matching family
    for (d->local = local.begin(); d->local != local.end(); ++d->local)
        if ((*d->local).family() == peer.family()) {
            // found a suitable address!
            foundone = true;

            if (socketDevice()->bind(*d->local)) {
                return true;
            }
        }

    if (!foundone) {
        // found nothing
        setError(NotSupported);
        emit gotError(NotSupported);
    } else {
        copyError();
    }
    return false;
}

void KStreamSocket::connectionSucceeded(const KResolverEntry &peer)
{
    QObject::disconnect(socketDevice()->readNotifier(), 0, this, SLOT(connectionEvent()));
    QObject::disconnect(socketDevice()->writeNotifier(), 0, this, SLOT(connectionEvent()));

    resetError();
    KActiveSocketBase::open(ReadWrite | Unbuffered);
    setState(Connected);
    socketDevice()->setSocketOptions(socketOptions());
    d->timer.stop();
    emit stateChanged(Connected);

    if (!localResults().isEmpty()) {
        emit bound(*d->local);
    }
    emit connected(peer);
}

