/**************************************************************************
**
** Copyright (C) 2012-2013 Digia Plc and/or its subsidiary(-ies).
** Contact: http://www.qt-project.org/legal
**
** This file is part of the Qt Installer Framework.
**
** $QT_BEGIN_LICENSE:LGPL$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and Digia.  For licensing terms and
** conditions see http://qt.digia.com/licensing.  For further information
** use the contact form at http://qt.digia.com/contact-us.
**
** GNU Lesser General Public License Usage
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 2.1 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPL included in the
** packaging of this file.  Please review the following information to
** ensure the GNU Lesser General Public License version 2.1 requirements
** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
**
** In addition, as a special exception, Digia gives you certain additional
** rights.  These rights are described in the Digia Qt LGPL Exception
** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3.0 as published by the Free Software
** Foundation and appearing in the file LICENSE.GPL included in the
** packaging of this file.  Please review the following information to
** ensure the GNU General Public License version 3.0 requirements will be
** met: http://www.gnu.org/copyleft/gpl.html.
**
**
** $QT_END_LICENSE$
**
**************************************************************************/
#include "fsengineserver.h"

#include "utils.h"

#include <QtCore/QCoreApplication>
#include <QtCore/QProcess>
#include <QtCore/QSettings>
#include <QtCore/QStringList>
#include <QtCore/QThread>

#include <QtNetwork/QTcpSocket>

#if QT_VERSION < 0x050000
#  include <QtCore/QFSFileEngine>
#else
#  include <QtCore/private/qfsfileengine_p.h>
#endif

typedef int descriptor_t;

#ifdef Q_OS_WIN
#   include <windows.h>
#endif

bool startDetached(const QString &program, const QStringList &args, const QString &workingDirectory,
    qint64 *pid)
{
#ifdef Q_OS_WIN
    PROCESS_INFORMATION pinfo;
    STARTUPINFOW startupInfo = { sizeof(STARTUPINFO), 0, 0, 0,
        static_cast<ulong>(CW_USEDEFAULT), static_cast<ulong>(CW_USEDEFAULT),
        static_cast<ulong>(CW_USEDEFAULT), static_cast<ulong>(CW_USEDEFAULT),
        0, 0, 0, STARTF_USESHOWWINDOW, SW_HIDE, 0, 0, 0, 0, 0
    };

    const QString arguments = QInstaller::createCommandline(program, args);
    const bool success = CreateProcess(0, (wchar_t*)arguments.utf16(),
        0, 0, false, CREATE_UNICODE_ENVIRONMENT | CREATE_NEW_CONSOLE,
        0, (wchar_t*)workingDirectory.utf16(),
        &startupInfo, &pinfo);

    if (success) {
        CloseHandle(pinfo.hThread);
        CloseHandle(pinfo.hProcess);
        if (pid)
            *pid = pinfo.dwProcessId;
    }

    return success;
#else
    return QProcess::startDetached(program, args, workingDirectory, pid);
#endif
}


class QProcessSignalReceiver : public QObject
{
    Q_OBJECT

public:
    QProcessSignalReceiver(QObject *parent = 0)
        : QObject(parent)
    {
        connect(parent, SIGNAL(finished(int, QProcess::ExitStatus)), this,
            SLOT(processFinished(int, QProcess::ExitStatus)));
        connect(parent, SIGNAL(error(QProcess::ProcessError)), this,
            SLOT(processError(QProcess::ProcessError)));
        connect(parent, SIGNAL(readyRead()), this, SLOT(processReadyRead()));
        connect(parent, SIGNAL(started()), this, SLOT(processStarted()));
        connect(parent, SIGNAL(stateChanged(QProcess::ProcessState)), this,
            SLOT(processStateChanged(QProcess::ProcessState)));
    }

    QList<QVariant> receivedSignals;

private Q_SLOTS:
    void processError(QProcess::ProcessError error);
    void processFinished(int exitCode, QProcess::ExitStatus exitStatus);
    void processReadyRead();
    void processStarted();
    void processStateChanged(QProcess::ProcessState newState);
};

/*!
    \internal
*/
class FSEngineConnectionThread : public QThread
{
    Q_OBJECT
public:
    FSEngineConnectionThread(descriptor_t socketDescriptor, QObject *parent)
        : QThread(parent)
        , descriptor(socketDescriptor)
        , settings(0)
        , process(0)
        , signalReceiver(0)
    {}

protected:
    void run();

private:
    QByteArray handleCommand(const QString &command);

