/*
 * Copyright (C) 2003-2006 Ben van Klinken and the CLucene Team
 *
 * Distributable under the terms of either the Apache License (Version 2.0) or 
 * the GNU Lesser General Public License, as specified in the COPYING file.
 *
 * Changes are Copyright (C) 2015 The Qt Company Ltd.
*/
#include <QtCore/QDir>
#include <QtCore/QDateTime>
#include <QtCore/QFileInfo>
#include <QtCore/QByteArray>
#include <QtCore/QCryptographicHash>

#include "CLucene/StdHeader.h"
#include "FSDirectory.h"
#include "CLucene/index/IndexReader.h"
#include "CLucene/util/Misc.h"
#include "CLucene/debug/condition.h"

CL_NS_DEF(store)
CL_NS_USE(util)

bool FSDirectory::disableLocks = false;

// This cache of directories ensures that there is a unique Directory instance
// per path, so that synchronization on the Directory can be used to synchronize
// access between readers and writers.
static CL_NS(util)::CLHashMap<QString, FSDirectory*,
    CL_NS(util)::Compare::Qstring, CL_NS(util)::Equals::Qstring,
    CL_NS(util)::Deletor::DummyQString> DIRECTORIES(false, false);
 
// # pragma mark -- FSDirectory::FSLock

FSDirectory::FSLock::FSLock(const QString& _lockDir, const QString& name)
    : lockDir(_lockDir)
    , lockFile(_lockDir + QDir::separator() + name)
{
}

FSDirectory::FSLock::~FSLock()
{
}

bool FSDirectory::FSLock::obtain()
{
    if (disableLocks)
        return true;

    if (QFile::exists(lockFile))
        return false;

    QDir dir(lockDir);
    if (!dir.exists()) {
        if (!dir.mkpath(lockDir)) {
            // 34: len of "Couldn't create lock directory: "
            char* err = _CL_NEWARRAY(
                char, 34 + strlen(lockDir.toLocal8Bit().constData()) + 1);
            strcpy(err, "Couldn't create lock directory: ");
            strcat(err, lockDir.toLocal8Bit().constData());
            _CLTHROWA_DEL(CL_ERR_IO, err);
        }
    }

    QFile file(lockFile);
    return file.open(QIODevice::ReadWrite);
}

void FSDirectory::FSLock::release()
{
    if (disableLocks)
        return;
    
    QFile file(lockFile);
    file.remove();
}

bool FSDirectory::FSLock::isLocked()
{
    if (disableLocks)
        return false;
    return QFile::exists(lockFile);
}

QString FSDirectory::FSLock::toString() const
{
    QString ret(QLatin1String("Lock@"));
    return ret.append(lockFile);
}

// # pragma mark -- FSDirectory::FSIndexInput

FSDirectory::FSIndexInput::FSIndexInput(const QString& path, int32_t bufferSize)
    : BufferedIndexInput(bufferSize)
{
    CND_PRECONDITION(!path.isEmpty(), "path is NULL");

    handle = _CLNEW SharedHandle();
    handle->fhandle.setFileName(path);
    handle->fhandle.open(QIODevice::ReadOnly);
    
    if (handle->fhandle.error() != QFile::NoError) {
        switch(handle->fhandle.error()) {
            case 1:
                _CLTHROWA(CL_ERR_IO, "An error occurred when reading from the file");
                break;
            case 2:
                _CLTHROWA(CL_ERR_IO, "An error occurred when writing to the file.");
                break;
            case 5:
                _CLTHROWA(CL_ERR_IO, "The file could not be opened.");
                break;
            case 6:
                _CLTHROWA(CL_ERR_IO, "The operation was aborted.");
                break;
            case 7:
                _CLTHROWA(CL_ERR_IO, "A timeout occurred.");
                break;
            case 8:
                _CLTHROWA(CL_ERR_IO, "An unspecified error occurred.");
                break;
            case 9:
                _CLTHROWA(CL_ERR_IO, "The file could not be removed.");
                break;
            case 10:
                _CLTHROWA(CL_ERR_IO, "The file could not be renamed.");
                break;
            case 11:
                _CLTHROWA(CL_ERR_IO, "The position in the file could not be changed.");
                break;
            case 12:
                _CLTHROWA(CL_ERR_IO, "The file could not be resized.e");
                break;
            case 13:
                _CLTHROWA(CL_ERR_IO, "The file could not be accessed.");
                break;
            case 14:
                _CLTHROWA(CL_ERR_IO, "The file could not be copied.");
                break;
            case 4:
            default:
                _CLTHROWA(CL_ERR_IO, "A fatal error occurred.");
        }
    }

    //Store the file length
    handle->_length = handle->fhandle.size();
    handle->_fpos = 0;
    this->_pos = 0;
}

