#include "newlogsmodel.h"

#include "engine.h"
#include "logmanager.h"

#include "logging.h"
//NYMEA_LOGGING_CATEGORY(dcLogEngine, "LogEngine")
Q_DECLARE_LOGGING_CATEGORY(dcLogEngine)

#include <QJsonDocument>
#include <QMetaEnum>

NewLogsModel::NewLogsModel(QObject *parent)
    : QAbstractListModel{parent}
{
    // Workaround for older Qt versions (5.12 and older) which can't deal with the QList<EnergyLogEntry*> argument
    connect(this, &NewLogsModel::entriesAdded, this, [this](int index, const QList<NewLogEntry*> &entries){
        emit entriesAddedIdx(index, entries.count());
    });

}

int NewLogsModel::rowCount(const QModelIndex &parent) const
{
    Q_UNUSED(parent)
    return m_list.count();
}

QVariant NewLogsModel::data(const QModelIndex &index, int role) const
{
    switch (role) {
    case RoleSource:
        return m_list.at(index.row())->source();
    case RoleTimestamp:
        return m_list.at(index.row())->timestamp();
    case RoleValues:
        return m_list.at(index.row())->values();
    }

    return QVariant();
}

QHash<int, QByteArray> NewLogsModel::roleNames() const
{
    return {
        {RoleSource, "source"},
        {RoleTimestamp, "timestamp"},
        {RoleValues, "values"}
    };
}

void NewLogsModel::classBegin()
{

}

void NewLogsModel::componentComplete()
{
    m_completed = true;
//    fetchMore();
}

bool NewLogsModel::canFetchMore(const QModelIndex &parent) const
{
    Q_UNUSED(parent)
    // Cannot fetchMore when there are multiple sources as paging doesn't really work in that case
    return m_canFetchMore && (m_sources.count() == 1 || m_list.isEmpty());
}

void NewLogsModel::fetchMore(const QModelIndex &parent)
{
    Q_UNUSED(parent)

    if (!m_engine) {
        return;
    }
    if (!m_completed) {
        return;
    }

    fetchLogs();

}

Engine *NewLogsModel::engine() const
{
    return m_engine;
}

void NewLogsModel::setEngine(Engine *engine)
{
    if (m_engine == engine) {
        return;
    }

    if (m_engine) {
        disconnect(m_engine->logManager(), &LogManager::logEntryReceived, this, &NewLogsModel::newLogEntryReceived);
    }

    m_engine = engine;
    emit engineChanged();

    if (m_engine) {
        connect(m_engine->logManager(), &LogManager::logEntryReceived, this, &NewLogsModel::newLogEntryReceived);
    }
}

QString NewLogsModel::source() const
{
    return m_sources.count() > 0 ? m_sources.first() : "";
}

void NewLogsModel::setSource(const QString &source)
{
    if (m_sources != QStringList(source)) {
        m_sources = QStringList(source);
        emit sourcesChanged();
    }
}

QStringList NewLogsModel::sources() const
{
    return m_sources;
}

void NewLogsModel::setSources(const QStringList &sources)
{
    if (m_sources != sources) {
        m_sources = sources;
        emit sourcesChanged();
    }
}

QStringList NewLogsModel::columns() const
{
    return m_columns;
}

void NewLogsModel::setColumns(const QStringList &columns)
{
    if (m_columns != columns) {
        m_columns = columns;
        emit columnsChanged();
    }
}

QVariantMap NewLogsModel::filter() const
{
    return m_filter;
}

void NewLogsModel::setFilter(const QVariantMap &filter)
{
    if (m_filter != filter) {
        m_filter = filter;
        emit filterChanged();
    }
}

QDateTime NewLogsModel::startTime() const
{
    return m_startTime;
}

void NewLogsModel::setStartTime(const QDateTime &startTime)
{
    if (m_startTime != startTime) {
        m_startTime = startTime;
        emit startTimeChanged();
    }
}

QDateTime NewLogsModel::endTime() const
{
    return m_endTime;
}

void NewLogsModel::setEndTime(const QDateTime &endTime)
{
    if (m_endTime != endTime) {
        m_endTime = endTime;
        emit endTimeChanged();
    }
}

NewLogsModel::SampleRate NewLogsModel::sampleRate() const
{
    return m_sampleRate;
}

