/*
 * Fooyin
 * Copyright © 2024, Luke Taylor <luket@pm.me>
 *
 * Fooyin 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 3 of the License, or
 * (at your option) any later version.
 *
 * Fooyin 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 Fooyin.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

#include "libarchiveinput.h"

#include <QBuffer>
#include <QDir>
#include <QFileInfo>
#include <QLoggingCategory>
#include <QMimeDatabase>

#ifdef Q_OS_WIN
#define NOMINMAX
#endif
#include <archive_entry.h>

#include <array>

Q_LOGGING_CATEGORY(LIBARCH, "fy.libarchive")

using namespace Qt::StringLiterals;

namespace {
QStringList fileExtensions()
{
    static const QStringList extensions = {u"zip"_s, u"rar"_s, u"tar"_s, u"gz"_s, u"7z"_s, u"vgm7z"_s};
    return extensions;
}

bool isImageFile(const QString& filePath)
{
    const QMimeDatabase mimeDatabase;
    const QMimeType mimeType = mimeDatabase.mimeTypeForFile(filePath);

    return mimeType.name().startsWith("image/"_L1);
}

bool setupForReading(archive* archive, const QString& filename)
{
    archive_read_support_filter_all(archive);
    archive_read_support_format_all(archive);

    if(archive_read_open_filename(archive, QFile::encodeName(filename).constData(), 10240) != ARCHIVE_OK) {
        qCWarning(LIBARCH) << "Unable to open archive:" << archive_error_string(archive);
        qCWarning(LIBARCH) << "Archive corrupted or insufficient permissions";
        return false;
    }

    return true;
}

uint64_t entryModifiedTimeMs(archive_entry* entry)
{
    if(!entry || archive_entry_mtime_is_set(entry) == 0) {
        return 0;
    }

    const auto seconds = archive_entry_mtime(entry);
    if(seconds < 0) {
        return 0;
    }

    return static_cast<uint64_t>(seconds) * 1000ULL;
}

} // namespace

namespace Fooyin::LibArchive {
LibArchiveIODevice::LibArchiveIODevice(ArchivePtr archive, archive_entry* entry, QObject* parent)
    : QIODevice{parent}
    , m_archive{std::move(archive)}
    , m_entry{entry}
{
    open(QIODevice::ReadOnly);
    m_buffer.open(QBuffer::ReadWrite);
}

LibArchiveIODevice::~LibArchiveIODevice()
{
    m_archive.reset();
}

bool LibArchiveIODevice::seek(qint64 pos)
{
    if(!isOpen()) {
        return false;
    }

    QIODevice::seek(pos);

    if(pos <= m_buffer.size()) {
        return m_buffer.seek(pos);
    }

    qint64 bufferLen = pos - m_buffer.size();
    std::vector<char> tmpBuffer(1024);

    while(bufferLen > 0) {
        qint64 lenToRead = std::min(static_cast<qint64>(tmpBuffer.size()), bufferLen);

        lenToRead = archive_read_data(m_archive.get(), tmpBuffer.data(), static_cast<size_t>(lenToRead));
        if(lenToRead > 0) {
            m_buffer.buffer().append(tmpBuffer.data(), static_cast<qsizetype>(lenToRead));
            bufferLen -= lenToRead;
            continue;
        }
        if(lenToRead < 0) {
            qCWarning(LIBARCH) << "Seeking failed:" << archive_error_string(m_archive.get());
            setErrorString(QString::fromLocal8Bit(archive_error_string(m_archive.get())));
            close();
        }
        return false;
    }

    return m_buffer.seek(pos);
}

qint64 LibArchiveIODevice::size() const
{
    return archive_entry_size(m_entry);
}

archive* LibArchiveIODevice::releaseArchive()
{
    return m_archive.release();
}

qint64 LibArchiveIODevice::readData(char* data, qint64 maxlen)
{
    if(!isOpen()) {
        return -1;
    }

    if(m_buffer.pos() + maxlen > m_buffer.size()) {
        const qint64 lenToRead = m_buffer.pos() + maxlen - m_buffer.size();
        std::vector<char> tmpBuffer(lenToRead);

        const auto read = archive_read_data(m_archive.get(), tmpBuffer.data(), lenToRead);
        if(read > 0) {
            m_buffer.buffer().append(tmpBuffer.data(), read);
        }
        else if(read < 0) {
            qCWarning(LIBARCH) << "Reading failed:" << archive_error_string(m_archive.get());
            setErrorString(QString::fromLocal8Bit(archive_error_string(m_archive.get())));
            return -1;
        }
    }

    return m_buffer.read(data, maxlen);
}

qint64 LibArchiveIODevice::writeData(const char* /*data*/, qint64 /*len*/)
{
    return -1;
}