    QFSFileEngine engine;
    const descriptor_t descriptor;
    QDataStream receivedStream;
    QSettings *settings;

    QProcess *process;
    QProcessSignalReceiver *signalReceiver;
};


FSEngineServer::FSEngineServer(quint16 port, QObject *parent)
    : QTcpServer(parent)
{
    listen(QHostAddress::LocalHost, port);
    connect(&watchdog, SIGNAL(timeout()), qApp, SLOT(quit()));
    watchdog.setSingleShot(true);
    watchdog.setInterval(30000);
    watchdog.start();
}

FSEngineServer::FSEngineServer(const QHostAddress &address, quint16 port, QObject *parent)
    : QTcpServer(parent)
{
    listen(address, port);
    connect(&watchdog, SIGNAL(timeout()), qApp, SLOT(quit()));
    watchdog.setSingleShot(true);
    watchdog.setInterval(30000);
    watchdog.start();
}

/*!
    Destroys the FSEngineServer.
*/
FSEngineServer::~FSEngineServer()
{
    const QList<QThread *> threads = findChildren<QThread *>();
    foreach (QThread *thread, threads)
        thread->wait();
}

/*!
    \reimp
*/
void FSEngineServer::incomingConnection(int socketDescriptor)
{
    qApp->processEvents();
    QThread *const thread = new FSEngineConnectionThread(socketDescriptor, this);
    connect(thread, SIGNAL(finished()), thread, SLOT(deleteLater()));
    thread->start();
    watchdog.start();
}

void FSEngineServer::enableTestMode()
{
    setAuthorizationKey(QLatin1String("testAuthorizationKey"));
    //we don't want to kill the server,
    //maybe we should introduce a call where the client can kill the server
    watchdog.disconnect();
}


/*!
    Sets the authorization key this server is asking the clients for to \a authorizationKey.
*/
void FSEngineServer::setAuthorizationKey(const QString &authorizationKey)
{
    key = authorizationKey;
}

QString FSEngineServer::authorizationKey() const
{
    return key;
}

void QProcessSignalReceiver::processError(QProcess::ProcessError error)
{
    receivedSignals.push_back(QLatin1String("error"));
    receivedSignals.push_back(static_cast<int> (error));
}

void QProcessSignalReceiver::processFinished(int exitCode, QProcess::ExitStatus exitStatus)
{
    receivedSignals.push_back(QLatin1String("finished"));
    receivedSignals.push_back(exitCode);
    receivedSignals.push_back(static_cast<int> (exitStatus));
}

void QProcessSignalReceiver::processStarted()
{
    receivedSignals.push_back(QLatin1String("started"));
}

void QProcessSignalReceiver::processReadyRead()
{
    receivedSignals.push_back(QLatin1String("readyRead"));
}

void QProcessSignalReceiver::processStateChanged(QProcess::ProcessState newState)
{
    receivedSignals.push_back(QLatin1String("stateChanged"));
    receivedSignals.push_back(static_cast<int>(newState));
}

/*!
    \reimp
*/
void FSEngineConnectionThread::run()
{
    QTcpSocket socket;
    socket.setSocketDescriptor(descriptor);

    receivedStream.setDevice(&socket);
    receivedStream.setVersion(QDataStream::Qt_4_2);

    bool authorized = false;

    while (static_cast<QAbstractSocket::SocketState>(socket.state()) == QAbstractSocket::ConnectedState) {
        if (!socket.bytesAvailable() && !socket.waitForReadyRead(250))
            continue;

        QString command;
        receivedStream >> command;

        if (authorized && command == QLatin1String("shutdown")) {
            // this is a graceful shutdown
            socket.close();
            parent()->deleteLater();
            return;
        } else if (command == QLatin1String("authorize")) {
            QString k;
            receivedStream >> k;
            if (k != dynamic_cast<FSEngineServer*> (parent())->authorizationKey()) {
                // this is closing the connection... auth failed
                socket.close();
                return;
            }
            authorized = true;
        } else if (authorized) {
            if (command.isEmpty())
                continue;
            const QByteArray result = handleCommand(command);
            receivedStream << static_cast<quint32> (result.size());
            if (!result.isEmpty())
                receivedStream.writeRawData(result.data(), result.size());
        } else {
            // authorization failed, connection not wanted
            socket.close();
            return;
        }
    }
}

static QDataStream &operator<<(QDataStream &stream, const QSettings::Status &status)
{
    return stream << static_cast<int>(status);
}