FSDirectory::FSIndexInput::FSIndexInput(const FSIndexInput& other)
    : BufferedIndexInput(other)
{
    if (other.handle == NULL)
        _CLTHROWA(CL_ERR_NullPointer, "other handle is null");

    SCOPED_LOCK_MUTEX(*other.handle->THIS_LOCK)

    _pos = other.handle->_fpos;
    handle = _CL_POINTER(other.handle);
}

FSDirectory::FSIndexInput::~FSIndexInput()
{
    FSIndexInput::close();
}

void FSDirectory::FSIndexInput::close()
{
    BufferedIndexInput::close();
#ifdef _LUCENE_THREADMUTEX
    if (handle != NULL) {
        // Here we have a bit of a problem... We need to lock the handle to
        // ensure that we can safely delete the handle... But if we delete the
        // handle, then the scoped unlock, won't be able to unlock the mutex...

        // take a reference of the lock object...
        _LUCENE_THREADMUTEX* mutex = handle->THIS_LOCK;
        //lock the mutex
        mutex->lock();

        // determine if we are about to delete the handle...
        bool doUnlock = (handle->__cl_refcount > 1);
        // decdelete (deletes if refcount is down to 0)
        _CLDECDELETE(handle);

        if (doUnlock)
            mutex->unlock();
        else
            delete mutex;
    }
#else
    _CLDECDELETE(handle);
#endif
}

IndexInput* FSDirectory::FSIndexInput::clone() const
{
    return _CLNEW FSDirectory::FSIndexInput(*this);
}

void FSDirectory::FSIndexInput::seekInternal(const int64_t position)
{
    CND_PRECONDITION(position >= 0 && position < handle->_length,
        "Seeking out of range")
    _pos = position;
}

void FSDirectory::FSIndexInput::readInternal(uint8_t* b, const int32_t len)
{
    SCOPED_LOCK_MUTEX(*handle->THIS_LOCK)

    CND_PRECONDITION(handle != NULL, "shared file handle has closed");
    CND_PRECONDITION(handle->fhandle.isOpen(), "file is not open");

    if (handle->_fpos != _pos) {
        handle->fhandle.seek(_pos);
        if (handle->fhandle.pos() != _pos)
            _CLTHROWA( CL_ERR_IO, "File IO Seek error");
        handle->_fpos = _pos;
    }

    bufferLength = (int32_t)handle->fhandle.read((char*)b, len);
    if (bufferLength == 0)
        _CLTHROWA(CL_ERR_IO, "read past EOF");

    if (bufferLength == -1)
        _CLTHROWA(CL_ERR_IO, "read error");

    _pos += bufferLength;
    handle->_fpos =_pos;
}

// # pragma mark -- FSDirectory::FSIndexInput::SharedHandle

FSDirectory::FSIndexInput::SharedHandle::SharedHandle()
    : _fpos(0)
    , _length(0)
{
#ifdef _LUCENE_THREADMUTEX
    THIS_LOCK = new _LUCENE_THREADMUTEX;
#endif
}

FSDirectory::FSIndexInput::SharedHandle::~SharedHandle()
{
    if (fhandle.isOpen())
        fhandle.close();
}

// # pragma mark -- FSDirectory::FSIndexOutput