QStringList LibArchiveReader::extensions() const
{
    return fileExtensions();
}

QString LibArchiveReader::type() const
{
    return m_type;
}

bool LibArchiveReader::init(const QString& file)
{
    m_file = file;
    m_type = QFileInfo{file}.suffix();
    return true;
}

ArchiveEntryData LibArchiveReader::entry(const QString& file)
{
    ArchivePtr archive{archive_read_new()};

    if(!setupForReading(archive.get(), m_file)) {
        return {};
    }

    archive_entry* entry{nullptr};

    while(archive_read_next_header(archive.get(), &entry) == ARCHIVE_OK) {
        if(archive_read_has_encrypted_entries(archive.get()) == 1) {
            qCInfo(LIBARCH) << "Unable to read encrypted file" << m_file;
            return {};
        }

        if(archive_entry_filetype(entry) == AE_IFREG) {
            const QString entryPath = QDir::fromNativeSeparators(QFile::decodeName(archive_entry_pathname(entry)));
            if(entryPath == file) {
                const la_int64_t entrySize = archive_entry_size(entry);
                return {.info   = {.path          = entryPath,
                                   .modifiedTime  = entryModifiedTimeMs(entry),
                                   .size          = entrySize > 0 ? static_cast<uint64_t>(entrySize) : 0,
                                   .isRegularFile = true},
                        .device = std::make_unique<LibArchiveIODevice>(std::move(archive), entry, nullptr)};
            }
        }
    }

    qCDebug(LIBARCH) << "Unable to find" << file << "in" << m_file;
    return {};
}

bool LibArchiveReader::copyEntryToDevice(const QString& file, QIODevice* device,
                                         const ShouldContinueCallback& shouldContinue)
{
    if(!device || !device->isWritable()) {
        return false;
    }

    const ArchivePtr archive{archive_read_new()};

    if(!setupForReading(archive.get(), m_file)) {
        return false;
    }

    archive_entry* entry{nullptr};

    while(archive_read_next_header(archive.get(), &entry) == ARCHIVE_OK) {
        if(archive_read_has_encrypted_entries(archive.get()) == 1) {
            qCInfo(LIBARCH) << "Unable to read encrypted file" << m_file;
            return false;
        }

        if(archive_entry_filetype(entry) != AE_IFREG) {
            continue;
        }

        const QString entryPath = QDir::fromNativeSeparators(QFile::decodeName(archive_entry_pathname(entry)));
        if(entryPath != file) {
            continue;
        }

        if(auto* outputFile = qobject_cast<QFile*>(device); outputFile && outputFile->isOpen()) {
            if(!shouldContinue()) {
                return false;
            }
            const int ret = archive_read_data_into_fd(archive.get(), outputFile->handle());
            if(ret == ARCHIVE_OK) {
                return true;
            }
            qCWarning(LIBARCH) << "Extracting entry failed:" << archive_error_string(archive.get());
            return false;
        }

        std::array<char, 64UL * 1024> buffer{};
        while(shouldContinue()) {
            const la_ssize_t read = archive_read_data(archive.get(), buffer.data(), buffer.size());
            if(read == 0) {
                return true;
            }
            if(read < 0) {
                qCWarning(LIBARCH) << "Reading failed:" << archive_error_string(archive.get());
                return false;
            }
            if(device->write(buffer.data(), read) != read) {
                qCWarning(LIBARCH) << "Writing extracted entry failed:" << device->errorString();
                return false;
            }
        }

        return false;
    }

    qCDebug(LIBARCH) << "Unable to find" << file << "in" << m_file;
    return false;
}

