/* SPDX-License-Identifier: GPL-3.0-or-later
 *
 * Qt GUI application for reading ATOM / Prival Tox client message logs.
 *
 * Parses the custom binary format used by some Tox clients to store
 * message history. See analysis in conversation history for format details.
 *
 * Build (with qmake):
 *   qmake && make
 *
 * Build (with cmake):
 *   mkdir -p build && cd build && cmake .. && make qt_atom_reader
 */

#include <QApplication>
#include <QMainWindow>
#include <QTextEdit>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QPushButton>
#include <QFileDialog>
#include <QStatusBar>
#include <QMessageBox>
#include <QFile>
#include <QFileInfo>
#include <QTextStream>
#include <QLabel>
#include <QDateTime>
#include <QTableWidget>
#include <QHeaderView>
#include <QSplitter>
#include <QFont>
#include <cstdint>
#include <cstring>

/* === ATOM / Prival Tox Message Log Binary Format ===
 *
 * Each record (message entry), minimum 16 bytes + data:
 *
 *   Offset  Size  Description
 *   ------  ----  -----------
 *   0       4     Unix timestamp (uint32 LE, seconds since epoch)
 *   4       4     Reserved/padding (zero)
 *   8       2     Record type (0x0008 = MSG from local user, 0x000D = MSG from peer,
 *                  0x0009 = text, 0x000B = INFO, 0x0011 = REPLY)
 *   10      2     Message text length in bytes (uint16 LE, sender = payload - textLen)
 *   12      2     Direction (uint16 LE: 1 = outgoing/sent, 0 = incoming/rcvd)
 *   14      2     Unknown/flags (varies per record)
 *   16      N     Sender name (ASCII/UTF-8, no null terminator, N bytes)
 *   16+N   M=N2   Message text (UTF-8, exactly textLen bytes)
 *
 * Notes:
 *   - The sender name is the text between the 16-byte header and the message text.
 *   - Records are concatenated back-to-back with no separator.
 *   - The next record's start is found by scanning for a valid Unix-timestamp
 *     pattern followed by zero-padding and a known record type.
 */

/* === Helper functions === */

static uint32_t readLE32(const uint8_t *data)
{
    return (uint32_t)data[0]
         | ((uint32_t)data[1] << 8)
         | ((uint32_t)data[2] << 16)
         | ((uint32_t)data[3] << 24);
}

static uint16_t readLE16(const uint8_t *data)
{
    return (uint16_t)data[0] | ((uint16_t)data[1] << 8);
}

/* === Data structures === */

struct AtomMessage {
    QDateTime timestamp;
    uint16_t  recordType;
    uint16_t  direction;   // 1 = outgoing, 0 = incoming
    uint16_t  flags;
    QString   sender;
    QString   text;
};

struct AtomParseResult {
    QList<AtomMessage> messages;
    bool    ok = false;
    QString error;
    int     totalRecords = 0;
    int     fileSize = 0;
};

/* === Parser === */

/* Check if a value looks like a valid record type for this format */
static bool isValidRecordType(uint16_t type)
{
    /* Known types observed in logs:
     *   8  — MSG from local user (SlProger, Тест)
     *   9  — MSG text (ATOM user)
     *   11 — INFO
     *   13 — MSG from peer (Magnetar user)
     *   17 — REPLY */
    return type == 8 || type == 9 || type == 11 || type == 13 || type == 17;
}

/* Try to find the start of the next record by looking for the pattern:
 *   [4 bytes: Unix timestamp in valid range ~1.7B-1.9B] [4 bytes: zero padding]
 *   [2 bytes: valid record type]
 * Returns the offset of the next record, or fileSize if not found. */
static qint64 findNextRecord(const uint8_t *data, qint64 fileSize, qint64 startOffset)
{
    /* Start searching a reasonable minimum distance from the current record.
     * Minimum record size: 16 header + 1 byte sender + 0 byte message = 17.
     * Scan byte-by-byte for the pattern. */
    for (qint64 pos = startOffset; pos + 16 <= fileSize; pos++) {
        uint32_t ts = readLE32(data + pos);

        /* Valid Unix timestamp for years ~2023-2027 */
        if (ts >= 1680000000 && ts <= 1900000000) {
            /* Zero padding */
            if (data[pos + 4] == 0 && data[pos + 5] == 0 &&
                data[pos + 6] == 0 && data[pos + 7] == 0) {
                /* Check for a valid record type */
                uint16_t recType = readLE16(data + pos + 8);
                if (isValidRecordType(recType)) {
                    return pos;
                }
            }
        }
    }
    return fileSize; /* No next record found — treat EOF as boundary */
}