FSDirectory::FSIndexOutput::FSIndexOutput(const QString& path)
{
    //O_BINARY - Opens file in binary (untranslated) mode
    //O_CREAT - Creates and opens new file for writing. Has no effect if file specified by filename exists
    //O_RANDOM - Specifies that caching is optimized for, but not restricted to, random access from disk.
    //O_WRONLY - Opens file for writing only;
    fhandle.setFileName(path);
    fhandle.open(QIODevice::ReadWrite | QIODevice::Truncate);
    
    if (fhandle.error() != QFile::NoError) {
        switch(fhandle.error()) {
            case 1:
                _CLTHROWA(CL_ERR_IO, "An error occurred when reading from the file");
                break;
            case 2:
                _CLTHROWA(CL_ERR_IO, "An error occurred when writing to the file.");
                break;
            case 5:
                _CLTHROWA(CL_ERR_IO, "The file could not be opened.");
                break;
            case 6:
                _CLTHROWA(CL_ERR_IO, "The operation was aborted.");
                break;
            case 7:
                _CLTHROWA(CL_ERR_IO, "A timeout occurred.");
                break;
            case 8:
                _CLTHROWA(CL_ERR_IO, "An unspecified error occurred.");
                break;
            case 9:
                _CLTHROWA(CL_ERR_IO, "The file could not be removed.");
                break;
            case 10:
                _CLTHROWA(CL_ERR_IO, "The file could not be renamed.");
                break;
            case 11:
                _CLTHROWA(CL_ERR_IO, "The position in the file could not be changed.");
                break;
            case 12:
                _CLTHROWA(CL_ERR_IO, "The file could not be resized.e");
                break;
            case 13:
                _CLTHROWA(CL_ERR_IO, "The file could not be accessed.");
                break;
            case 14:
                _CLTHROWA(CL_ERR_IO, "The file could not be copied.");
                break;
            case 4:
            default:
                _CLTHROWA(CL_ERR_IO, "A fatal error occurred.");
        }
    }
}

FSDirectory::FSIndexOutput::~FSIndexOutput()
{
    if (fhandle.isOpen()) {
        try {
            FSIndexOutput::close();
        } catch (CLuceneError& err) {
            //ignore IO errors...
            if (err.number() != CL_ERR_IO)
                throw;
        }
    }
}

void FSDirectory::FSIndexOutput::close()
{
    try {
        BufferedIndexOutput::close();
    } catch (CLuceneError& err) {
        //ignore IO errors...
        if (err.number() != CL_ERR_IO)
            throw;
    }
    fhandle.close();
}

int64_t FSDirectory::FSIndexOutput::length()
{
    CND_PRECONDITION(fhandle.isOpen(), "file is not open");
    return fhandle.size();
}

void FSDirectory::FSIndexOutput::seek(const int64_t pos)
{
    CND_PRECONDITION(fhandle.isOpen(), "file is not open");
    
    BufferedIndexOutput::seek(pos);
    fhandle.seek(pos);
    if (fhandle.pos() != pos)
        _CLTHROWA(CL_ERR_IO, "File IO Seek error");
}

void FSDirectory::FSIndexOutput::flushBuffer(const uint8_t* b, const int32_t size)
{
    CND_PRECONDITION(fhandle.isOpen(), "file is not open");

    if (size > 0 && fhandle.write((const char*)b, size) != size)
        _CLTHROWA(CL_ERR_IO, "File IO Write error");
}

// # pragma mark -- FSDirectory

FSDirectory::FSDirectory(const QString& path, const bool createDir)
    : Directory()
    , refCount(0)
    , useMMap(false)
{
    //set a realpath so that if we change directory, we can still function
    directory = QFileInfo(path).absoluteFilePath();
    lockDir = directory;

    QDir dir(lockDir);
    if (!dir.exists()) {
        if (!dir.mkpath(lockDir))
            _CLTHROWA(CL_ERR_IO, "Cannot create temp directory");
    }

    QFileInfo info(lockDir);
    if (info.isFile() || info.isSymLink())
        _CLTHROWA(CL_ERR_IO, "Found regular file where directory expected");

    if (createDir)
        create();

    dir.setPath(directory);
    if (!dir.exists()) {
         //19: len of " is not a directory"
        char* err = 
            _CL_NEWARRAY(char, 19 + strlen(path.toLocal8Bit().constData()) + 1);
        strcpy(err, path.toLocal8Bit().constData());
        strcat(err, " is not a directory");
        _CLTHROWA_DEL(CL_ERR_IO, err);
    }
}

