/*
 * 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 "wavebardatabase.h"

#include <core/track.h>
#include <utils/crypto.h>
#include <utils/database/dbquery.h>
#include <utils/datastream.h>

using namespace Qt::StringLiterals;

namespace {
using Fooyin::WaveBar::WaveformData;

QDataStream& operator>>(QDataStream& stream, std::vector<WaveformData<int16_t>::ChannelData>& data)
{
    quint32 size{0};
    stream >> size;
    if(stream.status() != QDataStream::Ok) {
        data.clear();
        return stream;
    }

    data.clear();
    data.reserve(size);

    while(size > 0) {
        --size;

        WaveformData<int16_t>::ChannelData channel;
        stream >> channel.max;
        stream >> channel.min;
        stream >> channel.rms;

        if(stream.status() != QDataStream::Ok) {
            data.clear();
            return stream;
        }

        data.emplace_back(channel);
    }
    return stream;
}

QDataStream& operator<<(QDataStream& stream, const std::vector<WaveformData<int16_t>::ChannelData>& data)
{
    stream << static_cast<quint32>(data.size());

    for(const auto& channel : data) {
        stream << channel.max;
        stream << channel.min;
        stream << channel.rms;
    }

    return stream;
}

QByteArray serialiseData(const WaveformData<int16_t>& data)
{
    QByteArray out;
    QDataStream stream{&out, QDataStream::WriteOnly};
    stream.setVersion(QDataStream::Qt_6_0);

    stream << data.channelData;

    out = qCompress(out, 9);

    return out;
}

bool deserialiseData(const QByteArray& cacheData, WaveformData<int16_t>& data)
{
    data.channelData.clear();

    QByteArray in = qUncompress(cacheData);
    if(in.isEmpty() && !cacheData.isEmpty()) {
        return false;
    }

    QDataStream stream{&in, QDataStream::ReadOnly};
    stream.setVersion(QDataStream::Qt_6_0);

    stream >> data.channelData;
    return stream.status() == QDataStream::Ok;
}
} // namespace

namespace Fooyin::WaveBar {
void WaveBarDatabase::initialiseDatabase() const
{
    const auto statement = u"CREATE TABLE IF NOT EXISTS WaveCache ("
                           "TrackKey TEXT PRIMARY KEY, "
                           "Data BLOB);"_s;

    DbQuery query{db(), statement};
    query.exec();
}

bool WaveBarDatabase::existsInCache(const QString& key) const
{
    const auto statement = u"SELECT COUNT(*) FROM WaveCache WHERE TrackKey = :trackKey;"_s;

    DbQuery query{db(), statement};

    query.bindValue(u":trackKey"_s, key);

    if(query.exec() && query.next()) {
        return query.value(0).toInt() > 0;
    }

    return false;
}

bool WaveBarDatabase::loadCachedData(const QString& key, WaveformData<int16_t>& data) const
{
    const auto statement = u"SELECT Data FROM WaveCache WHERE TrackKey = :trackKey;"_s;

    DbQuery query{db(), statement};

    query.bindValue(u":trackKey"_s, key);

    if(query.exec() && query.next()) {
        const QByteArray cacheData = query.value(0).toByteArray();
        return deserialiseData(cacheData, data);
    }

    return false;
}

bool WaveBarDatabase::storeInCache(const QString& key, const WaveformData<int16_t>& data) const
{
    const auto statement = u"INSERT OR REPLACE INTO WaveCache (TrackKey, Data) VALUES (:trackKey, :data);"_s;

    DbQuery query{db(), statement};

    query.bindValue(u":trackKey"_s, key);
    query.bindValue(u":data"_s, serialiseData(data));

    return query.exec();
}

bool WaveBarDatabase::removeFromCache(const QString& key) const
{
    const auto statement = u"DELETE FROM WaveCache WHERE TrackKey = :trackKey;"_s;

    DbQuery query{db(), statement};
    query.bindValue(u":trackKey"_s, key);

    return query.exec();
}

bool WaveBarDatabase::removeFromCache(const QStringList& keys) const
{
    if(keys.empty()) {
        return true;
    }

    QStringList placeholders;
    placeholders.reserve(keys.size());
    for(qsizetype i{0}; i < keys.size(); ++i) {
        placeholders.emplace_back(u":key%1"_s.arg(i));
    }

    const QString statement = u"DELETE FROM WaveCache WHERE TrackKey IN (%1);"_s.arg(placeholders.join(u','));

    DbQuery query{db(), statement};
    for(qsizetype i{0}; i < keys.size(); ++i) {
        query.bindValue(placeholders.at(i), keys.at(i));
    }

    return query.exec();
}

bool WaveBarDatabase::clearCache() const
{
    const auto statement = u"DELETE FROM WaveCache;"_s;

    DbQuery query{db(), statement};
    DbQuery cleanQuery{db(), u"VACUUM"_s};

    return query.exec() && cleanQuery.exec();
}

QString WaveBarDatabase::cacheKey(const Track& track)
{
    return cacheKey(track, track.channels());
}

QString WaveBarDatabase::cacheKey(const Track& track, int channels)
{
    return Utils::generateHash(track.hash(), QString::number(track.duration()), QString::number(track.sampleRate()),
                               QString::number(channels));
}
} // namespace Fooyin::WaveBar
