/*  QWinFF - a qt4 gui frontend for ffmpeg
 *  Copyright (C) 2011-2013 Timothy Lin <lzh9102@gmail.com>
 *
 *  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 "ffmpeginterface.h"
#include "mediaprobe.h"
#include "exepath.h"
#include <QRegExp>
#include <QTextStream>
#include <QDebug>
#include <QAtomicInt>
#include <cmath>

// timeout before force-terminating ffmpeg
#ifdef OPERATION_TIMEOUT
#   define TIMEOUT OPERATION_TIMEOUT
#else
#   define TIMEOUT 3000
#endif

namespace {
namespace patterns {
    const char progress[]
        = "size=\\s*([0-9]+)kB\\s+time=\\s*([0-9]+\\.[0-9]+)\\s+bitrate=\\s*([0-9]+\\.[0-9]+)kbits/s";
    enum Progress_1_Fields {
        PROG_1_TIME = 2
    };

    const char progress2[] // another possible format where time is represented as hh:mm:ss
        = "size=\\s*([0-9]+)kB\\s+time=\\s*([0-9][0-9]):([0-9][0-9]):([0-9][0-9](\\.[0-9][0-9]?)?)\\s+"
          "bitrate=\\s*([0-9]+\\.[0-9]+)kbits/s";
    enum Progress_2_Fields {
        PROG_2_HR = 2,
        PROG_2_MIN,
        PROG_2_SEC
    };

    const char duration[]
        = "Duration:\\s+([0-9][0-9]):([0-9][0-9]):([0-9][0-9](\\.[0-9][0-9]?)?)";
} // namespace patterns

namespace info {
    QAtomicInt is_encoders_read(false);
    volatile bool ffmpeg_exist = false;
    QString ffmpeg_version;
    QString ffmpeg_codec_info;
    QString ffmpeg_format_info;
    QList<QString> audio_encoders;
    QList<QString> video_encoders;
    QList<QString> subtitle_encoders;
    QList<QString> muxing_formats;
    QList<QString> demuxing_formats;

namespace inner {

    /**
     * @brief Extract encoder information from codec description.
     * @param target Extracted encoder names will be pushed into @a target
     * @param s the codec description string
     * @return number of encoders found
     */
    int find_encoders_in_desc(QStringList& target, const QString& s) {
        const char *keyword_begin = "(encoders:";
        const char *keyword_end = ")";
        int begin = s.indexOf(keyword_begin);
        if (begin < 0)
            return 0; // encoder name not found in description
        begin += strlen(keyword_begin);
        int end = s.indexOf(keyword_end, begin);
        if (end < 0)
            return 0; // error, mission ')'
        int length = end - begin;

        // encoder_str contains encoder names separated by spaces, and
        // may contain leading and trailing spaces.
        QString encoders_str = s.mid(begin, length);

        // split encoder_str into encoder names and skip whitespaces
        QStringList encoders = encoders_str.split(' ', QString::SkipEmptyParts);
        foreach (QString s, encoders) {
            target.push_back(s); // fill codec names into the list
        }

        return encoders.size();
    }

    bool read_ffmpeg_codecs(const char *flag)
    {
        QProcess ffmpeg_process;
        QStringList parameters;
        parameters.push_back(QString(flag));

        ffmpeg_process.setReadChannel(QProcess::StandardOutput);

        //qDebug() << ExePath::getPath("ffmpeg") << parameters.join(" ");
        ffmpeg_process.start(ExePath::getPath("ffmpeg"), parameters);

        // Wait until ffmpeg has started.
        if (!ffmpeg_process.waitForStarted(TIMEOUT)) {
            return false;
        }

        // Wait until ffmpeg has finished.
        if (!ffmpeg_process.waitForFinished(TIMEOUT)) {
            return false;
        }

        if (ffmpeg_process.exitCode() != 0) {
            return false; // error
        }

        // Find all available encoders
        QRegExp pattern("[ D]E([ VAS])...\\s+([^ ]+)\\s*(.*)$");
        QStringList encoder_list; // temporary storage of encoder names
        const int AV_INDEX = 1;
        const int CODEC_NAME_INDEX = 2;
        const int CODEC_DESC = 3;

        ffmpeg_codec_info.clear();
        while (ffmpeg_process.canReadLine()) {
            QString line(ffmpeg_process.readLine());
            ffmpeg_codec_info.append(line);

            if (pattern.indexIn(line) != -1) {
                QString av = pattern.cap(AV_INDEX);
                QString codec = pattern.cap(CODEC_NAME_INDEX);
                QString desc = pattern.cap(CODEC_DESC);

                // extract codec names
                encoder_list.clear();
                if (!find_encoders_in_desc(encoder_list, desc))
                    encoder_list.push_back(codec);

                foreach (QString codec_name, encoder_list) {
                    if (av == "A") { // audio encoder
                        //qDebug() << "Audio Codec: " + codec_name;
                        audio_encoders.push_back(codec_name);
                    } else if (av == "V") { // video encoder
                        //qDebug() << "Video Codec: " + codec_name;
                        video_encoders.push_back(codec_name);
                    } else if (av == "S") { // subtitle encoder
                        //qDebug() << "Subtitle Codec: " + codec_name;
                        subtitle_encoders.push_back(codec_name);
                    }
                }
            }
        }

        return true;
    }

    bool read_ffmpeg_version()
    {
        QProcess ffmpeg_process;
        QStringList parameters;
        parameters.push_back(QString("-version"));

        //qDebug() << ExePath::getPath("ffmpeg") << parameters.join(" ");
        ffmpeg_process.start(ExePath::getPath("ffmpeg"), parameters);

        ffmpeg_process.waitForStarted(TIMEOUT);
        ffmpeg_process.waitForFinished(TIMEOUT);
        ffmpeg_version = QString(ffmpeg_process.readAll());

        return true;
    }

    bool read_ffmpeg_formats()
    {
        QProcess ffmpeg_process;
        QStringList parameters;
        parameters << "-formats";

        ffmpeg_process.start(ExePath::getPath("ffmpeg"), parameters);

        ffmpeg_process.waitForStarted(TIMEOUT);
        ffmpeg_process.waitForFinished(TIMEOUT);
        if (ffmpeg_process.exitCode() != 0)
            return false;

        muxing_formats.clear();
        demuxing_formats.clear();
        ffmpeg_format_info.clear();

        QRegExp pattern("^ ([ D])([ E]) ([^ ]+)\\s+(.*)$");
        const int INDEX_DEMUX = 1;
        const int INDEX_MUX = 2;
        const int INDEX_NAME = 3;
        //const int INDEX_DETAIL = 4;

        while (ffmpeg_process.canReadLine()) {
            QString line(ffmpeg_process.readLine());
            ffmpeg_format_info.append(line);
            if (pattern.indexIn(line) != -1) {
                QString name = pattern.cap(INDEX_NAME);
                if (pattern.cap(INDEX_DEMUX) == "D")
                    demuxing_formats.append(name);
                if (pattern.cap(INDEX_MUX) == "E")
                    muxing_formats.append(name);
            }
        }

        return true;
    }

} // namespace inner

    /* Read FFmpeg information.
     *  (1) Check available encoder from "ffmpeg -codecs" and "ffmpeg -formats".
     *  (2) Read ffmpeg version information by "ffmpeg -version" command.
     * Once the information is correctly read, it never
     * execute ffmpeg to acquire the information again.
    */
    void read_ffmpeg_info()
    {
        if (!is_encoders_read.testAndSetAcquire(false, true))
            return; // Skip the operation if the information was already read.

        qDebug() << "Read FFmpeg Information";

        /* Older versions of ffmpeg has no "-codecs" flag and report all
         * supported codecs by "-formats". Recent versions report codecs
         * by "-codecs" flag, so we check "-codecs" first. If ffmpeg
         * returns an error, retry with flag "-formats".
         */
        if (!inner::read_ffmpeg_codecs("-codecs") && !inner::read_ffmpeg_codecs("-formats")) {
            is_encoders_read = false; // allow retry when failed
            return;
        }

        if (!inner::read_ffmpeg_version())
            return;

        if (!inner::read_ffmpeg_formats())
            return;

        ffmpeg_exist = true;
    }

} // namespace info

    // extract error message from the line
    QString extract_errmsg(const QString& line)
    {
        QRegExp pattern("^[^:]*:(.*)$");
        const int INDEX_MESSAGE = 1;
        if (pattern.indexIn(line) != -1)
            return pattern.cap(INDEX_MESSAGE).trimmed();
        else
            return "";
    }

    // scale sec with speed_factor if scale == true
    double scale_time(unsigned int sec, bool scale, double speed_factor)
    {
        if (!sec)
            return 0;
        else if (scale)
            return sec / speed_factor; // (speed *= 2) means (time /= 2)
        else
            return sec;
    }

} // anonymous namespace

