/*
 * Copyright (c) 2023-2026 Meltytech, LLC
 *
 * 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 3 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, see <http://www.gnu.org/licenses/>.
 */

#include "transcoder.h"

#include "jobqueue.h"
#include "jobs/ffmpegjob.h"
#include "mainwindow.h"
#include "settings.h"
#include "shotcut_mlt_properties.h"
#include "util.h"

#include <QFileDialog>
#include <QMessageBox>

static const auto kHandleSeconds = 15.0;

void Transcoder::setProducers(QList<Mlt::Producer> &producers)
{
    m_producers = producers;
}

void Transcoder::addProducer(Mlt::Producer &producer)
{
    m_producers.append(producer);
}

void Transcoder::addProducer(Mlt::Producer *producer)
{
    m_producers.append(*producer);
}

void Transcoder::convert(TranscodeDialog &dialog)
{
    int result = dialog.exec();
    if (dialog.isCheckBoxChecked()) {
        Settings.setShowConvertClipDialog(false);
    }
    if (result != QDialog::Accepted) {
        return;
    }

    QString path = Settings.savePath();
    QString suffix = dialog.isSubClip() ? tr("Sub-clip") + ' ' : tr("Converted");
    QString filename;
    QString nameFormat;
    QString nameFilter;

    switch (dialog.format()) {
    case 0:
        nameFormat = "/%1 - %2.mp4";
        nameFilter = tr("MP4 (*.mp4);;All Files (*)");
        break;
    case 1:
        nameFormat = "/%1 - %2.mov";
        nameFilter = tr("MOV (*.mov);;All Files (*)");
        break;
    case 2:
        nameFormat = "/%1 - %2.mkv";
        nameFilter = tr("MKV (*.mkv);;All Files (*)");
        break;
    }

    if (m_producers.length() == 1) {
        QString resource = Util::GetFilenameFromProducer(&m_producers[0]);
        QFileInfo fi(resource);
        filename = path + nameFormat.arg(fi.completeBaseName(), suffix);
        if (dialog.isSubClip()) {
            filename = Util::getNextFile(path);
        }
        filename = QFileDialog::getSaveFileName(MAIN.centralWidget(),
                                                dialog.windowTitle(),
                                                filename,
                                                nameFilter,
                                                nullptr,
                                                Util::getFileDialogOptions());
        if (!filename.isEmpty()) {
            if (filename == QDir::toNativeSeparators(resource)) {
                QMessageBox::warning(MAIN.centralWidget(),
                                     dialog.windowTitle(),
                                     QObject::tr("Unable to write file %1\n"
                                                 "Perhaps you do not have permission.\n"
                                                 "Try again with a different folder.")
                                         .arg(fi.fileName()));
                return;
            }
            if (JOBS.targetIsInProgress(filename)) {
                MAIN.showStatusMessage(tr("A job already exists for %1").arg(filename));
                return;
            }
            if (Util::warnIfNotWritable(filename, MAIN.centralWidget(), dialog.windowTitle()))
                return;

            if (Util::warnIfLowDiskSpace(filename)) {
                MAIN.showStatusMessage(tr("Convert canceled"));
                return;
            }
        }
        convertProducer(&m_producers[0], dialog, filename);
    } else if (m_producers.length() > 1) {
        path = QFileDialog::getExistingDirectory(MAIN.centralWidget(),
                                                 dialog.windowTitle(),
                                                 path,
                                                 Util::getFileDialogOptions());
        if (path.isEmpty()) {
            MAIN.showStatusMessage(tr("Convert canceled"));
            return;
        }
        if (Util::warnIfNotWritable(path, MAIN.centralWidget(), dialog.windowTitle())) {
            return;
        }
        if (Util::warnIfLowDiskSpace(path)) {
            MAIN.showStatusMessage(tr("Convert canceled"));
            return;
        }

        for (auto &producer : m_producers) {
            QString resource = Util::GetFilenameFromProducer(&producer);
            QFileInfo fi(resource);
            filename = path + nameFormat.arg(fi.completeBaseName(), suffix);
            filename = Util::getNextFile(filename);
            if (JOBS.targetIsInProgress(filename)) {
                if (JOBS.targetIsInProgress(filename)) {
                    MAIN.showStatusMessage(tr("A job already exists for %1").arg(filename));
                    return;
                }
            }
            convertProducer(&producer, dialog, filename);
        }
    }
    Settings.setSavePath(QFileInfo(filename).path());
}

