//    This file is part of Motion Watch.

//    Motion Watch 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.

//    Motion Watch 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.

#include "camera.h"

Camera::Camera(QObject *parent) : QObject(parent)
{
    shared.recordUrl.clear();
    shared.postCmd.clear();
    shared.camName.clear();

    shared.retCode    = 0;
    shared.imgThresh  = 10000;
    shared.maxEvents  = 100;
    shared.maxLogSize = 100000;
    shared.skipCmd    = false;
    shared.postSecs   = 60;
    shared.evMaxSecs  = 30;
    shared.webRoot    = "/var/www/html";
    shared.webBg      = "#485564";
    shared.webTxt     = "#dee5ee";
    shared.webFont    = "courier";
}

int Camera::start(const QStringList &args)
{
    shared.conf = getParam("-c", args);

    if (rdConf(&shared))
    {
        QDir("live").removeRecursively();
        QDir("img").removeRecursively();

        QDir().mkdir("live");
        QDir().mkdir("events");
        QDir().mkdir("logs");
        QDir().mkdir("img");

        auto thr1 = new QThread(nullptr);
        auto thr2 = new QThread(nullptr);
        auto thr3 = new QThread(nullptr);
        auto thr4 = new QThread(nullptr);

        new RecLoop(&shared, thr1, nullptr);
        new Upkeep(&shared, thr2, nullptr);
        new EventLoop(&shared, thr3, nullptr);
        new DetectLoop(&shared, thr4, nullptr);

        thr1->start();
        thr2->start();
        thr3->start();
        thr4->start();
    }

    return shared.retCode;
}

Loop::Loop(shared_t *sharedRes, QThread *thr, QObject *parent) : QObject(parent)
{
    shared    = sharedRes;
    heartBeat = 10;
    loopTimer = 0;

    connect(thr, &QThread::started, this, &Loop::init);

    moveToThread(thr);
}

void Loop::init()
{
    loopTimer = new QTimer(this);

    connect(loopTimer, &QTimer::timeout, this, &Loop::loopSlot);

    loopTimer->setSingleShot(false);
    loopTimer->start(heartBeat * 1000);

    loopSlot();
}

void Loop::loopSlot()
{
    if (!exec())
    {
        loopTimer->stop(); QCoreApplication::exit(shared->retCode);
    }
}

bool Loop::exec()
{
    if (loopTimer->interval() != heartBeat * 1000)
    {
        loopTimer->start(heartBeat * 1000);
    }

    return shared->retCode == 0;
}

RecLoop::RecLoop(shared_t *sharedRes, QThread *thr, QObject *parent) : Loop(sharedRes, thr, parent)
{
    recProc   = 0;
    imgProc   = 0;
    heartBeat = 1800;
}

void RecLoop::init()
{
    recProc = new QProcess(this);
    imgProc = new QProcess(this);

    connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, this, &RecLoop::term);
    connect(recProc, &QProcess::readyReadStandardError, this, &RecLoop::rdProcErr);
    connect(imgProc, &QProcess::readyReadStandardError, this, &RecLoop::rdProcErr);

    Loop::init();
}

void RecLoop::updateCmd()
{
    QStringList recArgs;
    QStringList imgArgs;

    recArgs << "-hide_banner";
    recArgs << "-i" << shared->recordUrl;
    recArgs << "-strftime" << "1";
    recArgs << "-strftime_mkdir" << "1";
    recArgs << "-hls_segment_filename" << "live/" + QString(STRFTIME_FMT) + ".ts";
    recArgs << "-y";
    recArgs << "-vcodec" << "copy";
    recArgs << "-f" << "hls";
    recArgs << "-hls_time" << "2";
    recArgs << "-hls_list_size" << "1000";
    recArgs << "-hls_flags" << "append_list";
    recArgs << "-rtsp_transport" << "tcp";
    recArgs << "-stimeout" << "3000";
    recArgs << "-t" << QString::number(heartBeat);
    recArgs << "stream.m3u8";

    imgArgs << "-hide_banner";
    imgArgs << "-i" << shared->recordUrl;
    imgArgs << "-strftime" << "1";
    imgArgs << "-strftime_mkdir" << "1";
    imgArgs << "-vf" << "fps=1,scale=320:240";
    imgArgs << "-rtsp_transport" << "tcp";
    imgArgs << "-stimeout" << "3000";
    imgArgs << "-t" << QString::number(heartBeat);
    imgArgs << "img/" + QString(STRFTIME_FMT) + ".bmp";

    recProc->setProgram("ffmpeg");
    recProc->setArguments(recArgs);

    imgProc->setProgram("ffmpeg");
    imgProc->setArguments(imgArgs);

    recLog("rec_args: " + recArgs.join(" "), shared);
    recLog("img_args: " + imgArgs.join(" "), shared);
}