struct FFmpegInterface::Private
{
    double duration;
    double progress;
    QString stringBuffer;
    QRegExp progress_pattern;
    QRegExp progress_pattern_2;
    QRegExp duration_pattern;

    bool encoders_read;
    QList<QString> audio_encoders;
    QList<QString> video_encoders;
    QList<QString> subtitle_encoders;

    QString errmsg;

    Private() : duration(0), progress(0)
      , progress_pattern(patterns::progress)
      , progress_pattern_2(patterns::progress2)
      , duration_pattern(patterns::duration)
      , encoders_read(false) { }

    bool check_progress(const QString&);
    QStringList getOptionList(const ConversionParameters&, bool*, bool*);
};

/*! Check whether the output line is a progress line.
    If it is, update p->progress and return true.
    Otherwise, keep the value of p->progress and return false.
*/
bool FFmpegInterface::Private::check_progress(const QString& line)
{
    QRegExp& pattern = progress_pattern;
    int index = pattern.indexIn(line);
    if (index != -1) {
        const double t = pattern.cap(patterns::PROG_1_TIME).toDouble();

        // calculate progress
        progress = (t / duration) * 100;

        return true;
    } else { // try another pattern
        QRegExp& alternate_pattern = progress_pattern_2;
        if (alternate_pattern.indexIn(line) != -1) {
            const int hour = alternate_pattern.cap(patterns::PROG_2_HR).toInt();
            const int min = alternate_pattern.cap(patterns::PROG_2_MIN).toInt();
            const double sec = alternate_pattern.cap(patterns::PROG_2_SEC).toDouble();
            const double t = hour*3600 + min*60 + sec;

            progress = (t / duration) * 100;

            return true;
        }
    }
    errmsg = line; // save the last output line
    return false;
}

