/***************************************************************************
 *   Copyright (C) 2013-2025 by Ilya Kotov                                 *
 *   forkotov02@ya.ru                                                      *
 *                                                                         *
 *   This program 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 2 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 *   This program 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 this program; if not, write to the                         *
 *   Free Software Foundation, Inc.,                                       *
 *   51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.         *
 ***************************************************************************/

#include <QSettings>
#include <QProgressBar>
#include <QThreadPool>
#include <qmmpui/playlisttrack.h>
#include <qmmpui/metadataformatter.h>
#include <qmmpui/filedialog.h>
#include <qmmp/metadatamanager.h>
#include <algorithm>
#include <taglib/mpegfile.h>
#include <taglib/apetag.h>
#include <taglib/flacfile.h>
#include <taglib/xiphcomment.h>
#include <taglib/oggflacfile.h>
#include <taglib/vorbisfile.h>
#include <taglib/wavpackfile.h>
#include <taglib/id3v2tag.h>
#include <taglib/textidentificationframe.h>
#include <taglib/mp4file.h>
#include <taglib/opusfile.h>
#include "rgscanner.h"
#include "gain_analysis.h"
#include "ui_rgscandialog.h"
#include "rgscandialog.h"

#define FILE_SKIPPED (Qt::UserRole + 1)

struct ReplayGainInfoItem
{
    QMap<Qmmp::ReplayGainKey, double> info;
    QString url;
    GainHandle_t *handle;
};

RGScanDialog::RGScanDialog(QList<PlayListTrack *> tracks,  QWidget *parent) : QDialog(parent),
    m_ui(new Ui::RGScanDialog)
{
    m_ui->setupUi(this);
    m_ui->tableWidget->verticalHeader()->setDefaultSectionSize(fontMetrics().height() + 3);
    m_ui->tableWidget->verticalHeader()->setSectionResizeMode(QHeaderView::Fixed);

    QStringList paths;
    MetaDataFormatter formatter(u"%if(%p&%t,%p - %t,%f) - %l"_s);
    for(const PlayListTrack *track : std::as_const(tracks))
    {
        //skip streams
        if(track->duration() == 0 || track->path().contains(u"://"_s))
            continue;
        //skip duplicates
        if(paths.contains(track->path()))
            continue;
        //skip unsupported files
        if(!MetaDataManager::instance()->supports(track->path()))
            continue;

        QString ext = track->path().section(QLatin1Char('.'), -1).toLower();

        if((ext == "mp3"_L1) || //mpeg 1 layer 3
                ext == "flac"_L1 || //native flac
                ext == "oga"_L1 || //ogg flac
                ext == "ogg"_L1 ||  //ogg vorbis
                ext == "wv"_L1 || //wavpack
                ext == "m4a"_L1 || //aac (mp4 container)
                ext == "opus"_L1)
        {
            paths.append(track->path());
            QString name = formatter.format(track);
            QTableWidgetItem *item = new QTableWidgetItem(name);
            item->setData(Qt::UserRole, track->path());
            item->setData(Qt::ToolTipRole, track->path());
            m_ui->tableWidget->insertRow(m_ui->tableWidget->rowCount());
            m_ui->tableWidget->setItem(m_ui->tableWidget->rowCount() - 1, 0, item);
            QProgressBar *progressBar = new QProgressBar(this);
            progressBar->setRange(0, 100);
            m_ui->tableWidget->setCellWidget(m_ui->tableWidget->rowCount() - 1, 1, progressBar);
        }
    }

    m_ui->tableWidget->resizeColumnsToContents();
    m_ui->writeButton->setEnabled(false);
    //read settings
    QSettings settings;
    restoreGeometry(settings.value(u"RGScanner/geometry"_s).toByteArray());
    m_ui->trackCheckBox->setChecked(settings.value(u"RGScanner/write_track"_s, true).toBool());
    m_ui->albumCheckBox->setChecked(settings.value(u"RGScanner/write_album"_s, true).toBool());
    m_ui->skipScannedCheckBox->setChecked(settings.value(u"RGScanner/skip_scanned"_s, true).toBool());
}

RGScanDialog::~RGScanDialog()
{
    stop();
    qDeleteAll(m_replayGainItemList);
    m_replayGainItemList.clear();
    delete m_ui;
}