void RecLoop::rdProcErr()
{
    procError("img", imgProc);
    procError("rec", recProc);
}

void RecLoop::term()
{
    recProc->kill();
    recProc->waitForFinished();

    imgProc->kill();
    imgProc->waitForFinished();
}

void RecLoop::procError(const QString &desc, QProcess *proc)
{
    if (proc->isOpen() && (proc->state() != QProcess::Running))
    {
        auto errBlob  = QString(proc->readAllStandardError());
        auto errLines = errBlob.split('\n');

        if (!errLines.isEmpty())
        {
            for (auto &&line : errLines)
            {
                recLog(desc + "_cmd_stderr: " + line, shared);
            }
        }
    }
}

bool RecLoop::exec()
{
    if ((imgProc->state() == QProcess::Running) || (recProc->state() == QProcess::Running))
    {
        term();
    }

    updateCmd();

    imgProc->start();
    recProc->start();

    return Loop::exec();
}

Upkeep::Upkeep(shared_t *sharedRes, QThread *thr, QObject *parent) : Loop(sharedRes, thr, parent) {}

bool Upkeep::exec()
{
    QDir().mkdir("live");
    QDir().mkdir("events");
    QDir().mkdir("logs");
    QDir().mkdir("img");

    enforceMaxLogSize(QString("logs/") + REC_LOG_NAME, shared);
    enforceMaxLogSize(QString("logs/") + DET_LOG_NAME, shared);

    dumpLogs(QString("logs/") + REC_LOG_NAME, shared->recLog);
    dumpLogs(QString("logs/") + DET_LOG_NAME, shared->detLog);

    shared->logMutex.lock();
    shared->recLog.clear();
    shared->detLog.clear();
    shared->logMutex.unlock();

    initLogFrontPages();
    enforceMaxEvents(shared);
    enforceMaxImages();
    enforceMaxVids();

    genHTMLul(".", shared->camName, shared);

    genCSS(shared);
    genHTMLul(shared->webRoot, QString(APP_NAME) + " " + QString(APP_VER), shared);

    return Loop::exec();
}

EventLoop::EventLoop(shared_t *sharedRes, QThread *thr, QObject *parent) : Loop(sharedRes, thr, parent)
{
    heartBeat = 2;
    highScore = 0;
    cycles    = 0;
}

bool EventLoop::exec()
{
    if (cycles * 2 >= shared->evMaxSecs)
    {
        vidList.removeDuplicates();

        if (vidList.size() > 1)
        {
            recLog("attempting write out of event: " + name, shared);

            if (wrOutVod())
            {
                genHTMLvod(name);

                QProcess    proc;
                QStringList args;

                args << "convert";
                args << imgPath;
                args << "events/" + name + ".jpg";

                proc.start("magick", args);
                proc.waitForFinished();
            }
        }

        cycles    = 0;
        highScore = 0;

        vidList.clear();
    }
    else
    {
        cycles += 1;

        shared->recMutex.lock();

        for (auto &&event : shared->recList)
        {
            auto maxFiles = shared->evMaxSecs / 2;
                // there's 2 secs in each hls segment

            if (highScore < event.score)
            {
                name      = event.timeStamp.toString(DATETIME_FMT);
                imgPath   = event.imgPath;
                highScore = event.score;
            }

            vidList.append(backwardFacingFiles("live", ".ts", event.timeStamp, maxFiles / 2));
            vidList.append(forwardFacingFiles("live", ".ts", event.timeStamp, maxFiles / 2));
        }

        shared->recList.clear();
        shared->recMutex.unlock();
    }

    return Loop::exec();
}

