/*
    QMPlay2 is a video and audio player.
    Copyright (C) 2010-2021  Błażej Szczygieł

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Lesser General Public License as published
    by the Free Software Foundation, either version 3 of the License, or
    (at your option) 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 Lesser General Public License for more details.

    You should have received a copy of the GNU Lesser General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

#include "IPC.hpp"

#include <QThread>

#include <windows.h>

class EventNotifier : public QThread
{
public:
    EventNotifier(HANDLE event, QObject *parent) :
        QThread(parent),
        m_event(event)
    {}

    void setEnabled()
    {
        if (!isRunning())
            start();
    }

private:
    void run() override
    {
        WaitForSingleObject(m_event, INFINITE);
    }

    HANDLE m_event;
};

/**/

class PipeData
{
public:
    void closePipe()
    {
        if (hPipe != INVALID_HANDLE_VALUE)
        {
            //Close the pipe before deleting overlapped and before deleting "eventNotifier" to be sure that event is signaled.
            CloseHandle(hPipe);
            hPipe = INVALID_HANDLE_VALUE;

            if (eventNotifier)
            {
                eventNotifier->wait();
                delete eventNotifier;
                eventNotifier = NULL;
            }

            if (ov)
            {
                if (ov->hEvent)
                    CloseHandle(ov->hEvent);
                delete ov;
                ov = NULL;
            }
        }
    }

    EventNotifier *eventNotifier;
    OVERLAPPED *ov;
    HANDLE hPipe;
};

class IPCSocketPriv : public PipeData
{
public:
    inline IPCSocketPriv(const QString &fileName, EventNotifier *eventNotifier = NULL, OVERLAPPED *ov = NULL, HANDLE hPipe = INVALID_HANDLE_VALUE) :
        fileName(fileName),
        bufferPos(-1)
    {
        this->eventNotifier = eventNotifier;
        this->ov = ov;
        this->hPipe = hPipe;
    }

    QString fileName;
    QByteArray buffer;
    int bufferPos;
};

class IPCServerPriv : public PipeData
{
public:
    inline IPCServerPriv(const QString &fileName) :
        fileName(fileName)
    {
        eventNotifier = NULL;
        ov = NULL;
        hPipe = INVALID_HANDLE_VALUE;
    }

    QString fileName;
};

/**/

static bool connectToNewClient(HANDLE hPipe, OVERLAPPED *ov)
{
    if (ConnectNamedPipe(hPipe, ov) == 0)
    {
        switch (GetLastError())
        {
            case ERROR_IO_PENDING:
                ResetEvent(ov->hEvent);
            case ERROR_PIPE_CONNECTED:
                return true;
        }
    }
    return false;
}

/**/

IPCSocket::IPCSocket(const QString &fileName, QObject *parent) :
    QIODevice(parent),
    m_priv(new IPCSocketPriv(fileName))
{}
IPCSocket::~IPCSocket()
{
    close();
    delete m_priv;
}

bool IPCSocket::isConnected() const
{
    return m_priv->hPipe != INVALID_HANDLE_VALUE;
}

bool IPCSocket::open(QIODevice::OpenMode mode)
{
    if (!m_priv->fileName.isEmpty())
    {
        for (;;)
        {
            m_priv->hPipe = CreateFileW((const wchar_t *)m_priv->fileName.utf16(), GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);
            if (m_priv->hPipe != INVALID_HANDLE_VALUE)
                break;
            if (GetLastError() != ERROR_PIPE_BUSY || !WaitNamedPipeW((const wchar_t *)m_priv->fileName.utf16(), 1000))
                break;
        }
    }

    if (m_priv->hPipe != INVALID_HANDLE_VALUE)
    {
        if (m_priv->eventNotifier) //Writing in a socket (open by file name) is blocking-only
        {
            connect(m_priv->eventNotifier, SIGNAL(finished()), this, SLOT(socketReadActive()));
            m_priv->eventNotifier->setEnabled();
        }
        return QIODevice::open(mode);
    }

    return false;
}

