/****************************************************************************
 **
 ** Copyright (C) Qxt Foundation. Some rights reserved.
 **
 ** This file is part of the QxtCore module of the Qxt library.
 **
 ** This library is free software; you can redistribute it and/or modify it
 ** under the terms of the Common Public License, version 1.0, as published
 ** by IBM, and/or under the terms of the GNU Lesser General Public License,
 ** version 2.1, as published by the Free Software Foundation.
 **
 ** This file is provided "AS IS", without WARRANTIES OR CONDITIONS OF ANY
 ** KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY
 ** WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR
 ** FITNESS FOR A PARTICULAR PURPOSE.
 **
 ** You should have received a copy of the CPL and the LGPL along with this
 ** file. See the LICENSE file and the cpl1.0.txt/lgpl-2.1.txt files
 ** included with the source distribution for more information.
 ** If you did not receive a copy of the licenses, contact the Qxt Foundation.
 **
 ** <http://libqxt.org>  <foundation@libqxt.org>
 **
 ****************************************************************************/
#include "qxtfilelock.h"
#include "qxtfilelock_p.h"

#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
/*include pthread to make errno threadsafe*/
#include <pthread.h>
#include <errno.h>
#include <QPointer>
#include <QMutableLinkedListIterator>
#include <QDebug>

/*!
 * \internal this class is used on *nix to register all locks created by a process and to let locks on *nix act like locks on windows
 */
class QxtFileLockRegistry
{
public:
    bool registerLock(QxtFileLock *lock);
    bool removeLock(QxtFileLock *lock);
    static QxtFileLockRegistry& instance();

private:
    QLinkedList < QPointer<QxtFileLock> > procLocks;
    QMutex registryMutex;
    QxtFileLockRegistry();
};

QxtFileLockRegistry::QxtFileLockRegistry()
{}

QxtFileLockRegistry& QxtFileLockRegistry::instance()
{
    static QxtFileLockRegistry instance;
    return instance;
}

/*!
 * \internal this function locks the lockregistry and checks if there is a collision between the process locks
 * \internal if there is no collision it inserts the lock into the registry and returns
 * \internal return true for success
 */
bool QxtFileLockRegistry::registerLock(QxtFileLock * lock)
{
    QMutexLocker locker(&this->registryMutex);

    QFile *fileToLock = lock ->file();

    if (fileToLock)
    {
        struct stat fileInfo;
        if (fstat(fileToLock->handle(), &fileInfo) < 0)
            return false;

        int newLockStart = lock ->offset();
        int newLockEnd = lock ->offset() + lock ->length();

        QMutableLinkedListIterator< QPointer<QxtFileLock> >iterator(this->procLocks);

        while (iterator.hasNext())
        {
            QPointer<QxtFileLock> currLock = iterator.next();
            if (currLock && currLock->file() && currLock->file()->isOpen())
            {
                struct stat currFileInfo;

                /*first check if the current lock is on the same file*/
                if (fstat(currLock->file()->handle(), &currFileInfo) < 0)
                {
                    /*that should never happen because a closing file should remove all locks*/
                    Q_ASSERT(false);
                    continue;
                }

                if (currFileInfo.st_dev == fileInfo.st_dev && currFileInfo.st_ino == fileInfo.st_ino)
                {
                    /*same file, check if our locks are in conflict*/
                    int currLockStart = currLock->offset();
                    int currLockEnd = currLock->offset() + currLock->length();

                    /*do we have to check for threads here?*/
                    if (newLockEnd >= currLockStart  && newLockStart <= currLockEnd)
                    {
                        //qDebug() << "we may have a collision";
                        //qDebug() << newLockEnd << " >= " << currLockStart << "  &&  " << newLockStart << " <= " << currLockEnd;

                        /*same lock region if one of both locks are exclusive we have a collision*/
                        if (lock ->mode() == QxtFileLock::WriteLockWait || lock ->mode() == QxtFileLock::WriteLock ||
                                currLock->mode() == QxtFileLock::WriteLockWait || currLock->mode() == QxtFileLock::WriteLock)
                        {
                            /*FIXED BUG #6 test rw_same() passes on Unix
                              keeping the old code for a while
                              */
                            return false;
#if 0
                           //qDebug() << "Okay if this is not the same thread using the same handle there is a collision";
                            /*the same thread  can lock the same region with the same handle*/

                            //qDebug() << "! (" << lock ->thread() << " == " << currLock->thread() << " && " << lock ->file()->handle() << " == " << currLock->file()->handle() << ")";

                            if (!(lock ->thread() == currLock->thread() && lock ->file()->handle() == currLock->file()->handle()))
                            {
                                qDebug() << "Collision";
                                return false;
                            }
#endif
                        }
                    }
                }
            }
            else //remove dead locks
                iterator.remove();
        }
        //qDebug() << "The lock is okay";
        /*here we can insert the lock into the list and return*/
        procLocks.append(QPointer<QxtFileLock>(lock));
        return true;

    }

    return false;
}

