/*
 * edbflatfile.cpp - asynchronous I/O event database
 * Copyright (C) 2001-2002  Justin Karneges
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 */

#include "edbflatfile.h"

#include "applicationinfo.h"
#include "common.h"
#include "iris/xmpp_jid.h"
#include "jidutil.h"
#include "psiaccount.h"
#include "psicon.h"
#include "psicontactlist.h"

#include <QDateTime>
#include <QDir>
#include <QFileInfo>
#include <QTextStream>
#include <QTimer>
#include <QVector>

#define FAKEDELAY 0

static const int MAX_FILES = 50;

using namespace XMPP;

//----------------------------------------------------------------------------
// EDBFlatFile
//----------------------------------------------------------------------------
struct item_file_req {
    Jid           j;
    int           type; // 0 = latest, 1 = oldest, 2 = random, 3 = write
    int           start;
    int           len;
    int           dir;
    int           id;
    QDateTime     date;
    QString       findStr;
    PsiEvent::Ptr event;

    enum Type { Type_get, Type_append, Type_find, Type_erase };
};

class EDBFlatFile::Private {
public:
    Private() { }

    QList<File *>          flist;
    QList<item_file_req *> rlist;
};

EDBFlatFile::EDBFlatFile(PsiCon *psi) : EDB(psi) { d = new Private; }

EDBFlatFile::~EDBFlatFile()
{
    qDeleteAll(d->rlist);
    qDeleteAll(d->flist);
    d->flist.clear();

    delete d;
}

int EDBFlatFile::features() const { return 0; }

int EDBFlatFile::get(const QString & /*accId*/, const Jid &j, const QDateTime date, int direction, int start, int len)
{
    item_file_req *r = new item_file_req;
    r->j             = j;
    r->type          = item_file_req::Type_get;
    r->start         = start;
    r->len           = len < 1 ? 1 : len;
    r->dir           = direction;
    r->date          = date;
    r->id            = genUniqueId();
    d->rlist.append(r);

    QTimer::singleShot(FAKEDELAY, this, SLOT(performRequests()));
    return r->id;
}

int EDBFlatFile::find(const QString & /*accId*/, const QString &str, const Jid &j, const QDateTime date, int direction)
{
    item_file_req *r = new item_file_req;
    r->j             = j;
    r->type          = item_file_req::Type_find;
    r->len           = 1;
    r->dir           = direction;
    r->findStr       = str;
    r->date          = date;
    r->id            = genUniqueId();
    d->rlist.append(r);

    QTimer::singleShot(FAKEDELAY, this, SLOT(performRequests()));
    return r->id;
}

int EDBFlatFile::append(const QString & /*accId*/, const Jid &j, const PsiEvent::Ptr &e, int type)
{
    if (type != EDB::Contact)
        return 0;
    item_file_req *r = new item_file_req;
    r->j             = j;
    r->type          = item_file_req::Type_append;
    r->event         = e;
    if (!r->event) {
        qWarning("EDBFlatFile::append(): Attempted to append incompatible type.");
        delete r;
        return 0;
    }
    r->id = genUniqueId();
    d->rlist.append(r);

    QTimer::singleShot(FAKEDELAY, this, SLOT(performRequests()));
    return r->id;
}

int EDBFlatFile::erase(const QString & /*accId*/, const Jid &j)
{
    item_file_req *r = new item_file_req;
    r->j             = j;
    r->type          = item_file_req::Type_erase;
    r->id            = genUniqueId();
    d->rlist.append(r);

    QTimer::singleShot(FAKEDELAY, this, SLOT(performRequests()));
    return r->id;
}

QList<EDB::ContactItem> EDBFlatFile::contacts(const QString &accId, int type)
{
    if (!accId.isEmpty())
        return File::contacts(accId, type);

    PsiAccount *acc = psi()->contactList()->defaultAccount();
    if (acc)
        return File::contacts(acc->id(), type);

    return QList<EDB::ContactItem>();
}