void RGScanDialog::on_calculateButton_clicked()
{
    m_ui->writeButton->setEnabled(false);
    for(int i = 0; i < m_ui->tableWidget->rowCount(); ++i)
    {
        QString url = m_ui->tableWidget->item(i, 0)->data(Qt::UserRole).toString();
        RGScanner *scanner = new RGScanner();
        m_ui->tableWidget->item(i, 0)->setData(FILE_SKIPPED, false);

        if(!scanner->prepare(url))
        {
            m_ui->tableWidget->setItem(i, 2, new QTableWidgetItem(tr("Error")));
            delete scanner;
            continue;
        }

        if(m_ui->skipScannedCheckBox->isChecked() && !scanner->oldReplayGainInfo().isEmpty())
        {
            qCDebug(plugin, "skipping scanned file..");
            m_ui->tableWidget->item(i, 0)->setData(FILE_SKIPPED, true);
            QMap<Qmmp::ReplayGainKey, double> rg = scanner->oldReplayGainInfo();
            m_ui->tableWidget->setItem(i, 2, new QTableWidgetItem(tr("%1 dB").arg(rg.value(Qmmp::REPLAYGAIN_TRACK_GAIN))));
            m_ui->tableWidget->setItem(i, 3, new QTableWidgetItem(tr("%1 dB").arg(rg.value(Qmmp::REPLAYGAIN_ALBUM_GAIN))));
            m_ui->tableWidget->setItem(i, 4, new QTableWidgetItem(QString::number(rg.value(Qmmp::REPLAYGAIN_TRACK_PEAK))));
            m_ui->tableWidget->setItem(i, 5, new QTableWidgetItem(QString::number(rg.value(Qmmp::REPLAYGAIN_ALBUM_PEAK))));
            delete scanner;
            continue;
        }

        scanner->setAutoDelete(false);
        m_scanners.append(scanner);
        connect(scanner, &RGScanner::progress, qobject_cast<QProgressBar *>(m_ui->tableWidget->cellWidget(i, 1)),
                &QProgressBar::setValue);
        connect(scanner, &RGScanner::finished, this, &RGScanDialog::onScanFinished);
        QThreadPool::globalInstance()->start(scanner);
    }
}

void RGScanDialog::onScanFinished(const QString &url)
{
    for(int i = 0; i < m_ui->tableWidget->rowCount(); ++i)
    {
        if(url != m_ui->tableWidget->item(i, 0)->data(Qt::UserRole).toString())
            continue;
        RGScanner *scanner = findScannerByUrl(url);
        if(!scanner)
            qCFatal(plugin) << "unable to find scanner by URL!";
        m_ui->tableWidget->setItem(i, 2, new QTableWidgetItem(tr("%1 dB").arg(scanner->gain())));
        m_ui->tableWidget->setItem(i, 4, new QTableWidgetItem(QString::number(scanner->peak())));
        break;
    }

    bool stopped = std::all_of(m_scanners.cbegin(), m_scanners.cend(), [](RGScanner *scanner) { return !scanner->isRunning() && !scanner->isPending(); });

    if(stopped)
    {
        qCDebug(plugin, "all threads are finished");
        QThreadPool::globalInstance()->waitForDone();

        QMultiMap<QString, ReplayGainInfoItem*> itemGroupMap; //items grouped  by album

        //group by album name
        for(RGScanner *scanner : std::as_const(m_scanners))
        {
            if(!scanner->hasValues())
                continue;
            ReplayGainInfoItem *item = new ReplayGainInfoItem;
            item->info[Qmmp::REPLAYGAIN_TRACK_GAIN] = scanner->gain();
            item->info[Qmmp::REPLAYGAIN_TRACK_PEAK] = scanner->peak();
            item->url = scanner->url();
            item->handle = scanner->handle();
            QString album = getAlbumName(item->url);
            itemGroupMap.insert(album, item);
        }
        //calculate album peak and gain
        for(const QString &album : itemGroupMap.keys())
        {
            QList<ReplayGainInfoItem*> items = itemGroupMap.values(album);
            GainHandle_t **a = (GainHandle_t **) malloc(items.count()*sizeof(GainHandle_t *));
            double album_peak = 0;
            for(int i = 0; i < items.count(); ++i)
            {
                a[i] = items[i]->handle;
                album_peak = qMax(items[i]->info[Qmmp::REPLAYGAIN_TRACK_PEAK], album_peak);
            }
            double album_gain = GetAlbumGain(a, items.count());
            free(a);
            for(ReplayGainInfoItem *item : std::as_const(items))
            {
                item->info[Qmmp::REPLAYGAIN_ALBUM_PEAK] = album_peak;
                item->info[Qmmp::REPLAYGAIN_ALBUM_GAIN] = album_gain;
            }
        }
        //clear scanners
        qDeleteAll(m_scanners);
        m_scanners.clear();
        //clear previous replaygain information
        qDeleteAll(m_replayGainItemList);
        m_replayGainItemList.clear();
        //update table
        m_replayGainItemList = itemGroupMap.values();
        for(int i = 0; i < m_ui->tableWidget->rowCount(); ++i)
        {
            QString itemUrl = m_ui->tableWidget->item(i, 0)->data(Qt::UserRole).toString();
            bool found = false;
            for(const ReplayGainInfoItem *item : std::as_const(m_replayGainItemList))
            {
                if(item->url == itemUrl)
                {
                    found = true;
                    double album_gain = item->info[Qmmp::REPLAYGAIN_ALBUM_GAIN];
                    double album_peak = item->info[Qmmp::REPLAYGAIN_ALBUM_PEAK];
                    m_ui->tableWidget->setItem(i, 3, new QTableWidgetItem(tr("%1 dB").arg(album_gain)));
                    m_ui->tableWidget->setItem(i, 5, new QTableWidgetItem(QString::number(album_peak)));
                }
            }
            if(!found && !m_ui->tableWidget->item(i, 0)->data(FILE_SKIPPED).toBool())
                m_ui->tableWidget->setItem(i, 3, new QTableWidgetItem(tr("Error")));
        }

        //clear items
        itemGroupMap.clear();

        m_ui->writeButton->setEnabled(true);
    }
}

