#include "sample.h"
using namespace Xybrid::Data;

#include "data/project.h"

#include <deque>

#include <QCborValue>
#include <QCborMap>
#include <QCborArray>

#include <QJsonDocument>
#include <QJsonArray>
#include <QJsonObject>

#include <QFileInfo>

#include <QAudioDecoder>
#include <QAudioFormat>

#include <QProcess>
#include <QDataStream>

#include <QEventLoop>

#define qs QStringLiteral

std::array<float, 2> Sample::plotBetween(size_t ch, size_t start, size_t end) const {
    if (end < start) end = start;
    if (ch >= 2 || start >= data[ch].size()) return {0, 0};
    end = std::min(end, data[ch].size());
    float mx = -100;
    float mn = 100;

    for (size_t i = start; i <= end; i++) {
        auto v = data[ch][i];
        mx = std::max(mx, v);
        mn = std::min(mn, v);
    }

    return {mn, mx};
}

QCborMap Sample::toCbor() const {
    QCborMap m;

    m[qs("name")] = name;
    m[qs("rate")] = sampleRate;
    {
        QCborArray ch;

        auto n = static_cast<size_t>(numChannels());
        for (size_t i = 0; i < n; i++) {
            ch[static_cast<qsizetype>(i)] = QByteArray(reinterpret_cast<const char*>(data[i].data()), static_cast<int>(data[i].size() * sizeof(data[i][0])));
        }

        m[qs("channels")] = ch;
    }

    return m;
}

std::shared_ptr<Sample> Sample::fromCbor(const QCborMap& m, QUuid uuid) {
    auto smp = std::make_shared<Sample>();
    smp->uuid = uuid;
    smp->name = m.value("name").toString();

    smp->sampleRate = static_cast<int>(m.value("rate").toInteger(48000));

    auto ch = m.value("channels").toArray();
    auto s = static_cast<size_t>(ch.size());

    for (size_t i = 0; i < s; i++) {
        auto c = ch[static_cast<qint64>(i)].toByteArray();
        auto bs = static_cast<size_t>(c.size());
        smp->data[i].resize(bs / sizeof(*smp->data[i].begin()));
        memcpy(smp->data[i].data(), c.constData(), bs);
    }

    return smp;
}
std::shared_ptr<Sample> Sample::fromCbor(const QCborValue& m, QUuid uuid) { return fromCbor(m.toMap(), uuid); }

bool Sample::changeUuid(QUuid newUuid) {
    if (!project) return false;
    //if (!project->samples.contains(uuid)) return false;
    if (project->samples.contains(newUuid)) return false;
    auto ptr = this->shared_from_this();
    project->samples.remove(uuid);
    uuid = newUuid;
    project->samples.insert(uuid, ptr);

    return true;
}

void Sample::newUuid() { changeUuid(QUuid::createUuid()); }

#ifdef OLD_SAMPLE_IMPORT
namespace {
    bool blah [[maybe_unused]];
    template<typename T> void insertbuf(std::shared_ptr<Sample> smp, const T* data, size_t len, size_t channels) {
        for (size_t i = 0; i < len; i++) {
            auto tch = i % channels;
            if (tch < 2) smp->data[tch].push_back(static_cast<float>(data[i]) / std::numeric_limits<T>::max());
        }
    }
    template<> void insertbuf(std::shared_ptr<Sample> smp, const float* data, size_t len, size_t channels) {
        for (size_t i = 0; i < len; i++) {
            auto tch = i % channels;
            if (tch < 2) smp->data[tch].push_back(data[i]);
        }
    }
}