quint64 EDBFlatFile::eventsCount(const QString &accId, const XMPP::Jid &jid)
{
    quint64 res = 0;
    if (!jid.isEmpty())
        res = quint64(ensureFile(jid)->total());
    else {
        const auto &cts = contacts(accId, Contact);
        for (const ContactItem &ci : cts)
            res += quint64(ensureFile(ci.jid)->total());
    }
    return res;
}

EDBFlatFile::File *EDBFlatFile::findFile(const Jid &j) const
{
    for (File *i : std::as_const(d->flist)) {
        if (i->j.compare(j, false))
            return i;
    }
    return nullptr;
}

EDBFlatFile::File *EDBFlatFile::ensureFile(const Jid &j)
{
    File *i = findFile(j);
    if (!i) {
        i = new File(Jid(j.bare()));
        connect(i, &File::timeout, this, [i, this]() {
            d->flist.removeAll(i);
            i->deleteLater();
        });
        d->flist.append(i);
        if (d->flist.size() > MAX_FILES) {
            delete d->flist.takeFirst();
        }
    }
    return i;
}

bool EDBFlatFile::deleteFile(const Jid &j)
{
    File *i = findFile(j);

    QString fname;

    if (i) {
        fname = i->fname;
        d->flist.removeAll(i);
        delete i;
    } else {
        fname = File::jidToFileName(j);
    }

    QFileInfo fi(fname);
    if (fi.exists()) {
        QDir dir = fi.dir();
        return dir.remove(fi.fileName());
    } else
        return true;
}

void EDBFlatFile::performRequests()
{
    if (d->rlist.isEmpty())
        return;

    item_file_req *r = d->rlist.takeFirst();

    File *f    = ensureFile(r->j);
    int   type = r->type;
    if (type == item_file_req::Type_get) {
        EDBResult result;
        int       startId   = 0;
        int       direction = r->dir;
        int       id        = f->getId(r->date, direction, r->start);
        if (id != -1) {
            int len;
            if (direction == Forward) {
                if (id + r->len > f->total())
                    len = f->total() - id;
                else
                    len = r->len;
            } else {
                if ((id + 1) - r->len < 0)
                    len = id + 1;
                else
                    len = r->len;
            }

            startId = id;
            for (int n = 0; n < len; ++n) {
                PsiEvent::Ptr e(f->get(id));
                if (e) {
                    EDBItemPtr ei = EDBItemPtr::create(e, QString::number(id));
                    result.append(ei);
                }

                if (direction == Forward)
                    ++id;
                else
                    --id;
            }
            if (direction == Backward)
                startId = id + 1;
        }
        resultReady(r->id, result, startId);
    } else if (type == item_file_req::Type_append) {
        writeFinished(r->id, f->append(r->event));
    } else if (type == item_file_req::Type_find) {
        int       id = f->getId(r->date, r->dir, 0);
        EDBResult result;
        int       total = f->total();
        while (id >= 0 && id < total) {
            PsiEvent::Ptr e(f->get(id));
            if (e) {
                if (e->type() == PsiEvent::Message) {
                    MessageEvent::Ptr me = e.staticCast<MessageEvent>();
                    const Message    &m  = me->message();
                    if (m.body().indexOf(r->findStr, 0, Qt::CaseInsensitive) != -1) {
                        EDBItemPtr ei = EDBItemPtr(new EDBItem(e, QString::number(id)));
                        result.append(ei);
                        // commented line below to return ALL(instead of just first) messages that contain findStr
                        // break;
                    }
                }
            }
            if (r->dir == Forward)
                ++id;
            else
                --id;
        }
        resultReady(r->id, result, 0);
    } else if (type == item_file_req::Type_erase) {
        writeFinished(r->id, deleteFile(f->j));
    } else {
        qWarning("EDBFlatFile::performRequests(): Invalid type.");
    }

    delete r;
}

//----------------------------------------------------------------------------
// EDBFlatFile::File
//----------------------------------------------------------------------------
class EDBFlatFile::File::Private {
public:
    Private() = default;

    QVector<quint64> index;
    bool             indexed = false;
};

EDBFlatFile::File::File(const Jid &_j)
{
    d          = new Private;
    d->indexed = false;

    j     = _j;
    valid = false;
    t     = new QTimer(this);
    connect(t, SIGNAL(timeout()), SLOT(timer_timeout()));

    // printf("[EDB opening -- %s]\n", j.full().latin1());
    fname = jidToFileName(_j);
    f.setFileName(fname);
    valid = f.open(QIODevice::ReadWrite);

    touch();
}