bool QxtFileLockRegistry::removeLock(QxtFileLock * lock)
{
    QMutexLocker locker(&this->registryMutex);
    procLocks.removeAll(lock);
    return true;
}

bool QxtFileLock::unlock()
{
    if (file() && file()->isOpen() && isActive())
    {
        /*first remove real lock*/
        int lockmode,  locktype;
        int result = -1;
        struct  flock lockDesc;

        lockmode = F_SETLK;
        locktype = F_UNLCK;

        errno = 0;
        do
        {
            lockDesc.l_type = locktype;
            lockDesc.l_whence = SEEK_SET;
            lockDesc.l_start = qxt_d().offset;
            lockDesc.l_len = qxt_d().length;
            lockDesc.l_pid = 0;
            result = fcntl(this->file()->handle(), lockmode, &lockDesc);
        }
        while (result && errno == EINTR);

        QxtFileLockRegistry::instance().removeLock(this);
        qxt_d().isLocked = false;
        return true;
    }
    return false;
}

bool QxtFileLock::lock()
{
    if (file() && file()->isOpen() && !isActive())
    {
        /*this has to block if we can get no lock*/

        bool locked = false;

        while (1)
        {
            locked = QxtFileLockRegistry::instance().registerLock(this);
            if (locked)
                break;
            else
            {
                if (qxt_d().mode == ReadLockWait || qxt_d().mode == WriteLockWait)
                    usleep(1000 * 5);
                else
                    return false;
            }
        }

        /*now get real lock*/
        int lockmode,
        locktype;

        int result = -1;

        struct  flock lockDesc;

        switch (qxt_d().mode)
        {
        case    ReadLock:
            lockmode = F_SETLK;
            locktype = F_RDLCK;
            break;

        case    ReadLockWait:
            lockmode = F_SETLKW;
            locktype = F_RDLCK;
            break;

        case    WriteLock:
            lockmode = F_SETLK;
            locktype = F_WRLCK;
            break;

        case    WriteLockWait:
            lockmode = F_SETLKW;
            locktype = F_WRLCK;
            break;

        default:
            QxtFileLockRegistry::instance().removeLock(this);
            return (false);
            break;
        }

        errno = 0;
        do
        {
            lockDesc.l_type = locktype;
            lockDesc.l_whence = SEEK_SET;
            lockDesc.l_start = qxt_d().offset;
            lockDesc.l_len = qxt_d().length;
            lockDesc.l_pid = 0;
            result = fcntl(this->file()->handle(), lockmode, &lockDesc);
        }
        while (result && errno == EINTR);

        /*we dot get the lock unregister from lockregistry and return*/
        if (result == -1)
        {
            QxtFileLockRegistry::instance().removeLock(this);
            return false;
        }

        qxt_d().isLocked = true;
        return true;
    }
    return false;
}