void NewLogsModel::setSampleRate(SampleRate sampleRate)
{
    if (m_sampleRate != sampleRate) {
        m_sampleRate = sampleRate;
        emit sampleRateChanged();
        clear();
    }
}

Qt::SortOrder NewLogsModel::sortOrder() const
{
    return m_sortOrder;
}

void NewLogsModel::setSortOrder(Qt::SortOrder sortOrder)
{
    if (m_sortOrder != sortOrder) {
        m_sortOrder = sortOrder;
        emit sortOrderChanged();
    }
}

bool NewLogsModel::busy() const
{
    return m_busy;
}

bool NewLogsModel::live() const
{
    return m_live;
}

void NewLogsModel::setLive(bool live)
{
    if (m_live != live) {
        m_live = live;
        emit liveChanged();
    }
}

int NewLogsModel::fetchBlockSize() const
{
    return m_blockSize;
}

void NewLogsModel::setFetchBlockSize(int fetchBlockSize)
{
    if (m_blockSize != fetchBlockSize) {
        m_blockSize = fetchBlockSize;
        emit fetchBlockSizeChanged();
    }
}

NewLogEntry *NewLogsModel::get(int index) const
{
    if (index < 0 || index >= m_list.count()) {
        return nullptr;
    }
    return m_list.at(index);
}

NewLogEntry *NewLogsModel::find(const QDateTime &timestamp) const
{
//    qCDebug(dcLogEngine()) << "finding:" << timestamp.toString();
    if (m_list.isEmpty()) {
        return nullptr;
    }
    int idx = m_list.count() / 2;
    int jump = m_list.count() / 4;
    int stopper = 10;
    while (stopper-- > 0) {
//        qCDebug(dcLogEngine()) << "idx:" << idx << "cnt:" << m_list.count() << "jmp" << jump;
        NewLogEntry *entry = m_list.at(idx);
        if (entry->timestamp() == timestamp) {
//            qCDebug(dcLogEngine()) << "found exact";
            return entry;
        }
        qint64 diff = timestamp.msecsTo(entry->timestamp());
        if (entry->timestamp() > timestamp) {
//            qCDebug(dcLogEngine()) << "entry is newer than searched:" << entry->timestamp().toString() << timestamp.toString();
            if (idx == 0) {
//                qCDebug(dcLogEngine()) << "Is oldest.";
                return entry;
            }
            NewLogEntry *previousEntry = m_list.at(idx-1);
            if (previousEntry->timestamp() < timestamp) {
                qint64 previousDiff = timestamp.msecsTo(previousEntry->timestamp());
//                qCDebug(dcLogEngine()) << "time between this and previous:" << entry->timestamp().toString() << previousEntry->timestamp().toString() << (qAbs(previousDiff) < qAbs(diff) ? "next" : "this");
                return qAbs(previousDiff) < qAbs(diff) ? previousEntry : entry;
            }
            idx -= jump;
        } else if (entry->timestamp() < timestamp) {
//            qCDebug(dcLogEngine()) << "entry is older than searched:" << entry->timestamp().toString() << timestamp.toString();
            if (idx == m_list.count() - 1) {
//                qCDebug(dcLogEngine()) << "Is newest.";
                return entry;
            }
            NewLogEntry *nextEntry = m_list.at(idx+1);
            if (nextEntry->timestamp() > timestamp) {
                qint64 nextDiff = timestamp.msecsTo(nextEntry->timestamp());
//                qCDebug(dcLogEngine()) << "time between next and this:" << nextEntry->timestamp().toString() << "-" << entry->timestamp().toString()  << (qAbs(nextDiff) > qAbs(diff) ? "prev" : "this");
                return qAbs(nextDiff) < qAbs(diff) ? nextEntry : entry;
            }
            idx += jump;
        }
        jump = qMax(1, jump / 2);
    };
    return nullptr;
}

void NewLogsModel::clear()
{
    int count = m_list.count();
    beginResetModel();
    qDeleteAll(m_list);
    m_list.clear();
    m_currentNewest = QDateTime();
    m_lastOffset = 0;
    endResetModel();
    emit countChanged();
    emit entriesRemoved(0, count);
}