EDBFlatFile::File::~File()
{
    if (valid)
        f.close();
    // printf("[EDB closing -- %s]\n", j.full().latin1());

    delete d;
}

QString EDBFlatFile::File::jidToFileName(const XMPP::Jid &j)
{
    return ApplicationInfo::historyDir() + "/" + strToFileName(JIDUtil::encode(j.bare()).toLower());
}

QString EDBFlatFile::File::strToFileName(const QString &s)
{
    QFileInfo fi(s);
    return fi.fileName() + ".history";
}

QList<EDB::ContactItem> EDBFlatFile::File::contacts(const QString &accId, int type)
{
    QList<ContactItem> res;
    if (type == EDB::Contact) {
        QDir          dir(ApplicationInfo::historyDir() + "/");
        QFileInfoList flist = dir.entryInfoList(QStringList(strToFileName("*")), QDir::Files);
        for (const QFileInfo &fi : flist) {
            XMPP::Jid jid(JIDUtil::decode(fi.completeBaseName()));
            if (jid.isValid())
                res.append(ContactItem(accId, jid));
        }
    }
    return res;
}

void EDBFlatFile::File::ensureIndex()
{
    if (valid && !d->indexed) {
        if (f.isSequential()) {
            qWarning("EDBFlatFile::File::ensureIndex(): Can't index sequential files.");
            return;
        }

        f.reset(); // go to beginning
        d->index.clear();

        // printf(" file: %s\n", fname.latin1());
        // build index
        while (1) {
            quint64 at = quint64(f.pos());

            // locate a newline
            bool found = false;
            char c;
            while (f.getChar(&c)) {
                if (c == '\n') {
                    found = true;
                    break;
                }
            }

            if (!found)
                break;

            int oldsize = d->index.size();
            d->index.resize(oldsize + 1);
            d->index[oldsize] = at;
        }

        d->indexed = true;
    } else {
        // printf(" file: can't open\n");
    }

    // printf(" messages: %d\n\n", d->index.size());
}

int EDBFlatFile::File::total() const
{
    const_cast<EDBFlatFile::File *>(this)->ensureIndex();
    return d->index.size();
}

int EDBFlatFile::File::getId(QDateTime &date, int dir, int offset)
{
    if (date.isNull()) {
        if (dir == EDBFlatFile::Forward)
            return offset;
        if (offset >= total())
            return 0;
        return total() - offset - 1;
    }
    ensureIndex();
    if (total() == 0)
        return 0;
    int id = findNearestDate(date);
    if (id == -1)
        return -1;

    QDateTime fDate = getDate(id);
    if (!fDate.isValid())
        return -1;

    if (dir == EDBFlatFile::Forward) {
        if (fDate < date)
            ++id;
        id += offset;
    } else {
        if (fDate > date)
            --id;
        id -= offset;
    }
    if (id >= total())
        id = total() - 1;
    else if (id < 0)
        id = 0;
    return id;
}

/*
 * This method returns an index of a string with the event
 * which has the nearest date to the specified one.
 * Returned date may be earlier than that is passed as an argument.
 */
int EDBFlatFile::File::findNearestDate(const QDateTime &date)
{
    int cnt = total();
    if (cnt == 0)
        return 0;

    // Binary search algorithm
    int left  = 0;
    int right = cnt;
    while (right - left > 0) {
        int             idx = left + (right - left) / 2;
        const QDateTime mid = getDate(idx);
        if (!mid.isValid())
            return -1;
        if (date <= mid)
            right = idx;
        else
            left = idx + 1;
    }
    // --
    if (right == cnt) // Specified date is later than the latest one in the history
        return cnt - 1;

    // Now `right` is pointing to the index with an identical or later date
    while (right > 0) { // in case of there are more than one identical date
        const QDateTime dt = getDate(right - 1);
        if (!dt.isValid())
            return -1;
        if (dt != date)
            break;
        --right;
    }
    if (right == 0)
        return 0;

    const QDateTime dt1 = getDate(right - 1);
    const QDateTime dt2 = getDate(right);
    if (!dt1.isValid() || !dt2.isValid())
        return -1;
    if (dt1.secsTo(date) <= date.secsTo(dt2)) // compares with earlier one
        --right;
    return right;
}