/*!
    Handles \a command and returns a QByteArray which has the result streamed into it.
*/
QByteArray FSEngineConnectionThread::handleCommand(const QString &command)
{
    QByteArray block;
    QDataStream returnStream(&block, QIODevice::WriteOnly);
    returnStream.setVersion(QDataStream::Qt_4_2);

    // first, QSettings handling
    if (command == QLatin1String("createQSettings")) {
        QString fileName;
        receivedStream >> fileName;
        settings = new QSettings(fileName, QSettings::NativeFormat);
    } else if (command == QLatin1String("destroyQSettings")) {
        delete settings;
        settings = 0;
    } else if (command == QLatin1String("QSettings::allKeys")) {
        returnStream << settings->allKeys();
    } else if (command == QLatin1String("QSettings::beginGroup")) {
        QString prefix;
        receivedStream >> prefix;
        settings->beginGroup(prefix);
    } else if (command == QLatin1String("QSettings::beginReadArray")) {
        QString prefix;
        int size;
        receivedStream >> prefix;
        receivedStream >> size;
        settings->beginWriteArray(prefix, size);
    } else if (command == QLatin1String("QSettings::beginWriteArray")) {
        QString prefix;
        receivedStream >> prefix;
        returnStream << settings->beginReadArray(prefix);
    } else if (command == QLatin1String("QSettings::childGroups")) {
        returnStream << settings->childGroups();
    } else if (command == QLatin1String("QSettings::childKeys")) {
        returnStream << settings->childKeys();
    } else if (command == QLatin1String("QSettings::clear")) {
        settings->clear();
    } else if (command == QLatin1String("QSettings::contains")) {
        QString key;
        receivedStream >> key;
        returnStream << settings->contains(key);
    } else if (command == QLatin1String("QSettings::endArray")) {
        settings->endArray();
    } else if (command == QLatin1String("QSettings::endGroup")) {
        settings->endGroup();
    } else if (command == QLatin1String("QSettings::fallbacksEnabled")) {
        returnStream << settings->fallbacksEnabled();
    } else if (command == QLatin1String("QSettings::fileName")) {
        returnStream << settings->fileName();
    } else if (command == QLatin1String("QSettings::group")) {
        returnStream << settings->group();
    } else if (command == QLatin1String("QSettings::isWritable")) {
        returnStream << settings->isWritable();
    } else if (command == QLatin1String("QSettings::remove")) {
        QString key;
        receivedStream >> key;
        settings->remove(key);
    } else if (command == QLatin1String("QSettings::setArrayIndex")) {
        int i;
        receivedStream >> i;
        settings->setArrayIndex(i);
    } else if (command == QLatin1String("QSettings::setFallbacksEnabled")) {
        bool b;
        receivedStream >> b;
        settings->setFallbacksEnabled(b);
    } else if (command == QLatin1String("QSettings::status")) {
        returnStream << settings->status();
    } else if (command == QLatin1String("QSettings::sync")) {
        settings->sync();
    } else if (command == QLatin1String("QSettings::setValue")) {
        QString key;
        QVariant value;
        receivedStream >> key;
        receivedStream >> value;
        settings->setValue(key, value);
    } else if (command == QLatin1String("QSettings::value")) {
        QString key;
        QVariant defaultValue;
        receivedStream >> key;
        receivedStream >> defaultValue;
        returnStream << settings->value(key, defaultValue);
    }

    // from here, QProcess handling
    else if (command == QLatin1String("createQProcess")) {
        process = new QProcess;
        signalReceiver = new QProcessSignalReceiver(process);
    } else if (command == QLatin1String("destroyQProcess")) {
        signalReceiver->receivedSignals.clear();
        process->deleteLater();
        process = 0;
    } else if (command == QLatin1String("getQProcessSignals")) {
        returnStream << signalReceiver->receivedSignals;
        signalReceiver->receivedSignals.clear();
        qApp->processEvents();
    } else if (command == QLatin1String("QProcess::closeWriteChannel")) {
        process->closeWriteChannel();
    } else if (command == QLatin1String("QProcess::exitCode")) {
        returnStream << process->exitCode();
    } else if (command == QLatin1String("QProcess::exitStatus")) {
        returnStream << static_cast<int> (process->exitStatus());
    } else if (command == QLatin1String("QProcess::kill")) {
        process->kill();
    } else if (command == QLatin1String("QProcess::readAll")) {
        returnStream << process->readAll();
    } else if (command == QLatin1String("QProcess::readAllStandardOutput")) {
        returnStream << process->readAllStandardOutput();
    } else if (command == QLatin1String("QProcess::readAllStandardError")) {
        returnStream << process->readAllStandardError();
    } else if (command == QLatin1String("QProcess::startDetached")) {
        QString program;
        QStringList arguments;
        QString workingDirectory;
        receivedStream >> program;
        receivedStream >> arguments;
        receivedStream >> workingDirectory;
        qint64 pid;
        const bool result = startDetached(program, arguments, workingDirectory, &pid);
        returnStream << qMakePair< bool, qint64> (result, pid);
    } else if (command == QLatin1String("QProcess::setWorkingDirectory")) {
        QString dir;
        receivedStream >> dir;
        process->setWorkingDirectory(dir);
    } else if (command == QLatin1String("QProcess::setEnvironment")) {
        QStringList env;
        receivedStream >> env;
        process->setEnvironment(env);
    } else if (command == QLatin1String("QProcess::start")) {
        QString program;
        QStringList arguments;
        int mode;
        receivedStream >> program;
        receivedStream >> arguments;
        receivedStream >> mode;
        process->start(program, arguments, static_cast<QIODevice::OpenMode> (mode));
    } else if (command == QLatin1String("QProcess::state")) {
        returnStream << static_cast<int> (process->state());
    } else if (command == QLatin1String("QProcess::terminate")) {
        process->terminate();
    } else if (command == QLatin1String("QProcess::waitForFinished")) {
        int msecs;
        receivedStream >> msecs;
        returnStream << process->waitForFinished(msecs);
    } else if (command == QLatin1String("QProcess::waitForStarted")) {
        int msecs;
        receivedStream >> msecs;
        returnStream << process->waitForStarted(msecs);
    } else if (command == QLatin1String("QProcess::workingDirectory")) {
        returnStream << process->workingDirectory();
    } else if (command == QLatin1String("QProcess::errorString")) {
        returnStream << process->errorString();
    } else if (command == QLatin1String("QProcess::write")) {
        QByteArray byteArray;
        receivedStream >> byteArray;
        returnStream << process->write(byteArray);
    } else if (command == QLatin1String("QProcess::readChannel")) {
        returnStream << static_cast<int> (process->readChannel());
    } else if (command == QLatin1String("QProcess::setReadChannel")) {
        int processChannel;
        receivedStream >> processChannel;
        process->setReadChannel(static_cast<QProcess::ProcessChannel>(processChannel));
    } else if (command == QLatin1String("QProcess::write")) {
        QByteArray byteArray;
        receivedStream >> byteArray;
        returnStream << process->write(byteArray);
    }

    // from here, QFSEngine handling
    else if (command == QLatin1String("QFSFileEngine::atEnd")) {
        returnStream << engine.atEnd();
    } else if (command == QLatin1String("QFSFileEngine::caseSensitive")) {
        returnStream << engine.caseSensitive();
    } else if (command == QLatin1String("QFSFileEngine::close")) {
        returnStream << engine.close();
    } else if (command == QLatin1String("QFSFileEngine::copy")) {
        QString newName;
        receivedStream >> newName;
        returnStream << engine.copy(newName);
    } else if (command == QLatin1String("QFSFileEngine::entryList")) {
        int filters;
        QStringList filterNames;
        receivedStream >> filters;
        receivedStream >> filterNames;
        returnStream << engine.entryList(static_cast<QDir::Filters> (filters), filterNames);
    } else if (command == QLatin1String("QFSFileEngine::error")) {
        returnStream << static_cast<int> (engine.error());
    } else if (command == QLatin1String("QFSFileEngine::errorString")) {
        returnStream << engine.errorString();
    }
    // extension
    else if (command == QLatin1String("QFSFileEngine::fileFlags")) {
        int flags;
        receivedStream >> flags;
        returnStream << static_cast<int>(engine.fileFlags(static_cast<QAbstractFileEngine::FileFlags>(flags)));
    } else if (command == QLatin1String("QFSFileEngine::fileName")) {
        int file;
        receivedStream >> file;
        returnStream << engine.fileName(static_cast<QAbstractFileEngine::FileName> (file));
    } else if (command == QLatin1String("QFSFileEngine::flush")) {
        returnStream << engine.flush();
    } else if (command == QLatin1String("QFSFileEngine::handle")) {
        returnStream << engine.handle();
    } else if (command == QLatin1String("QFSFileEngine::isRelativePath")) {
        returnStream << engine.isRelativePath();
    } else if (command == QLatin1String("QFSFileEngine::isSequential")) {
        returnStream << engine.isSequential();
    } else if (command == QLatin1String("QFSFileEngine::link")) {
        QString newName;
        receivedStream >> newName;
        returnStream << engine.link(newName);
    } else if (command == QLatin1String("QFSFileEngine::mkdir")) {
        QString dirName;
        bool createParentDirectories;
        receivedStream >> dirName;
        receivedStream >> createParentDirectories;
        returnStream << engine.mkdir(dirName, createParentDirectories);
    } else if (command == QLatin1String("QFSFileEngine::open")) {
        int openMode;
        receivedStream >> openMode;
        returnStream << engine.open(static_cast<QIODevice::OpenMode> (openMode));
    } else if (command == QLatin1String("QFSFileEngine::owner")) {
        int owner;
        receivedStream >> owner;
        returnStream << engine.owner(static_cast<QAbstractFileEngine::FileOwner> (owner));
    } else if (command == QLatin1String("QFSFileEngine::ownerId")) {
        int owner;
        receivedStream >> owner;
        returnStream << engine.ownerId(static_cast<QAbstractFileEngine::FileOwner> (owner));
    } else if (command == QLatin1String("QFSFileEngine::pos")) {
        returnStream << engine.pos();
    } else if (command == QLatin1String("QFSFileEngine::read")) {
        qint64 maxlen;
        receivedStream >> maxlen;
        QByteArray ba(maxlen, '\0');
        const qint64 result = engine.read(ba.data(), maxlen);
        returnStream << result;
        int written = 0;
        while (written < result)
            written += returnStream.writeRawData(ba.data() + written, result - written);
    } else if (command == QLatin1String("QFSFileEngine::readLine")) {
        qint64 maxlen;
        receivedStream >> maxlen;
        QByteArray ba(maxlen, '\0');
        const qint64 result = engine.readLine(ba.data(), maxlen);
        returnStream << result;
        int written = 0;
        while (written < result)
            written += returnStream.writeRawData(ba.data() + written, result - written);
    } else if (command == QLatin1String("QFSFileEngine::remove")) {
        returnStream << engine.remove();
    } else if (command == QLatin1String("QFSFileEngine::rename")) {
        QString newName;
        receivedStream >> newName;
        returnStream << engine.rename(newName);
    } else if (command == QLatin1String("QFSFileEngine::rmdir")) {
        QString dirName;
        bool recurseParentDirectories;
        receivedStream >> dirName;
        receivedStream >> recurseParentDirectories;
        returnStream << engine.rmdir(dirName, recurseParentDirectories);
    } else if (command == QLatin1String("QFSFileEngine::seek")) {
        quint64 offset;
        receivedStream >> offset;
        returnStream << engine.seek(offset);
    } else if (command == QLatin1String("QFSFileEngine::setFileName")) {
        QString fileName;
        receivedStream >> fileName;
        engine.setFileName(fileName);
    } else if (command == QLatin1String("QFSFileEngine::setPermissions")) {
        uint perms;
        receivedStream >> perms;
        returnStream << engine.setPermissions(perms);
    } else if (command == QLatin1String("QFSFileEngine::setSize")) {
        qint64 size;
        receivedStream >> size;
        returnStream << engine.setSize(size);
    } else if (command == QLatin1String("QFSFileEngine::size")) {
        returnStream << engine.size();
    } else if (command == QLatin1String("QFSFileEngine::supportsExtension")) {
        int extension;
        receivedStream >> extension;
        //returnStream << engine.supportsExtension(static_cast<QAbstractFileEngine::Extension> (extension));
        returnStream << false;
    } else if (command == QLatin1String("QFSFileEngine::write")) {
        qint64 length;
        receivedStream >> length;
        qint64 read = 0;
        qint64 written = 0;
        QByteArray buffer(65536, '\0');
        while (read < length) {
            if (!receivedStream.device()->bytesAvailable())
                receivedStream.device()->waitForReadyRead(-1);
            const qint64 r = receivedStream.readRawData(buffer.data(), qMin(length - read,
                static_cast<qint64> (buffer.length())));
            read += r;
            qint64 w = 0;
            while (w < r)
                w += engine.write(buffer.data(), r);
            written += r;
        }
        returnStream << written;
    } else if (!command.isEmpty()) {
        qDebug() << "unknown command:" << command;
    }

    return block;
}

#include "fsengineserver.moc"
#include "moc_fsengineserver.cpp"