/**
 * Convert a ConversionParameters object into ffmpeg command-line option list.
 * @param o the parameter object
 * @return a QStringList containing command-line options
 * @note This function is time-consuming because it calls ffprobe to obtain
 *       media information.
 */
QStringList FFmpegInterface::Private::getOptionList(const ConversionParameters &o
                                                    , bool *needs_audio_filter
                                                    , bool *success)
{
    MediaProbe probe;
    bool bNeedsAudioFilter;

    if (!probe.run(o.source, TIMEOUT)) {
        if (success)
            *success = false;
        return QStringList();
    }

    bNeedsAudioFilter = o.speed_scaling && !o.disable_audio && probe.hasAudio();

    QStringList list;

    // overwrite if file exists
    list.append("-y");

    if (!bNeedsAudioFilter) {
        /* in this configuration, input is read from file
           arguments: -i <infile>
        */
        list << "-i" << o.source;
    } else {
        /* In this configuration, video (if any) is read from file
           and audio is read from stdin
           if source file contains video:
              arguments: -i <infile> -i - -map 0:<vstreamindex> -map 1
           if source file has no video stream:
              arguments: -i -
        */
        if (probe.hasVideo() && !o.disable_video)
            list << "-i" << o.source << "-i" << "-"
                 << "-map" << QString("0:%1").arg(probe.videoStreamIndex())
                 << "-map" << "1";
        else
            list << "-i" << "-";
    }

    // enable experimental codecs by default
    list << "-strict" << "experimental";

    /* ==== Additional Options ==== */
    if (!o.ffmpeg_options.isEmpty()) {
        QList<QString> additional_options =
                o.ffmpeg_options.split(" ", QString::SkipEmptyParts);
        foreach (QString opt, additional_options)
            list.append(opt);
    }

    if (o.threads >= 2) {
        list.append("-threads");
        list.append(QString::number(o.threads));
    }

    /* ==== Audio/Video Options ==== */

    // Audio Options
    if (o.disable_audio) {
        list.append("-an"); // no audio
    } else if (o.copy_audio) { // copy audio data (no re-encode)
        list.append("-acodec");
        list.append("copy");
    } else { // audio enabled

        // audio bitrate in kb/s
        if (o.audio_bitrate > 0) {
            list.append("-ab");
            list.append(QString("%1k").arg(o.audio_bitrate));
        }

        // audio sample rate in hz
        if (o.audio_sample_rate > 0) {
            list.append("-ar");

            int sample_rate = o.audio_sample_rate;
            if (o.audio_keep_sample_rate
                    && !probe.error()
                    && probe.audioSampleRate() != 0) {
                sample_rate = probe.audioSampleRate();
                qDebug() << "Apply probed sample rate: " + QString::number(sample_rate);
            }

            list.append(QString("%1").arg(sample_rate));
        }

        // audio channels
        if (o.audio_channels > 0) {
            list.append("-ac");
            list.append(QString("%1").arg(o.audio_channels));
        }

        // volume
        // 256 is normal volume
        if (o.audio_volume > 0 && o.audio_volume != 256) {
            list.append("-vol");
            list.append(QString("%1").arg(o.audio_volume));
        }

    }

    // Video Options
    if (o.disable_video || !probe.hasVideo()) {
        list.append("-vn"); // no video
    } else if (o.copy_video) { // copy video data (no re-encode)
        list.append("-vcodec");
        list.append("copy");
    } else { // video enabled

        // same video quality as source
        if (o.video_same_quality) {
            list.append("-sameq");
        }

        // deinterlace
        if (o.video_deinterlace) {
            list.append("-deinterlace");
        }

        // video bitrate
        if (o.video_bitrate > 0) {
            list.append("-b");
            list.append(QString("%1k").arg(o.video_bitrate));
        }

        // video dimensions
        if (o.video_width > 0 && o.video_height > 0) {
            list.append("-s");
            list.append(QString("%1x%2").arg(o.video_width).arg(o.video_height));
        }

        if (o.video_crop_top > 0) {
            list.append("-croptop");
            list.append(QString("%1").arg(o.video_crop_top));
        }

        if (o.video_crop_bottom > 0) {
            list.append("-cropbottom");
            list.append(QString("%1").arg(o.video_crop_bottom));
        }

        if (o.video_crop_left > 0) {
            list.append("-cropleft");
            list.append(QString("%1").arg(o.video_crop_left));
        }

        if (o.video_crop_right > 0) {
            list.append("-cropright");
            list.append(QString("%1").arg(o.video_crop_right));
        }

        /* -vf "setpts=<1/rate>*PTS": video filter to change video speed
            <1/rate> is the reciprocal of the scaling factor (1.0 is original speed) */
        if (o.speed_scaling)
            list << "-vf" << QString("setpts=%1*PTS").arg(1/o.speed_scaling_factor);
    }

    // Time Options

    /* Scale begin time and end time if speed_scaling is on.
     * scaled_time_begin and scaled_time_end are doubles, so never compare time
     * (ex. time == 0) by them. Compare using o.time_begin and o.time_end instead.
     */
    double scaled_time_begin = scale_time(o.time_begin, o.speed_scaling, o.speed_scaling_factor);
    double scaled_time_end = scale_time(o.time_end, o.speed_scaling, o.speed_scaling_factor);

    /* -ss time_begin
        When used as an output option, ffmpeg decodes but discards input
        until timestamp reaches time_begin */
    if (o.time_begin > 0) {
        list.append("-ss");
        list.append(QString("%1").arg(scaled_time_begin));
    }
    /* -t time_duration
        Stop writing the output after its duration reaches time_duration */
    if (o.time_end > 0) {
        Q_ASSERT(o.time_end >= o.time_begin);
        double scaled_duration = scaled_time_end - scaled_time_begin;
        list.append("-t");
        list.append(QString("%1").arg(scaled_duration));
    }

    // destination file
    list.append(o.destination);

    // record duration
    duration = probe.mediaDuration();
    if (o.speed_scaling)
        duration /= o.speed_scaling_factor;

    if (needs_audio_filter)
        *needs_audio_filter = bNeedsAudioFilter;
    if (success)
        *success = true;
    return list;
}