void EDBFlatFile::File::touch() { t->start(30000); }

void EDBFlatFile::File::timer_timeout() { emit timeout(); }

PsiEvent::Ptr EDBFlatFile::File::get(int id)
{
    QString line = getLine(id);
    if (line.isNull())
        return PsiEvent::Ptr();
    PsiEvent::Ptr res = lineToEvent(line);
    if (!res)
        qWarning("EDBFlatFile::File::get() Failed to parse file %s, line %d", fname.toLatin1().data(), id + 1);
    return res;
}

bool EDBFlatFile::File::append(const PsiEvent::Ptr &e)
{
    touch();

    if (!valid)
        return false;

    QString line = eventToLine(e);
    if (line.isEmpty())
        return false;

    f.seek(f.size());
    quint64 at = quint64(f.pos());

    QTextStream t;
    t.setDevice(&f);
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
    t.setEncoding(QStringConverter::Utf8);
#else
    t.setCodec("UTF-8");
#endif
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
    t << line << Qt::endl;
#else
    t << line << endl;
#endif
    f.flush();

    if (d->indexed) {
        int oldsize = d->index.size();
        d->index.resize(oldsize + 1);
        d->index[oldsize] = at;
    }

    return true;
}

PsiEvent::Ptr EDBFlatFile::File::lineToEvent(const QString &line)
{
    // -- parse the line --
    enum { Time = 0, Type = 1, Origin = 2, Flags = 3, Subj = 4, UrlAddr = 5, UrlDesc = 6 };
    QStringList strData;
    int         x1 = line.indexOf('|');
    if (x1 != -1) {
        ++x1;
        for (int i = 0; i <= UrlDesc; ++i) // Filing default data
            strData << QString();
        int max = Flags;
        for (int idx = 0; idx <= max;) {
            int x2 = line.indexOf('|', x1);
            if (x2 == -1) {
                x1 = -1;
                break;
            }
            QString s    = line.mid(x1, x2 - x1);
            strData[idx] = s;
            x1           = x2 + 1;

            if (idx == Flags) { // check for extra fields
                if (s.length() < 2) {
                    x1 = -1;
                    break;
                }
                if (s.at(1) != '-') {
                    int subflag = QString(s.at(1)).toInt(nullptr, 16);
                    if (subflag & 1) // have subject?
                        max = Subj;
                    else // Skip subject
                        ++idx;
                    if (subflag & 2) // have url?
                        max = UrlDesc;
                }
            }
            ++idx;
        }
    }

    if (x1 == -1)
        return PsiEvent::Ptr();

    // body text is last
    QString sText = line.mid(x1);

    // -- read end --

    int type = strData.at(Type).toInt();
    if (type == 0 || type == 1 || type == 4 || type == 5) {
        Message m;
        m.setTimeStamp(QDateTime::fromString(strData.at(Time), Qt::ISODate));
        if (type == 1)
            m.setType(Message::Type::Chat);
        else if (type == 4)
            m.setType(Message::Type::Error);
        else if (type == 5)
            m.setType(Message::Type::Headline);
        else
            m.setType(Message::Type::Normal);

        bool originLocal = strData.at(Origin) == "to";
        m.setFrom(j);
        if (strData.at(Flags).at(0) == 'N')
            m.setBody(logdecode(sText));
        else
            m.setBody(logdecode(QString::fromUtf8(sText.toLatin1())));
        m.setSubject(logdecode(strData.at(Subj)));

        QString url = logdecode(strData.at(UrlAddr));
        if (!url.isEmpty())
            m.urlAdd(Url(url, logdecode(strData.at(UrlDesc))));
        m.setSpooled(true);

        MessageEvent::Ptr me(new MessageEvent(m, nullptr));
        me->setOriginLocal(originLocal);

        return me.staticCast<PsiEvent>();
    } else if (type == 2 || type == 3 || type == 6 || type == 7 || type == 8) {
        QString subType = "subscribe";
        if (type == 2) {
            // stupid "system message" from Psi <= 0.8.6
            // try to figure out what kind it REALLY is based on the text
            if (sText == tr("<big>[System Message]</big><br>You are now authorized."))
                subType = "subscribed";
            else if (sText == tr("<big>[System Message]</big><br>Your authorization has been removed!"))
                subType = "unsubscribed";
        } else if (type == 3)
            subType = "subscribe";
        else if (type == 6)
            subType = "subscribed";
        else if (type == 7)
            subType = "unsubscribe";
        else if (type == 8)
            subType = "unsubscribed";

        AuthEvent::Ptr ae(new AuthEvent(j, subType, nullptr));
        ae->setTimeStamp(QDateTime::fromString(strData.at(Time), Qt::ISODate));
        return ae.staticCast<PsiEvent>();
    }

    return PsiEvent::Ptr();
}