void RGScanDialog::reject()
{
    QSettings settings;
    settings.setValue(u"RGScanner/geometry"_s, saveGeometry());
    settings.setValue(u"RGScanner/write_track"_s, m_ui->trackCheckBox->isChecked());
    settings.setValue(u"RGScanner/write_album"_s, m_ui->albumCheckBox->isChecked());
    settings.setValue(u"RGScanner/skip_scanned"_s, m_ui->skipScannedCheckBox->isChecked());
    QDialog::reject();
}

void RGScanDialog::stop()
{
    if(m_scanners.isEmpty())
        return;
    for(RGScanner *scaner : std::as_const(m_scanners))
    {
        scaner->stop();
    }
    QThreadPool::globalInstance()->waitForDone();
    qDeleteAll(m_scanners);
    m_scanners.clear();
}

RGScanner *RGScanDialog::findScannerByUrl(const QString &url)
{
    auto it = std::find_if(m_scanners.cbegin(), m_scanners.cend(), [url](RGScanner *scanner){ return scanner->url() == url; });
    return it == m_scanners.cend() ? nullptr : *it;
}

QString RGScanDialog::getAlbumName(const QString &url)
{
    QList<TrackInfo *> infoList = MetaDataManager::instance()->createPlayList(url);
    if(infoList.isEmpty())
        return QString();
    QString album = infoList.constFirst()->value(Qmmp::ALBUM);
    qDeleteAll(infoList);
    return album;
}

TagLib::String RGScanDialog::gainToString(double value)
{
    return QStringToTString(QStringLiteral("%1 dB").arg(value, 0, 'f', 2));
}

TagLib::String RGScanDialog::peakToString(double value)
{
    return QStringToTString(QStringLiteral("%1").arg(value, 0, 'f', 6));
}

TagLib::StringList RGScanDialog::gainToStringList(double value)
{
   return TagLib::StringList(gainToString(value));
}

TagLib::StringList RGScanDialog::peakToStringList(double value)
{
    return TagLib::StringList(peakToString(value));
}

void RGScanDialog::writeAPETag(TagLib::APE::Tag *tag, ReplayGainInfoItem *item)
{
    if(m_ui->trackCheckBox->isChecked())
    {
        tag->addValue("REPLAYGAIN_TRACK_GAIN", gainToString(item->info[Qmmp::REPLAYGAIN_TRACK_GAIN]));
        tag->addValue("REPLAYGAIN_TRACK_PEAK", peakToString(item->info[Qmmp::REPLAYGAIN_TRACK_PEAK]));
    }
    if(m_ui->albumCheckBox->isChecked())
    {
        tag->addValue("REPLAYGAIN_ALBUM_GAIN", gainToString(item->info[Qmmp::REPLAYGAIN_ALBUM_GAIN]));
        tag->addValue("REPLAYGAIN_ALBUM_PEAK", peakToString(item->info[Qmmp::REPLAYGAIN_ALBUM_PEAK]));
    }
}

void RGScanDialog::writeID3v2Tag(TagLib::ID3v2::Tag *tag, ReplayGainInfoItem *item)
{
    tag->removeFrames("TXXX");
    if(m_ui->trackCheckBox->isChecked())
    {
        TagLib::ID3v2::UserTextIdentificationFrame *frame = new TagLib::ID3v2::UserTextIdentificationFrame();
        TagLib::StringList fields;
        fields.append("REPLAYGAIN_TRACK_GAIN");
        fields.append(gainToString(item->info[Qmmp::REPLAYGAIN_TRACK_GAIN]));
        frame->setText(fields);
        tag->addFrame(frame);

        fields.clear();
        frame = new TagLib::ID3v2::UserTextIdentificationFrame();
        fields.append("REPLAYGAIN_TRACK_PEAK");
        fields.append(peakToString(item->info[Qmmp::REPLAYGAIN_TRACK_PEAK]));
        frame->setText(fields);
        tag->addFrame(frame);
    }
    if(m_ui->albumCheckBox->isChecked())
    {
        TagLib::ID3v2::UserTextIdentificationFrame *frame = new TagLib::ID3v2::UserTextIdentificationFrame();
        TagLib::StringList fields;
        fields.append("REPLAYGAIN_ALBUM_GAIN");
        fields.append(gainToString(item->info[Qmmp::REPLAYGAIN_ALBUM_GAIN]));
        frame->setText(fields);
        tag->addFrame(frame);

        fields.clear();
        frame = new TagLib::ID3v2::UserTextIdentificationFrame();
        fields.append("REPLAYGAIN_ALBUM_PEAK");
        fields.append(peakToString(item->info[Qmmp::REPLAYGAIN_ALBUM_PEAK]));
        frame->setText(fields);
        tag->addFrame(frame);
    }
}