FFmpegInterface::FFmpegInterface(QObject *parent) :
    ConverterInterface(parent), p(new Private)
{
}

FFmpegInterface::~FFmpegInterface()
{
    delete p;
}

// virtual functions
QString FFmpegInterface::executableName() const
{
    return ExePath::getPath("ffmpeg");
}

void FFmpegInterface::reset()
{
    p->duration = 0;
    p->progress = 0;
    p->stringBuffer.clear();
    p->errmsg.clear();
}

QProcess::ProcessChannel FFmpegInterface::processReadChannel() const
{
    return QProcess::StandardError;
}

bool FFmpegInterface::needsAudioFiltering(const ConversionParameters& param) const
{
    return !param.disable_audio && param.speed_scaling;
}

void FFmpegInterface::fillParameterList(const ConversionParameters &param, QStringList &list
                                        , bool *needs_audio_filter)
{
    bool success; // TODO: return success
    list = p->getOptionList(param, needs_audio_filter, &success);
}

void FFmpegInterface::parseProcessOutput(const QString &data)
{
    //qDebug() << data;

    // split incoming data by [end of line] or [carriage return]
    QStringList lines(data.split(QRegExp("[\r\n]"), QString::KeepEmptyParts));

    if (!p->stringBuffer.isEmpty()) { // prepend buffered data
        lines.front().prepend(p->stringBuffer);
        p->stringBuffer.clear();
    }

    if (!lines.back().isEmpty()) { // buffer incomplete data
        p->stringBuffer = lines.back();
        lines.back().clear();
    }

    QStringList::iterator it = lines.begin();

    for (; it!=lines.end(); ++it) { // parse lines
        QString& line = *it;
        if (line.isEmpty()) continue;
        if (p->check_progress(line)) {
            emit progressRefreshed(p->progress);
            continue;
        }
    }
}