static AtomParseResult parseAtomLog(const QString &filePath)
{
    AtomParseResult result;

    QFile file(filePath);
    if (!file.open(QIODevice::ReadOnly)) {
        result.error = QStringLiteral("Could not open '%1'").arg(filePath);
        return result;
    }

    QByteArray raw = file.readAll();
    file.close();

    result.fileSize = raw.size();

    if (raw.size() < 16) {
        result.error = QStringLiteral("File too small to be a valid ATOM message log (min 16 bytes)");
        return result;
    }

    const uint8_t *data = reinterpret_cast<const uint8_t *>(raw.constData());
    qint64 fileSize = raw.size();
    qint64 offset = 0;

    while (offset + 16 <= fileSize) {
        AtomMessage msg;

        /* Parse 16-byte header */
        uint32_t ts = readLE32(data + offset);
        msg.timestamp = QDateTime::fromSecsSinceEpoch(ts, Qt::LocalTime);

        msg.recordType = readLE16(data + offset + 8);
        uint16_t msgTextLen = readLE16(data + offset + 10);
        msg.direction = readLE16(data + offset + 12);
        msg.flags     = readLE16(data + offset + 14);

        /* Sanity check: message text length should be reasonable */
        if (msgTextLen > 100000) {
            break;
        }

        /* Find next record boundary */
        /* Start search after the current message text to avoid false matches */
        qint64 searchStart = offset + 16 + qMax((int)msgTextLen, 1);
        qint64 nextRec = findNextRecord(data, fileSize, searchStart);

        /* The payload (sender name + message text) spans from offset+16 to nextRec */
        qint64 payloadLen = nextRec - (offset + 16);

        if (payloadLen < msgTextLen || payloadLen > 1000000) {
            /* Invalid — message text exceeds payload, or payload is unreasonably large */
            break;
        }

        /* Message text is the last msgTextLen bytes of the payload.
         * Everything before that is the sender name. */
        qint64 senderLen = payloadLen - msgTextLen;

        if (senderLen > 0) {
            msg.sender = QString::fromUtf8(
                reinterpret_cast<const char *>(data + offset + 16),
                static_cast<int>(senderLen));
        } else {
            msg.sender = QStringLiteral("<?>");
        }

        if (msgTextLen > 0) {
            msg.text = QString::fromUtf8(
                reinterpret_cast<const char *>(data + offset + 16 + senderLen),
                msgTextLen);
        }

        result.messages.append(msg);
        result.totalRecords++;

        /* If we hit the end of file, stop */
        if (nextRec >= fileSize) {
            break;
        }

        offset = nextRec;
    }

    result.ok = true;
    return result;
}

/* Format a single message as a display string */
static QString formatMessage(const AtomMessage &msg, int index)
{
    QString ts = msg.timestamp.toString(QStringLiteral("yyyy-MM-dd HH:mm:ss"));
    QString arrow;

    switch (msg.direction) {
        case 0:  arrow = QStringLiteral("←"); break;  /* incoming */
        case 1:  arrow = QStringLiteral("→"); break;  /* outgoing */
        default: arrow = QStringLiteral("↔"); break;   /* unknown */
    }

    QString typeStr;
    switch (msg.recordType) {
        case 8:  typeStr = QStringLiteral("MSG");  break;
        case 9:  typeStr = QStringLiteral("MSG");  break;
        case 11: typeStr = QStringLiteral("INFO"); break;
        case 13: typeStr = QStringLiteral("MSG");  break;
        case 17: typeStr = QStringLiteral("REPLY"); break;
        default: typeStr = QStringLiteral("TYPE%1").arg(msg.recordType); break;
    }

    QString result;
    result += QStringLiteral("┌─ [%1]  %2  %3  %4\n")
                  .arg(ts)
                  .arg(arrow)
                  .arg(typeStr)
                  .arg(msg.sender);

    result += QStringLiteral("│  %1\n").arg(msg.text.isEmpty()
        ? QStringLiteral("(empty)")
        : msg.text);

    result += QStringLiteral("│  flags=0x%1  |  #%2\n")
                  .arg(msg.flags, 4, 16, QChar('0'))
                  .arg(index);

    result += QStringLiteral("└───────────────────────────────────────\n");
    return result;
}

/* === Main Window === */