void RGScanDialog::writeVorbisComment(TagLib::Ogg::XiphComment *tag, ReplayGainInfoItem *item)
{
    if(m_ui->trackCheckBox->isChecked())
    {
        tag->addField("REPLAYGAIN_TRACK_GAIN", gainToString(item->info[Qmmp::REPLAYGAIN_TRACK_GAIN]));
        tag->addField("REPLAYGAIN_TRACK_PEAK", peakToString(item->info[Qmmp::REPLAYGAIN_TRACK_PEAK]));
    }
    if(m_ui->albumCheckBox->isChecked())
    {
        tag->addField("REPLAYGAIN_ALBUM_GAIN", gainToString(item->info[Qmmp::REPLAYGAIN_ALBUM_GAIN]));
        tag->addField("REPLAYGAIN_ALBUM_PEAK", peakToString(item->info[Qmmp::REPLAYGAIN_ALBUM_PEAK]));
    }
}

void RGScanDialog::writeMP4Tag(TagLib::MP4::Tag *tag, ReplayGainInfoItem *item)
{
    if(m_ui->trackCheckBox->isChecked())
    {
        tag->setItem("----:com.apple.iTunes:replaygain_track_gain",
                     gainToStringList(item->info[Qmmp::REPLAYGAIN_TRACK_GAIN]));
        tag->setItem("----:com.apple.iTunes:replaygain_track_peak",
                     gainToStringList(item->info[Qmmp::REPLAYGAIN_TRACK_PEAK]));
    }
    if(m_ui->albumCheckBox->isChecked())
    {
        tag->setItem("----:com.apple.iTunes:replaygain_album_gain",
                     gainToStringList(item->info[Qmmp::REPLAYGAIN_ALBUM_GAIN]));
        tag->setItem("----:com.apple.iTunes:replaygain_album_peak",
                     gainToStringList(item->info[Qmmp::REPLAYGAIN_ALBUM_PEAK]));
    }
}

void RGScanDialog::on_writeButton_clicked()
{
    if(m_replayGainItemList.isEmpty())
        return;

    qCDebug(plugin, "writing ReplayGain values...");

    for(ReplayGainInfoItem *item : std::as_const(m_replayGainItemList))
    {
        QString ext = item->url.section(QLatin1Char('.'), -1).toLower();

        if(ext == "mp3"_L1) //mpeg 1 layer 3
        {
            TagLib::MPEG::File file(qPrintable(item->url));
            writeAPETag(file.APETag(true), item);
            writeID3v2Tag(file.ID3v2Tag(true), item);
            file.save(TagLib::MPEG::File::APE | TagLib::MPEG::File::ID3v2, TagLib::File::StripNone);
        }
        else if(ext == "flac"_L1) //flac
        {
            TagLib::FLAC::File file(qPrintable(item->url));
            writeVorbisComment(file.xiphComment(true), item);
            file.save();
        }
        else if(ext == "oga"_L1) //ogg flac
        {
            TagLib::Ogg::FLAC::File file(qPrintable(item->url));
            writeVorbisComment(file.tag(), item);
            file.save();
        }
        else if(ext == "ogg"_L1) //ogg vorbis
        {
            TagLib::Ogg::Vorbis::File file(qPrintable(item->url));
            writeVorbisComment(file.tag(), item);
            file.save();
        }
        else if(ext == "wv"_L1) //wavpack
        {
            TagLib::WavPack::File file(qPrintable(item->url));
            writeAPETag(file.APETag(true), item);
            file.save();
        }
        else if(ext == "m4a"_L1) //MPEG-4 Part 14
        {
            TagLib::MP4::File file(qPrintable(item->url));
            writeMP4Tag(file.tag(), item);
            file.save();
        }
        else if(ext == "opus"_L1)
        {
            TagLib::Ogg::Opus::File file(qPrintable(item->url));
            writeVorbisComment(file.tag(), item);
            file.save();
        }
    }
}