double FFmpegInterface::progress() const
{
    return p->progress;
}

QString FFmpegInterface::errorMessage() const
{
    return extract_errmsg(p->errmsg);
}

bool FFmpegInterface::getAudioEncoders(QList<QString> &target)
{
    info::read_ffmpeg_info();
    if (!info::ffmpeg_exist) return false;

    target = info::audio_encoders;
    return true;
}

bool FFmpegInterface::getAudioEncoders(QSet<QString> &target)
{
    QList<QString> encoder_list;
    if (!getAudioEncoders(encoder_list))
        return false;
    target = QSet<QString>::fromList(encoder_list);
    return true;
}

bool FFmpegInterface::getVideoEncoders(QList<QString> &target)
{
    info::read_ffmpeg_info();
    if (!info::ffmpeg_exist) return false;

    target = info::video_encoders;
    return true;
}

bool FFmpegInterface::getVideoEncoders(QSet<QString> &target)
{
    QList<QString> encoder_list;
    if (!getVideoEncoders(encoder_list))
        return false;
    target = QSet<QString>::fromList(encoder_list);
    return true;
}

bool FFmpegInterface::getSubtitleEncoders(QList<QString> &target)
{
    info::read_ffmpeg_info();
    if (!info::ffmpeg_exist) return false;

    target = info::subtitle_encoders;
    return true;
}

QString FFmpegInterface::getFFmpegVersionInfo()
{
    info::read_ffmpeg_info();
    return info::ffmpeg_version;
}

QString FFmpegInterface::getFFmpegCodecInfo()
{
    info::read_ffmpeg_info();
    return info::ffmpeg_codec_info;
}

QString FFmpegInterface::getFFmpegFormatInfo()
{
    info::read_ffmpeg_info();
    return info::ffmpeg_format_info;
}

bool FFmpegInterface::getSupportedMuxingFormats(QSet<QString> &target)
{
    info::read_ffmpeg_info();
    if (!info::ffmpeg_exist) return false;

    target = QSet<QString>::fromList(info::muxing_formats);
    return true;
}

bool FFmpegInterface::getSupportedDemuxingFormats(QSet<QString> &target)
{
    info::read_ffmpeg_info();
    if (!info::ffmpeg_exist) return false;

    target = QSet<QString>::fromList(info::demuxing_formats);
    return true;
}

bool FFmpegInterface::hasFFmpeg()
{
    info::read_ffmpeg_info();
    return info::ffmpeg_exist;
}

void FFmpegInterface::refreshFFmpegInformation()
{
    info::is_encoders_read = false;
    info::read_ffmpeg_info();
}

bool FFmpegInterface::getSubtitleEncoders(QSet<QString> &target)
{
    QList<QString> encoder_list;
    if (!getSubtitleEncoders(encoder_list))
        return false;
    target = QSet<QString>::fromList(encoder_list);
    return true;
}