void Transcoder::convertProducer(Mlt::Producer *producer, TranscodeDialog &dialog, QString filename)
{
    QString resource = Util::GetFilenameFromProducer(producer);
    QStringList args;
    int in = -1;
    const bool is10bit = Settings.isHdrCompatibleProcessingMode();

    args << "-loglevel"
         << "verbose";
    args << "-i" << resource;
    args << "-max_muxing_queue_size"
         << "9999";

    if (dialog.isSubClip()) {
        if (Settings.proxyEnabled()) {
            producer->Mlt::Properties::clear(kOriginalResourceProperty);
        }

        // set trim options
        if (producer->get(kFilterInProperty)) {
            in = producer->get_int(kFilterInProperty);
            int ss = qMax(0, in - qRound(producer->get_fps() * kHandleSeconds));
            auto s = QString::fromLatin1(producer->frames_to_time(ss, mlt_time_clock));
            args << "-ss" << s.replace(',', '.');
            in -= ss;
        } else {
            args << "-ss"
                 << QString::fromLatin1(producer->get_time("in", mlt_time_clock))
                        .replace(',', '.')
                        .replace(',', '.');
        }
        if (producer->get(kFilterOutProperty)) {
            int out = producer->get_int(kFilterOutProperty);
            int to = qMin(producer->get_playtime() - 1,
                          out + qRound(producer->get_fps() * kHandleSeconds));
            auto s = QString::fromLatin1(producer->frames_to_time(to, mlt_time_clock));
            args << "-to" << s.replace(',', '.');
        } else {
            args << "-to"
                 << QString::fromLatin1(producer->get_time("out", mlt_time_clock)).replace(',', '.');
        }
    }

    // transcode all streams except data, subtitles, and attachments
    auto audioIndex = producer->property_exists(kDefaultAudioIndexProperty)
                          ? producer->get_int(kDefaultAudioIndexProperty)
                          : producer->get_int("audio_index");
    if (producer->get_int("video_index") < audioIndex) {
        if (Util::hasiPhoneAmbisonic(producer))
            args << "-map"
                 << "0:V?"
                 << "-map"
                 << "0:a:0";
        else
            args << "-map"
                 << "0:V?"
                 << "-map"
                 << "0:a?";
    } else {
        args << "-map"
             << "0:a?"
             << "-map"
             << "0:V?";
    }
    args << "-map_metadata"
         << "0"
         << "-ignore_unknown";

    // Set Sample rate if different than source
    if (!dialog.sampleRate().isEmpty()) {
        args << "-ar" << dialog.sampleRate();
    }

    // Set video filters
    args << "-vf";
    QString filterString;
    if (dialog.deinterlace()) {
        QString deinterlaceFilter = QStringLiteral("bwdif,");
        filterString = filterString + deinterlaceFilter;
    }

    QString color_range;
    if (producer->get("color_range")) {
        if (producer->get_int("color_range") == 2) {
            color_range = "full";
        } else {
            color_range = "mpeg";
        }
    } else if (producer->get("force_full_range")) {
        if (producer->get_int("force_full_range")) {
            color_range = "full";
        } else {
            color_range = "mpeg";
        }
    } else {
        color_range = producer->get("meta.media.color_range");
    }
    if (color_range != "full" && color_range != "mpeg") {
        color_range = "mpeg";
    }

    if (dialog.get709Convert()) {
        QString convertFilter = QStringLiteral(
            "zscale=t=linear:npl=100,format=gbrpf32le,zscale=p=bt709,tonemap=tonemap=hable:desat=0,"
            "zscale=t=bt709:m=bt709:r=tv,format=yuv422p,");
        filterString = filterString + convertFilter;
    }
    filterString
        = filterString
          + QStringLiteral(
                "scale=flags=accurate_rnd+full_chroma_inp+full_chroma_int:in_range=%1:out_range=%2")
                .arg(color_range)
                .arg(color_range);
    auto fps = dialog.fpsOverride() ? dialog.fps() : Util::getSuggestedFrameRate(producer);
    auto fpsStr = QStringLiteral("%1").arg(fps, 0, 'f', 6);
    int numerator, denominator;
    Util::normalizeFrameRate(fps, numerator, denominator);
    if (denominator == 1001) {
        fpsStr = QStringLiteral("%1/%2").arg(numerator).arg(denominator);
    }
    if (dialog.fpsOverride()) {
        QString minterpFilter
            = QStringLiteral(
                  ",minterpolate='mi_mode=%1:mc_mode=aobmc:me_mode=bidir:vsbmc=1:fps=%2'")
                  .arg(dialog.frc(), fpsStr);
        filterString += minterpFilter;
    }
    if (is10bit) {
        // Intermediate (format 1) encodes to yuv422p10le (DNxHR HQX); preserve chroma.
        const auto pixFmt = dialog.format() == 1 ? QStringLiteral("yuv422p10le")
                                                 : QStringLiteral("yuv420p10le");
        filterString += QStringLiteral(",format=") + pixFmt;
    }
    args << filterString;
    args << "-fps_mode"
         << "cfr"
         << "-r" << fpsStr;

    // Specify color range
    if (color_range == "full") {
        args << "-color_range"
             << "2";
    } else {
        args << "-color_range"
             << "1";
    }

    int progressive = producer->get_int("meta.media.progressive")
                      || producer->get_int("force_progressive");
    if (!dialog.deinterlace() && !progressive) {
        int tff = producer->get_int("meta.media.top_field_first") || producer->get_int("force_tff");
        args << "-flags"
             << "+ildct+ilme"
             << "-top" << QString::number(tff);
    }

    switch (dialog.format()) {
    case 0:
        args << "-f"
             << "mp4"
             << "-codec:a"
             << "ac3"
             << "-b:a"
             << "512k"
             << "-codec:v"
             << "libx264";
        if (is10bit) {
            args << "-profile:v"
                 << "high10"
                 << "-pix_fmt"
                 << "yuv420p10le";
        }
        args << "-preset"
             << "medium"
             << "-g"
             << "1"
             << "-crf"
             << "15";
        break;
    case 1:
        args << "-f"
             << "mov"
             << "-codec:a"
             << "pcm_f32le";
        if (dialog.deinterlace() || progressive) {
            args << "-codec:v"
                 << "dnxhd"
                 << "-profile:v" << (is10bit ? "dnxhr_hqx" : "dnxhr_hq") << "-pix_fmt"
                 << (is10bit ? "yuv422p10le" : "yuv422p");
        } else { // interlaced
            args << "-codec:v"
                 << "prores_ks"
                 << "-profile:v"
                 << "standard";
        }
        break;
    case 2:
        args << "-f"
             << "matroska"
             << "-codec:a"
             << "pcm_f32le";
        if (is10bit) {
            args << "-codec:v"
                 << "libx264"
                 << "-profile:v"
                 << "high10"
                 << "-pix_fmt"
                 << "yuv420p10le"
                 << "-crf"
                 << "0";
        } else {
            args << "-codec:v"
                 << "utvideo"
                 << "-pix_fmt"
                 << "yuv422p";
        }
        break;
    }
    if (dialog.get709Convert()) {
        args << "-colorspace"
             << "bt709"
             << "-color_primaries"
             << "bt709"
             << "-color_trc"
             << "bt709";
    } else if (dialog.format() == 2 && !is10bit
               && producer->get_int("meta.media.colorspace") == 709) {
        // Work around a limitation that FFmpeg does not pass colorspace for utvideo
        args << "-colorspace"
             << "bt709";
    }

    args << "-y" << filename;
    producer->Mlt::Properties::clear(kOriginalResourceProperty);

    FfmpegJob *job = new FfmpegJob(filename, args, false);
    job->setLabel(tr("Convert %1").arg(Util::baseName(filename)));
    job->setTarget(filename);
    if (dialog.isSubClip()) {
        if (producer->get(kMultitrackItemProperty)) {
            QString s = QString::fromLatin1(producer->get(kMultitrackItemProperty));
            auto parts = s.split(':');
            if (parts.length() == 2) {
                int clipIndex = parts[0].toInt();
                int trackIndex = parts[1].toInt();
                QUuid uuid = MAIN.timelineClipUuid(trackIndex, clipIndex);
                if (!uuid.isNull()) {
                    job->setPostJobAction(
                        new ReplaceOnePostJobAction(resource, filename, QString(), uuid, in));
                    JOBS.add(job);
                }
            }
        } else {
            job->setPostJobAction(new OpenPostJobAction(resource, filename, QString()));
            JOBS.add(job);
        }
        return;
    }
    job->setPostJobAction(new ReplaceAllPostJobAction(resource, filename, Util::getHash(*producer)));
    JOBS.add(job);
}