bool EventLoop::wrOutVod()
{
    auto cnt    = 0;
    auto concat = name + ".tmp";
    auto ret    = false;

    QFile file(concat);

    file.open(QFile::WriteOnly);

    for (auto &&vid : vidList)
    {
        recLog("event_src: " + vid, shared);

        if (QFile::exists(vid))
        {
            file.write(QString("file '" + vid + "'\n").toUtf8()); cnt++;
        }
    }

    file.close();

    if (cnt == 0)
    {
        recLog("err: none of the event hls clips exists, canceling write out.", shared);

        QFile::remove(concat);
    }
    else
    {
        QProcess    proc;
        QStringList args;

        args << "-f";
        args << "concat";
        args << "-safe" << "0";
        args << "-i" << concat;
        args << "-c" << "copy";
        args << "events/" + name + ".mp4";

        proc.setProgram("ffmpeg");
        proc.setArguments(args);
        proc.start();

        if (proc.waitForStarted())
        {
            recLog("concat_cmd_start: ok", shared);

            proc.waitForFinished(); ret = true;
        }
        else
        {
            recLog("concat_cmd_start: fail", shared);
            recLog("concat_cmd_stderr: " + QString(proc.readAllStandardError()), shared);
        }

        QFile::remove(concat);
    }

    return ret;
}

DetectLoop::DetectLoop(shared_t *sharedRes, QThread *thr, QObject *parent) : Loop(sharedRes, thr, parent)
{
    pcTimer     = 0;
    heartBeat   = 2;
    delayCycles = 8; // this will be used to delay the
                     // actual start of DetectLoop by
                     // 16secs.
}

void DetectLoop::init()
{
    pcTimer = new QTimer(this);
    mod     = false;

    connect(pcTimer, &QTimer::timeout, this, &DetectLoop::pcBreak);

    resetTimers();

    Loop::init();
}

void DetectLoop::resetTimers()
{
    pcTimer->start(shared->postSecs * 1000);
}

void DetectLoop::pcBreak()
{
    if (!shared->postCmd.isEmpty())
    {
        detLog("---POST_BREAK---", shared);

        if (mod)
        {
            detLog("motion detected, skipping the post command.", shared);
        }
        else
        {
            if (delayCycles == 0) delayCycles  = 5;
            else                  delayCycles += 5;

            detLog("no motion detected, running post command: " + shared->postCmd, shared);
            system(shared->postCmd.toUtf8().data());
        }
    }

    mod = false;
}

bool DetectLoop::exec()
{
    if (delayCycles > 0)
    {
        delayCycles -= 1;

        detLog("spec: detection cycle skipped. cycles left to be skipped: " + QString::number(delayCycles), shared);
    }
    else
    {
        auto curDT  = QDateTime::currentDateTime();
        auto images = backwardFacingFiles("img", ".bmp", curDT, 6);

        if (images.size() < 2)
        {
            detLog("wrn: didn't pick up enough image files from the image stream. number of files: " + QString::number(images.size()), shared);
            detLog("     will try again on the next loop.", shared);
        }
        else
        {
            QProcess    extComp;
            QStringList args;

            auto pos = images.size() - 1;

            args << "compare";
            args << "-metric" << "FUZZ";
            args << images[pos - 1];
            args << images[pos];
            args << "/dev/null";

            extComp.start("magick", args);
            extComp.waitForFinished();

            QString output = extComp.readAllStandardError();

            output = output.left(output.indexOf(' '));

            detLog(extComp.program() + " " + args.join(" ") +  " --result: " + output, shared);

            auto score = output.toFloat();

            if (score >= shared->imgThresh)
            {
                detLog("--threshold_breached: " + QString::number(shared->imgThresh), shared);

                evt_t event;

                event.timeStamp = curDT;
                event.score     = score;
                event.imgPath   = images[pos];

                shared->recMutex.lock();
                shared->recList.append(event); mod = true;
                shared->recMutex.unlock();
            }
        }
    }

    return Loop::exec();
}