void IPCSocket::socketReadActive()
{
    DWORD bytes = 0;
    const bool result = GetOverlappedResult(m_priv->hPipe, m_priv->ov, &bytes, false);
    if (result)
    {
        int pos;
        if (m_priv->bufferPos > -1) //Pending
        {
            pos = m_priv->bufferPos;
            m_priv->bufferPos = -1;
        }
        else //Not pending
        {
            pos = m_priv->buffer.size();
            m_priv->buffer.resize(pos + 1024);
            const bool readOK = ReadFile(m_priv->hPipe, m_priv->buffer.data() + pos, 1024, &bytes, m_priv->ov);
            if (!readOK)
            {
                if (GetLastError() == ERROR_IO_PENDING)
                    m_priv->bufferPos = pos;
                else
                    pos = -1; //Error, e.g. "ERROR_BROKEN_PIPE"
            }
        }
        if (pos > -1)
        {
            if (m_priv->bufferPos <= -1) //Not pending
            {
                m_priv->buffer.resize(pos + bytes);
                if (!m_priv->buffer.isEmpty())
                    emit readyRead();
            }
            m_priv->eventNotifier->setEnabled();
            return;
        }
    }

    emit aboutToClose();
    close();
}

IPCSocket::IPCSocket(const PipeData &pipeData, QObject *parent) :
    QIODevice(parent),
    m_priv(new IPCSocketPriv(QString(), pipeData.eventNotifier, pipeData.ov, pipeData.hPipe))
{
    if (m_priv->eventNotifier->parent() == parent)
        m_priv->eventNotifier->setParent(this);
}

void IPCSocket::close()
{
    m_priv->closePipe();
}

qint64 IPCSocket::readData(char *data, qint64 maxSize)
{
    if (maxSize >= 0 && isOpen())
    {
        if (maxSize == 0)
            return 0;
        const int toRead = qMin<qint64>(maxSize, m_priv->buffer.size());
        memcpy(data, m_priv->buffer.constData(), toRead);
        if (toRead == m_priv->buffer.size())
            m_priv->buffer.clear();
        else
            m_priv->buffer.remove(0, toRead);
        return toRead;
    }
    return -1;
}
qint64 IPCSocket::writeData(const char *data, qint64 maxSize)
{
    if (isOpen())
    {
        DWORD written = 0;
        if (WriteFile(m_priv->hPipe, data, maxSize, &written, NULL))
            return written;
    }
    return -1;
}

/**/

IPCServer::IPCServer(const QString &fileName, QObject *parent) :
    QObject(parent),
    m_priv(new IPCServerPriv(fileName))
{}
IPCServer::~IPCServer()
{
    close();
    delete m_priv;
}

bool IPCServer::listen()
{
    m_priv->hPipe = CreateNamedPipeW
    (
        (const wchar_t *)m_priv->fileName.utf16(),
        PIPE_ACCESS_INBOUND | FILE_FLAG_OVERLAPPED,
        PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT,
        PIPE_UNLIMITED_INSTANCES,
        0,
        0,
        0,
        NULL
    );
    if (m_priv->hPipe != INVALID_HANDLE_VALUE)
    {
        m_priv->ov = new OVERLAPPED();
        m_priv->ov->hEvent = CreateEvent(NULL, true, true, NULL);
        if (m_priv->ov->hEvent != NULL && connectToNewClient(m_priv->hPipe, m_priv->ov))
        {
            m_priv->eventNotifier = new EventNotifier(m_priv->ov->hEvent, this);
            connect(m_priv->eventNotifier, SIGNAL(finished()), this, SLOT(socketAcceptActive()));
            m_priv->eventNotifier->setEnabled();
            return true;
        }
        close();
    }
    return false;
}

void IPCServer::close()
{
    m_priv->closePipe();
}

void IPCServer::socketAcceptActive()
{
    bool hasOldPipe = false;
    PipeData oldPipe;

    DWORD bytes = 0;
    disconnect(m_priv->eventNotifier, SIGNAL(finished()), this, SLOT(socketAcceptActive()));
    if (GetOverlappedResult(m_priv->hPipe, m_priv->ov, &bytes, false) && bytes == 0)
    {
        oldPipe = *m_priv;
        hasOldPipe = true;
    }
    else
    {
        close();
    }

    //Listen for new connection
    m_priv->eventNotifier = NULL;
    m_priv->ov = NULL;
    m_priv->hPipe = INVALID_HANDLE_VALUE;
    listen();

    if (hasOldPipe)
    {
        //Accept current connection
        IPCSocket *socket = new IPCSocket(oldPipe, this);
        if (socket->open(IPCSocket::ReadWrite))
            emit newConnection(socket);
        else
            socket->deleteLater();
    }
}