void FSDirectory::create()
{
    SCOPED_LOCK_MUTEX(THIS_LOCK)

    bool clear = false;
    QDir dir(directory);
    if (!dir.exists()) {
        if (!dir.mkpath(directory)) {
            char* err = _CL_NEWARRAY( // 27 len of "Couldn't create directory:"
                char, 27 + strlen(directory.toLocal8Bit().constData()) + 1);
            strcpy(err, "Couldn't create directory: ");
            strcat(err, directory.toLocal8Bit().constData());
            _CLTHROWA_DEL(CL_ERR_IO, err);
        }
    } else {
        clear = true;
    }

    QFileInfo info(directory);
    if (info.isFile() || info.isSymLink()) {
        char tmp[1024];
        _snprintf(tmp, 1024, "%s not a directory",
            directory.toLocal8Bit().constData());
        _CLTHROWA(CL_ERR_IO, tmp);
    }

    if (clear) {
        dir.setPath(directory);
        // clear probably existing lucene index files 
        QStringList fileList = dir.entryList(QDir::Files | QDir::Hidden
            | QDir::NoSymLinks);
        foreach(const QString file, fileList) {
            if (CL_NS(index)::IndexReader::isLuceneFile(file)) {
                if (!dir.remove(file))
                    _CLTHROWA(CL_ERR_IO, "Couldn't delete file ");
            }
        }

        // clear probably existing file locks
        QFileInfo dirInfo(lockDir);
        if (dirInfo.exists() && dirInfo.isReadable() && dirInfo.isWritable()
            && !dirInfo.isFile() && !dirInfo.isSymLink()) {
            QDir lockDirectory(lockDir);
            fileList = dir.entryList(QStringList() << getLockPrefix()
                + QLatin1Char('*'), QDir::Files | QDir::Hidden | QDir::NoSymLinks);

            foreach(const QString file, fileList) {
                if (!lockDirectory.remove(file))
                    _CLTHROWA(CL_ERR_IO, "Couldn't delete file ");
            }
        }
        else {
             //todo: richer error: + lockDir.getAbsolutePath());
            _CLTHROWA(CL_ERR_IO, "Cannot read lock directory");
        }
    }
}

void FSDirectory::priv_getFN(QString& buffer, const QString& name) const
{
    buffer.clear();
    buffer.append(directory);
    buffer.append(QDir::separator());
    buffer.append(name);
}

FSDirectory::~FSDirectory()
{
}

QStringList FSDirectory::list() const
{
    CND_PRECONDITION(!directory.isEmpty(), "directory is not open");

    QDir dir(directory);
    return dir.entryList(QDir::Files | QDir::Hidden);
}

bool FSDirectory::fileExists(const QString& name) const
{
    CND_PRECONDITION(!directory.isEmpty(), "directory is not open");

    QDir dir(directory);
    return dir.entryList().contains(name);
}

QString FSDirectory::getDirName() const
{
    return directory;
}

//static
FSDirectory* FSDirectory::getDirectory(const QString& file, const bool _create)
{
    FSDirectory* dir = NULL;
    {
        if (file.isEmpty())
            _CLTHROWA(CL_ERR_IO, "Invalid directory");

        SCOPED_LOCK_MUTEX(DIRECTORIES.THIS_LOCK)
            dir = DIRECTORIES.get(file);
        if ( dir == NULL  ){
            dir = _CLNEW FSDirectory(file, _create);
            DIRECTORIES.put(dir->directory, dir);
        } else if (_create) {
            dir->create();
        }

        {
            SCOPED_LOCK_MUTEX(dir->THIS_LOCK)
            dir->refCount++;
        }
    }    

    return _CL_POINTER(dir);
}

int64_t FSDirectory::fileModified(const QString& name) const
{
    CND_PRECONDITION(!directory.isEmpty(), "directory is not open");

    QFileInfo fInfo(directory + QDir::separator() + name);
    return fInfo.lastModified().toTime_t();
}

//static
int64_t FSDirectory::fileModified(const QString& dir, const QString& name)
{
    QFileInfo fInfo(dir + QDir::separator() + name);
    return fInfo.lastModified().toTime_t();
}

void FSDirectory::touchFile(const QString& name)
{
    CND_PRECONDITION(!directory.isEmpty(), "directory is not open");

    QFile file(directory + QDir::separator() + name);
    if (!file.open(QIODevice::ReadWrite))
        _CLTHROWA(CL_ERR_IO, "IO Error while touching file");
}