QString EDBFlatFile::File::eventToLine(const PsiEvent::Ptr &e)
{
    int     subflags = 0;
    QString sTime, sType, sOrigin, sFlags;

    if (e->type() == PsiEvent::Message) {
        MessageEvent::Ptr me   = e.staticCast<MessageEvent>();
        const Message    &m    = me->message().displayMessage();
        const UrlList     urls = m.urlList();

        if (!m.subject().isEmpty())
            subflags |= 1;
        if (!urls.isEmpty())
            subflags |= 2;

        sTime = m.timeStamp().toString(Qt::ISODate);
        int n = 0;
        if (m.type() == Message::Type::Chat)
            n = 1;
        else if (m.type() == Message::Type::Error)
            n = 4;
        else if (m.type() == Message::Type::Headline)
            n = 5;
        sType.setNum(n);
        sOrigin = e->originLocal() ? "to" : "from";
        sFlags  = "N---";

        if (subflags != 0)
            sFlags[1] = QString::number(subflags, 16)[0];

        //  | date | type | To/from | flags | text
        QString line = "|" + sTime + "|" + sType + "|" + sOrigin + "|" + sFlags + "|";

        if (subflags & 1) {
            line += logencode(m.subject()) + "|";
        }
        if (subflags & 2) {
            const Url &url = urls.first();
            line += logencode(url.url()) + "|";
            line += logencode(url.desc()) + "|";
        }
        line += logencode(m.body());

        return line;
    } else if (e->type() == PsiEvent::Auth) {
        AuthEvent::Ptr ae = e.staticCast<AuthEvent>();
        sTime             = ae->timeStamp().toString(Qt::ISODate);
        QString subType   = ae->authType();
        int     n         = 0;
        if (subType == "subscribe")
            n = 3;
        else if (subType == "subscribed")
            n = 6;
        else if (subType == "unsubscribe")
            n = 7;
        else if (subType == "unsubscribed")
            n = 8;
        sType.setNum(n);
        sOrigin = e->originLocal() ? "to" : "from";
        sFlags  = "N---";

        //  | date | type | To/from | flags | text
        QString line = "|" + sTime + "|" + sType + "|" + sOrigin + "|" + sFlags + "|";
        line += logencode(subType);

        return line;
    }

    return "";
}

QString EDBFlatFile::File::getLine(int id)
{
    touch();

    if (!valid)
        return QString();

    ensureIndex();
    if (id < 0 || id >= int(d->index.size()))
        return QString();

    f.seek(qint64(d->index[id]));

    QTextStream t;
    t.setDevice(&f);
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
    t.setEncoding(QStringConverter::Utf8);
#else
    t.setCodec("UTF-8");
#endif
    return t.readLine();
}

QDateTime EDBFlatFile::File::getDate(int id)
{
    QString line = getLine(id);
    if (line.isNull())
        return QDateTime();

    int p1 = line.indexOf('|') + 1;
    if (p1 == -1)
        return QDateTime();
    int p2 = line.indexOf('|', p1);
    if (p2 == -1)
        return QDateTime();

    QString sTime = line.mid(p1, p2 - p1);
    return QDateTime::fromString(sTime, Qt::ISODate);
}