class MainWindow : public QMainWindow {
    Q_OBJECT
public:
    explicit MainWindow(QWidget *parent = nullptr)
        : QMainWindow(parent)
    {
        setWindowTitle(QStringLiteral("ATOM Message Log Reader"));
        resize(1000, 750);

        auto *central = new QWidget(this);
        auto *mainLayout = new QVBoxLayout(central);
        mainLayout->setContentsMargins(12, 12, 12, 12);
        mainLayout->setSpacing(8);

        /* -- Top bar -- */
        auto *topBar = new QHBoxLayout();
        topBar->setSpacing(10);

        m_openBtn = new QPushButton(QStringLiteral("📂 Open ATOM Log..."), this);
        m_openBtn->setMinimumHeight(38);
        m_openBtn->setStyleSheet(
            QStringLiteral("QPushButton {"
                           "  background-color: #2d6a9f;"
                           "  color: white;"
                           "  border: none;"
                           "  border-radius: 6px;"
                           "  padding: 8px 22px;"
                           "  font-size: 14px;"
                           "  font-weight: bold;"
                           "}"
                           "QPushButton:hover {"
                           "  background-color: #1d5a8f;"
                           "}"
                           "QPushButton:pressed {"
                           "  background-color: #0d4a7f;"
                           "}"));

        m_infoLabel = new QLabel(QStringLiteral(
            "<span style='color:#999;'>No file loaded</span>"), this);

        topBar->addWidget(m_openBtn);
        topBar->addWidget(m_infoLabel, 1);
        mainLayout->addLayout(topBar);

        /* -- Splitter: table on top, detail view below -- */
        auto *splitter = new QSplitter(Qt::Vertical, this);

        /* Table */
        m_table = new QTableWidget(0, 4, this);
        m_table->setHorizontalHeaderLabels({
            QStringLiteral("Time"),
            QStringLiteral("Dir"),
            QStringLiteral("Sender"),
            QStringLiteral("Message")
        });
        m_table->horizontalHeader()->setStretchLastSection(true);
        m_table->horizontalHeader()->setSectionResizeMode(0, QHeaderView::ResizeToContents);
        m_table->horizontalHeader()->setSectionResizeMode(1, QHeaderView::ResizeToContents);
        m_table->horizontalHeader()->setSectionResizeMode(2, QHeaderView::ResizeToContents);
        m_table->setSelectionBehavior(QAbstractItemView::SelectRows);
        m_table->setSelectionMode(QAbstractItemView::SingleSelection);
        m_table->setAlternatingRowColors(true);
        m_table->verticalHeader()->setVisible(false);
        m_table->setEditTriggers(QAbstractItemView::NoEditTriggers);
        m_table->setStyleSheet(
            QStringLiteral("QTableWidget {"
                           "  background-color: #1e1e1e;"
                           "  color: #d4d4d4;"
                           "  border: 1px solid #3c3c3c;"
                           "  border-radius: 4px;"
                           "  gridline-color: #333333;"
                           "  font-size: 12px;"
                           "}"
                           "QTableWidget::item { padding: 4px 8px; }"
                           "QTableWidget::item:alternate { background-color: #2a2a2a; }"
                           "QHeaderView::section {"
                           "  background-color: #2d2d2d;"
                           "  color: #cccccc;"
                           "  padding: 6px;"
                           "  border: 1px solid #3c3c3c;"
                           "  font-weight: bold;"
                           "}"
                           "QTableWidget::item:selected {"
                           "  background-color: #264f78;"
                           "}"));

        /* Detail text view */
        m_detailEdit = new QTextEdit(this);
        m_detailEdit->setReadOnly(true);
        m_detailEdit->setFont(QFont(QStringLiteral("Courier New"), 10));
        m_detailEdit->setStyleSheet(
            QStringLiteral("QTextEdit {"
                           "  background-color: #1e1e1e;"
                           "  color: #d4d4d4;"
                           "  border: 1px solid #3c3c3c;"
                           "  border-radius: 4px;"
                           "  padding: 8px;"
                           "}"
                           "QTextEdit:focus {"
                           "  border-color: #2d6a9f;"
                           "}"));

        splitter->addWidget(m_table);
        splitter->addWidget(m_detailEdit);
        splitter->setStretchFactor(0, 3);
        splitter->setStretchFactor(1, 2);

        mainLayout->addWidget(splitter, 1);
        setCentralWidget(central);

        /* Status bar */
        m_statusLabel = new QLabel(
            QStringLiteral("Ready. Open an ATOM message log file to begin."), this);
        statusBar()->addWidget(m_statusLabel);

        /* Connections */
        connect(m_openBtn, &QPushButton::clicked, this, &MainWindow::onOpenFile);
        connect(m_table, &QTableWidget::currentCellChanged,
                this, &MainWindow::onRowChanged);
    }

private slots:
    void onOpenFile()
    {
        QString filePath = QFileDialog::getOpenFileName(
            this,
            QStringLiteral("Select ATOM Message Log"),
            QString(),
            QStringLiteral("All Files (*);;Text Files (*.txt);;Tox Files (*.tox)")
        );

        if (filePath.isEmpty()) {
            return;
        }

        setWindowTitle(QStringLiteral("ATOM Message Log Reader — %1").arg(filePath));
        statusBar()->showMessage(QStringLiteral("Parsing %1...").arg(filePath));
        QApplication::processEvents();

        AtomParseResult result = parseAtomLog(filePath);

        if (!result.ok && !result.error.isEmpty()) {
            QMessageBox::critical(this,
                                  QStringLiteral("Parse Error"),
                                  result.error);
            statusBar()->showMessage(QStringLiteral("Error: %1").arg(result.error));
            return;
        }

        m_messages = result.messages;

        /* Fill table */
        m_table->setRowCount(0);
        m_table->setRowCount(m_messages.size());

        for (int i = 0; i < m_messages.size(); i++) {
            const AtomMessage &msg = m_messages[i];

            /* Time */
            auto *timeItem = new QTableWidgetItem(
                msg.timestamp.toString(QStringLiteral("yyyy-MM-dd HH:mm:ss")));

            /* Direction */
            QString dirStr;
            switch (msg.direction) {
                case 0:  dirStr = QStringLiteral("← IN");  break;
                case 1:  dirStr = QStringLiteral("→ OUT"); break;
                default: dirStr = QStringLiteral("↔ ?");    break;
            }
            auto *dirItem = new QTableWidgetItem(dirStr);
            dirItem->setTextAlignment(Qt::AlignCenter);

            /* Sender */
            auto *senderItem = new QTableWidgetItem(msg.sender);

            /* Message preview (first 80 chars) */
            QString preview = msg.text.left(80);
            if (msg.text.length() > 80) {
                preview += QStringLiteral("...");
            }
            auto *msgItem = new QTableWidgetItem(preview);

            m_table->setItem(i, 0, timeItem);
            m_table->setItem(i, 1, dirItem);
            m_table->setItem(i, 2, senderItem);
            m_table->setItem(i, 3, msgItem);
        }

        /* Update info */
        if (m_messages.size() > 0) {
            const AtomMessage &first = m_messages.first();
            const AtomMessage &last  = m_messages.last();

            m_infoLabel->setText(QStringLiteral(
                "<span style='color:#aaa;'>"
                "<b>%1</b> messages &nbsp;|&nbsp; "
                "<b>%2</b> &nbsp;→&nbsp; <b>%3</b> &nbsp;|&nbsp; "
                "File: %4 bytes"
                "</span>")
                .arg(m_messages.size())
                .arg(first.timestamp.toString(QStringLiteral("dd.MM HH:mm")))
                .arg(last.timestamp.toString(QStringLiteral("dd.MM HH:mm")))
                .arg(result.fileSize));
        }

        /* Select first row */
        m_table->selectRow(0);

        /* Save to .txt */
        QFileInfo fi(filePath);
        QString txtPath = fi.absolutePath() + QChar('/')
                          + fi.completeBaseName() + QStringLiteral("_parsed.txt");

        QFile outFile(txtPath);
        if (outFile.open(QIODevice::WriteOnly | QIODevice::Text)) {
            QTextStream out(&outFile);
            out << QStringLiteral("=== ATOM Message Log: %1 ===\n\n").arg(filePath);

            for (int i = 0; i < m_messages.size(); i++) {
                out << formatMessage(m_messages[i], i + 1) << QStringLiteral("\n");
            }

            outFile.close();
            statusBar()->showMessage(
                QStringLiteral("Parsed %1 message(s). Saved to %2")
                    .arg(m_messages.size()).arg(txtPath));
        } else {
            statusBar()->showMessage(
                QStringLiteral("Parsed %1 message(s). Could not save to %2")
                    .arg(m_messages.size()).arg(txtPath));
        }
    }