int64_t FSDirectory::fileLength(const QString& name) const
{
    CND_PRECONDITION(!directory.isEmpty(), "directory is not open");

    QFileInfo fInfo(directory + QDir::separator() + name);
    return fInfo.size();
}

IndexInput* FSDirectory::openInput(const QString& name)
{
    return openInput(name, CL_NS(store)::BufferedIndexOutput::BUFFER_SIZE);
}

IndexInput* FSDirectory::openInput(const QString& name, int32_t bufferSize )
{
    CND_PRECONDITION(directory[0]!=0,"directory is not open")

    return _CLNEW FSIndexInput(directory + QDir::separator() + name, bufferSize);
}

void FSDirectory::close()
{
    SCOPED_LOCK_MUTEX(DIRECTORIES.THIS_LOCK)
    {
        SCOPED_LOCK_MUTEX(THIS_LOCK)

        CND_PRECONDITION(!directory.isEmpty(), "directory is not open");

        //refcount starts at 1
        if (--refCount <= 0) {
            Directory* dir = DIRECTORIES.get(getDirName());
            if (dir) {
                //this will be removed in ~FSDirectory
                DIRECTORIES.remove(getDirName());
                _CLDECDELETE(dir);
            }
        }
    }
}

QString FSDirectory::getLockPrefix() const
{
    CND_PRECONDITION(!directory.isEmpty(), "directory is not open");

    QString dirName(QFileInfo(directory).absoluteFilePath());
    if (dirName.isEmpty())
        _CLTHROWA(CL_ERR_Runtime, "Invalid directory path");

    // to be compatible with jlucene,
    // we need to make some changes ...
    if (dirName.at(1) == QLatin1Char(':'))
        dirName[0] = dirName.at(0).toUpper();

    TCHAR tBuffer[2048] = { 0 };
    dirName.toWCharArray(tBuffer);
    
    char aBuffer[4096] = { 0 };
    STRCPY_TtoA(aBuffer, tBuffer, 4096);

    QString string(QLatin1String("lucene-"));
    QByteArray hash(QCryptographicHash::hash(aBuffer, QCryptographicHash::Md5));

    // TODO: verify this !!!
    return string.append(QLatin1String(hash.toHex().constData()));
}

bool FSDirectory::doDeleteFile(const QString& name)
{
    CND_PRECONDITION(!directory.isEmpty(), "directory is not open");

    QDir dir(directory);
    return dir.remove(name);
}

void FSDirectory::renameFile(const QString& from, const QString& to)
{
    CND_PRECONDITION(!directory.isEmpty(), "directory is not open");
    SCOPED_LOCK_MUTEX(THIS_LOCK)
    
    if (fileExists(to))
        deleteFile(to, false);

    QFile file(directory + QDir::separator() + from);
    QString newFile(directory + QDir::separator() + to);
    if (!file.rename(newFile)) {
        // try a second time if we fail
        if (fileExists(to))
            deleteFile(to, false);

        if (!file.rename(newFile)) {
            QString error(QLatin1String("Could not rename: %1 to %2!!!!"));
            error.arg(from).arg(newFile);
            QByteArray bArray(error.toLocal8Bit());
            _CLTHROWA(CL_ERR_IO, bArray.constData());
        }
    }
}

IndexOutput* FSDirectory::createOutput(const QString& name)
{
    CND_PRECONDITION(!directory.isEmpty(), "directory is not open");

    QString file = directory + QDir::separator() + name;
    if (QFileInfo(file).exists()) {
        if (!QFile::remove(file)) {
            QByteArray bArray("Cannot overwrite: ");
            bArray.append(name.toLocal8Bit());
            _CLTHROWA(CL_ERR_IO, bArray.constData());
        }
    }
    return _CLNEW FSIndexOutput(file);
}

LuceneLock* FSDirectory::makeLock(const QString& name)
{
    CND_PRECONDITION(!directory.isEmpty(), "directory is not open");


    QString lockFile(getLockPrefix());
    lockFile.append(QLatin1Char('-')).append(name);

    return _CLNEW FSLock(lockDir, lockFile);
}

QString FSDirectory::toString() const
{
    return QString::fromLatin1("FSDirectory@").append(directory);
}

CL_NS_END