void NewLogsModel::fetchLogs()
{
    if (!m_engine) {
        return;
    }
    QVariantMap params {
        {"sources", m_sources},
        {"columns", m_columns},
        {"filter", m_filter}
    };


    if (m_sampleRate == SampleRateAny) { // Discrete logs

        if (!m_startTime.isNull() && !m_endTime.isNull()) { // Either specific time frame
            params.insert("startTime", m_startTime.toMSecsSinceEpoch());
            params.insert("endTime", m_endTime.toMSecsSinceEpoch());

        } else {
            params.insert("limit", m_blockSize);
            if (m_list.count() > 0) {
                if (m_currentNewest.isNull()) {
                    m_currentNewest = QDateTime::currentDateTime();
                }
                params.insert("offset", m_lastOffset);
                params.insert("endTime", m_currentNewest.toMSecsSinceEpoch());
            }
            m_lastOffset += m_blockSize;
        }

    } else {
        if (!m_startTime.isNull() && !m_endTime.isNull()) {
            params.insert("startTime", m_startTime.toMSecsSinceEpoch());
            params.insert("endTime", m_endTime.toMSecsSinceEpoch());

            QMetaEnum sampleRateEnum = QMetaEnum::fromType<SampleRate>();
            params.insert("sampleRate", sampleRateEnum.valueToKey(m_sampleRate));
        } else {
            qCWarning(dcLogEngine()) << "startTime and endTime is required when asking for resampling";
            return;
        }
    }

    QMetaEnum sortOrderEnum = QMetaEnum::fromType<Qt::SortOrder>();
    params.insert("sortOrder", sortOrderEnum.valueToKey(m_sortOrder));

    qCDebug(dcLogEngine()) << "Fetching logs:" << QJsonDocument::fromVariant(params).toJson();
    m_engine->jsonRpcClient()->sendCommand("Logging.GetLogEntries", params, this, "logsReply");

    m_busy = true;
    emit busyChanged();
}

void NewLogsModel::logsReply(int commandId, const QVariantMap &data)
{
    Q_UNUSED(commandId)

    m_busy = false;
    emit busyChanged();

    QList<NewLogEntry*> entries;
    foreach (const QVariant &entryVariant, data.value("logEntries").toList()) {
        QVariantMap map = entryVariant.toMap();
        QString source = map.value("source").toString();
        QDateTime timestamp = QDateTime::fromMSecsSinceEpoch(map.value("timestamp").toULongLong());
        QVariantMap values = map.value("values").toMap();
        NewLogEntry *entry = new NewLogEntry(source, timestamp, values, this);
        entries.append(entry);

    }

    m_canFetchMore = entries.count() >= m_blockSize;
    qCDebug(dcLogEngine()) << "Logs received:" << entries.count();

    if (!m_startTime.isNull() && !m_endTime.isNull()) {
        beginResetModel();
        QList<NewLogEntry*> oldEntries = m_list;
        m_list.clear();
        endResetModel();
        emit entriesRemoved(0, oldEntries.count());
        qDeleteAll(oldEntries);

        if (!entries.isEmpty()) {
            beginInsertRows(QModelIndex(), 0, entries.count() - 1);
            m_list = entries;
            endInsertRows();
            emit entriesAdded(0, entries);
        }
        emit countChanged();

    } else {
        if (!entries.isEmpty()) {
            beginInsertRows(QModelIndex(), m_list.count(), m_list.count() + entries.count() - 1);
            qSort(entries.begin(), entries.end(), [](NewLogEntry *left, NewLogEntry *right){
                return left->timestamp() > right->timestamp();
            });
            m_list.append(entries);
            endInsertRows();
            emit entriesAdded(m_list.count() - entries.count(), entries);
            emit countChanged();
        }
    }

}

void NewLogsModel::newLogEntryReceived(const QVariantMap &map)
{
    QString source = map.value("source").toString();
    QDateTime timestamp = QDateTime::fromMSecsSinceEpoch(map.value("timestamp").toULongLong());
    QVariantMap values = map.value("values").toMap();

    if (m_sources.contains(source) && m_sampleRate == SampleRateAny) {
        NewLogEntry *entry = new NewLogEntry(source, timestamp, values, this);
        if (m_sortOrder == Qt::AscendingOrder) {
            beginInsertRows(QModelIndex(), m_list.count(), m_list.count());
            m_list.append(entry);
            endInsertRows();
            emit entriesAdded(m_list.count() - 1, {entry});
        } else {
            beginInsertRows(QModelIndex(), 0, 0);
            m_list.prepend(entry);
            endInsertRows();
            emit entriesAdded(0, {entry});
        }
        emit countChanged();
    }
}