    void onRowChanged(int currentRow, int, int, int)
    {
        if (currentRow < 0 || currentRow >= m_messages.size()) {
            m_detailEdit->clear();
            return;
        }

        m_detailEdit->setPlainText(formatMessage(m_messages[currentRow], currentRow + 1));
    }

private:
    QPushButton    *m_openBtn    = nullptr;
    QLabel         *m_infoLabel  = nullptr;
    QLabel         *m_statusLabel = nullptr;
    QTableWidget   *m_table      = nullptr;
    QTextEdit      *m_detailEdit = nullptr;
    QList<AtomMessage> m_messages;
};

/* === Entry point === */

int main(int argc, char *argv[])
{
    QApplication app(argc, argv);
    app.setApplicationName(QStringLiteral("ATOM Message Log Reader"));
    app.setOrganizationName(QStringLiteral("ToxTools"));

    /* Global dark stylesheet */
    app.setStyleSheet(
        QStringLiteral(
            "QMainWindow { background-color: #2d2d2d; }"
            "QStatusBar { background-color: #252526; color: #cccccc; }"
            "QLabel { color: #cccccc; }"
            "QSplitter::handle { background-color: #3c3c3c; height: 2px; }"
        ));

    MainWindow w;
    w.show();

    return app.exec();
}

#include "qt_atom_reader.moc"