bool LibArchiveReader::readEntries(const ReadEntryInfoCallback& readEntry)
{
    const ArchivePtr archive{archive_read_new()};

    if(!setupForReading(archive.get(), m_file)) {
        return false;
    }

    archive_entry* entry{nullptr};

    while(archive_read_next_header(archive.get(), &entry) == ARCHIVE_OK) {
        if(archive_read_has_encrypted_entries(archive.get()) == 1) {
            qCInfo(LIBARCH) << "Unable to read encrypted file" << m_file;
            return false;
        }

        const la_int64_t entrySize = archive_entry_size(entry);
        const ArchiveEntryInfo entryInfo{
            .path          = QDir::fromNativeSeparators(QFile::decodeName(archive_entry_pathname(entry))),
            .modifiedTime  = entryModifiedTimeMs(entry),
            .size          = entrySize > 0 ? static_cast<uint64_t>(entrySize) : 0,
            .isRegularFile = archive_entry_filetype(entry) == AE_IFREG,
        };

        if(readEntry && !readEntry(entryInfo)) {
            return true;
        }
    }

    return true;
}

bool LibArchiveReader::readTracks(ReadEntryCallback readEntry)
{
    ArchivePtr archive{archive_read_new()};

    if(!setupForReading(archive.get(), m_file)) {
        return false;
    }

    archive_entry* entry{nullptr};

    while(archive_read_next_header(archive.get(), &entry) == ARCHIVE_OK) {
        if(archive_read_has_encrypted_entries(archive.get()) == 1) {
            qCInfo(LIBARCH) << "Unable to read encrypted file" << m_file;
            return false;
        }

        if(archive_entry_filetype(entry) == AE_IFREG) {
            const la_int64_t entrySize = archive_entry_size(entry);
            ArchiveEntryData entryData{
                .info   = {.path          = QDir::fromNativeSeparators(QFile::decodeName(archive_entry_pathname(entry))),
                           .modifiedTime  = entryModifiedTimeMs(entry),
                           .size          = entrySize > 0 ? static_cast<uint64_t>(entrySize) : 0,
                           .isRegularFile = true},
                .device = std::make_unique<LibArchiveIODevice>(std::move(archive), entry, nullptr)};
            auto* archiveDevice = static_cast<LibArchiveIODevice*>(entryData.device.get());
            readEntry(std::move(entryData));
            archive.reset(archiveDevice->releaseArchive());
        }
    }

    return true;
}

QByteArray LibArchiveReader::readCover(const Track& track, Track::Cover cover)
{
    if(cover != Track::Cover::Front) {
        // Only read front cover for now
        return {};
    }

    ArchivePtr archive{archive_read_new()};

    if(!setupForReading(archive.get(), m_file)) {
        return {};
    }

    QByteArray coverData;

    archive_entry* entry{nullptr};

    while(archive_read_next_header(archive.get(), &entry) == ARCHIVE_OK) {
        if(archive_read_has_encrypted_entries(archive.get()) == 1) {
            qCInfo(LIBARCH) << "Unable to read encrypted file" << m_file;
            return {};
        }

        if(archive_entry_filetype(entry) == AE_IFREG) {
            const QString entryPath = QDir::fromNativeSeparators(QFile::decodeName(archive_entry_pathname(entry)));

            if(isImageFile(entryPath)) {
                const QFileInfo info{entryPath};
                if(info.path() == track.relativeArchivePath()) {
                    auto entryDev = std::make_unique<LibArchiveIODevice>(std::move(archive), entry, nullptr);
                    if(entryDev) {
                        // Use first valid image
                        coverData = entryDev->readAll();
                        break;
                    }
                }
            }
        }
    }

    return coverData;
}
} // namespace Fooyin::LibArchive

#include "moc_libarchiveinput.cpp"