std::shared_ptr<Sample> Sample::fromFile(QString fileName) {
    QAudioFormat ifmt;
    ifmt.setSampleType(QAudioFormat::SignedInt);
    ifmt.setSampleSize(16);
    auto dec = std::make_shared<QAudioDecoder>();
    dec->setAudioFormat(ifmt);
    dec->setSourceFilename(fileName);

    std::deque<QAudioBuffer> bufs;
    QEventLoop loop;
    QObject::connect(dec.get(), &QAudioDecoder::bufferReady, &loop, [&bufs, dec] { bufs.push_back(dec->read()); });
    QObject::connect(dec.get(), &QAudioDecoder::finished, &loop, [&loop] { loop.exit(); });
    QObject::connect(dec.get(), static_cast<void(QAudioDecoder::*)(QAudioDecoder::Error)>(&QAudioDecoder::error), &loop, [&loop] { loop.exit(); });
    dec->start();
    loop.exec();
    if (dec->error()) {
        qDebug() << "sample decode error:" << dec->errorString();
        return nullptr; // errored
    }
    if (bufs.empty()) return nullptr; // no sample data

    auto fmt = bufs.front().format();
    size_t channels = static_cast<size_t>(fmt.channelCount());
    if (channels == 0) return nullptr; // zero channels means no sample data
    size_t len = 0;
    for (auto b : bufs) len += static_cast<size_t>(b.frameCount());
    qDebug() << "format:" << fmt;
    //qDebug() << "total length:" << len;

    auto smp = std::make_shared<Sample>();
    if (channels >= 1) smp->data[0].reserve(len);
    if (channels >= 2) smp->data[1].reserve(len);

    auto st = fmt.sampleType();
    auto sz = fmt.sampleSize();
    if (st == QAudioFormat::SignedInt) {
        if (sz == 16) for (auto b : bufs) insertbuf(smp, b.constData<int16_t>(), static_cast<size_t>(b.sampleCount()), channels);
        else if (sz == 32) for (auto b : bufs) insertbuf(smp, b.constData<int32_t>(), static_cast<size_t>(b.sampleCount()), channels);
        else return nullptr; // unsupported
    } else if (st == QAudioFormat::Float) {
        if (sz == 32) for (auto b : bufs) insertbuf(smp, b.constData<float>(), static_cast<size_t>(b.sampleCount()), channels);
        else return nullptr; // unsupported
    } else return nullptr; // unsupported

    smp->sampleRate = fmt.sampleRate();
    smp->uuid = QUuid::createUuid();
    smp->name = QFileInfo(fileName).baseName();

    return smp;
}
#else
std::shared_ptr<Sample> Sample::fromFile(QString fileName) {
    QJsonObject info;
    {
        // get stream info
        QProcess probe;
        QStringList param;
        param << "-v" << "quiet" << "-show_streams" << "-select_streams" << "a" << "-of" << "json";
        param << fileName;
        probe.start("ffprobe", param);
        probe.waitForFinished();

        auto doc = QJsonDocument::fromJson(probe.readAllStandardOutput());
        info = doc.object()["streams"].toArray().first().toObject();
    }
    if (!info.contains("sample_rate") || !info.contains("channels")) return nullptr; // no/invalid audio streams

    int channels = info["channels"].toInt();
    int sampleRate = info["sample_rate"].toString().toInt(); // for some reason ffprobe stores this as a string??

    auto smp = std::make_shared<Sample>();
    smp->sampleRate = sampleRate;

    // grab raw pcm_f32le via ffmpeg
    QByteArray raw;
    {
        QProcess dec;
        QStringList param;
        param << "-i" << fileName << "-f" << "f32le" << "-acodec" << "pcm_f32le" << "-";
        dec.start("ffmpeg", param);
        dec.waitForFinished();
        raw = dec.readAllStandardOutput();
    }
    // pre-size sample data buffers
    auto len = static_cast<size_t>((raw.length() / channels) / 4);
    smp->data[0].reserve(len);
    if (channels > 1) smp->data[1].reserve(len);

    // read raw bytes into channels
    QDataStream r(&raw, QIODevice::ReadOnly);
    size_t chs = static_cast<size_t>(channels);
    size_t ch = chs;
    float f;
    while (!r.atEnd()) {
        r.readRawData(reinterpret_cast<char*>(&f), sizeof(f));
        ch = (ch+1) % chs;
        if (ch < 2) smp->data[ch].push_back(f);
    }

    // add info
    smp->uuid = QUuid::createUuid();
    smp->name = QFileInfo(fileName).baseName();

    return smp;
}

#endif

namespace {
    bool exporting = false;
    std::unordered_map<Sample*, bool> exportMap;
}

void Sample::startExport() {
    exporting = true;
    exportMap.reserve(16);
}

std::vector<std::shared_ptr<Sample>> Sample::finishExport() {
    std::vector<std::shared_ptr<Sample>> v;
    if (exporting) {
        exporting = false;
        v.reserve(exportMap.size());
        for (auto it : exportMap) v.push_back(it.first->shared_from_this());
        exportMap.clear();
    }
    return v;
}

void Sample::markForExport() {
    if (exporting) {
        exportMap[this] = true;
    }
}
