/***************************************************************************
                          recording.cpp  -  description
                             -------------------
    begin                : Mi Aug 27 2003
    copyright            : (C) 2003 by Martin Witte
    email                : witte@kawo1.rwth-aachen.de
 ***************************************************************************/

/***************************************************************************
 *                                                                         *
 *   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.                                   *
 *                                                                         *
 ***************************************************************************/

#include "../../src/include/radiostation.h"
#include "../../src/include/errorlog-interfaces.h"
#include "../../src/include/aboutwidget.h"
#include "../../src/include/fileringbuffer.h"
#include "../../src/include/utils.h"

#include "recording.h"
#include "recording-configuration.h"
#include "soundstreamevent.h"
#include "recording-monitor.h"
#include "encoder_mp3.h"
#include "encoder_ogg.h"
#include "encoder_pcm.h"

#include <tqevent.h>
#include <tqapplication.h>
#include <tqregexp.h>

#include <tdeconfig.h>
#include <tdeversion.h>

#include <tdeaboutdata.h>


///////////////////////////////////////////////////////////////////////
//// plugin library functions

PLUGIN_LIBRARY_FUNCTIONS2(
    Recording,        "tderadio-recording", i18n("TDERadio Recording Plugin"),
    RecordingMonitor,                     i18n("TDERadio Recording Monitor")
);

///////////////////////////////////////////////////////////////////////

Recording::Recording(const TQString &name)
    : TQObject(NULL, NULL),
      PluginBase(name, i18n("TDERadio Recording Plugin")),
      m_config()
{
}


Recording::~Recording()
{
    TQMapIterator<SoundStreamID, RecordingEncoding*> it = m_EncodingThreads.begin();
    TQMapIterator<SoundStreamID, RecordingEncoding*> end = m_EncodingThreads.end();
    for (; it != end; ++it) {
        sendStopRecording(it.key());
    }
}


bool Recording::connectI(Interface *i)
{
    bool a = IRecCfg::connectI(i);
    bool b = PluginBase::connectI(i);
    bool c = ISoundStreamClient::connectI(i);
    return a || b || c;
}


bool Recording::disconnectI(Interface *i)
{
    bool a = IRecCfg::disconnectI(i);
    bool b = PluginBase::disconnectI(i);
    bool c = ISoundStreamClient::disconnectI(i);
    return a || b || c;
}


void Recording::noticeConnectedI (ISoundStreamServer *s, bool pointer_valid)
{
    ISoundStreamClient::noticeConnectedI(s, pointer_valid);
    if (s && pointer_valid) {
        s->register4_sendStartPlayback(this);
        s->register4_sendStopPlayback(this);
        s->register4_sendStartRecording(this);
        s->register4_sendStartRecordingWithFormat(this);
        s->register4_notifySoundStreamData(this);
        s->register4_sendStopRecording(this);
        s->register4_queryIsRecordingRunning(this);
        s->register4_querySoundStreamDescription(this);
        s->register4_querySoundStreamRadioStation(this);
        s->register4_queryEnumerateSoundStreams(this);
        s->register4_notifySoundStreamChanged(this);
        s->register4_notifySoundStreamClosed(this);
    }
}

// PluginBase

void Recording::saveState (TDEConfig *c) const
{
    c->setGroup(TQString("recording-") + PluginBase::name());
    m_config.saveConfig(c);
}


void Recording::restoreState (TDEConfig *c)
{
    c->setGroup(TQString("recording-") + PluginBase::name());
    RecordingConfig cfg;
    cfg.restoreConfig(c);
    setRecordingConfig(cfg);
    //notifyRecordingConfigChanged(m_config);
}


ConfigPageInfo  Recording::createConfigurationPage()
{
    RecordingConfiguration *c = new RecordingConfiguration(NULL);
    connectI(c);
    return ConfigPageInfo(c,
                          i18n("Recording"),
                          i18n("Recording"),
                          "tderadio_record");
}


AboutPageInfo Recording::createAboutPage()
{
/*    TDEAboutData aboutData("tderadio",
                         NULL,
                         NULL,
                         I18N_NOOP("Recording Monitor for TDERadio"),
                         TDEAboutData::License_GPL,
                         "(c) 2002-2005 Martin Witte",
                         0,
                         "http://sourceforge.net/projects/tderadio",
                         0);
    aboutData.addAuthor("Martin Witte",  "", "witte@kawo1.rwth-aachen.de");

    return AboutPageInfo(
              new TDERadioAboutWidget(aboutData, TDERadioAboutWidget::AbtTabbed),
              i18n("Recording"),
              i18n("Recording Plugin"),
              "tderadio_record"
           );*/
    return AboutPageInfo();
}


// IRecCfg

bool   Recording::setEncoderBuffer     (size_t BufferSize, size_t BufferCount)
{
    if (m_config.m_EncodeBufferSize  != BufferSize ||
        m_config.m_EncodeBufferCount != BufferCount)
    {
        m_config.m_EncodeBufferSize  = BufferSize;
        m_config.m_EncodeBufferCount = BufferCount;
        notifyEncoderBufferChanged(BufferSize, BufferCount);
    }
    return true;
}

bool   Recording::setSoundFormat       (const SoundFormat &sf)
{
    if (m_config.m_SoundFormat != sf) {
        m_config.m_SoundFormat = sf;
        notifySoundFormatChanged(sf);
    }
    return true;
}

bool   Recording::setMP3Quality        (int q)
{
    if (m_config.m_mp3Quality != q) {
        m_config.m_mp3Quality = q;
        notifyMP3QualityChanged(q);
    }
    return true;
}

bool   Recording::setOggQuality        (float q)
{
    if (m_config.m_oggQuality != q) {
        m_config.m_oggQuality = q;
        notifyOggQualityChanged(q);
    }
    return true;
}

bool   Recording::setRecordingDirectory(const TQString &dir)
{
    if (m_config.m_Directory != dir) {
        m_config.m_Directory = dir;
        notifyRecordingDirectoryChanged(dir);
    }
    return true;
}

bool   Recording::setOutputFormat (RecordingConfig::OutputFormat of)
{
    if (m_config.m_OutputFormat != of) {
        m_config.m_OutputFormat = of;
        notifyOutputFormatChanged(of);
    }
    return true;
}

bool   Recording::setPreRecording (bool enable, int seconds)
{
    if (m_config.m_PreRecordingEnable != enable || m_config.m_PreRecordingSeconds != seconds) {
        m_config.m_PreRecordingEnable = enable;
        m_config.m_PreRecordingSeconds = seconds;

        if (enable) {
            for (TQMapIterator<SoundStreamID,FileRingBuffer*> it = m_PreRecordingBuffers.begin(); it != m_PreRecordingBuffers.end(); ++it) {
                if (*it != NULL) {
                    delete *it;
                }
                *it = new FileRingBuffer(m_config.m_Directory + "/tderadio-prerecord-"+TQString::number(it.key().getID()), m_config.m_PreRecordingSeconds * m_config.m_SoundFormat.m_SampleRate * m_config.m_SoundFormat.frameSize());
                SoundFormat sf = m_config.m_SoundFormat;
                sendStartCaptureWithFormat(it.key(), sf, sf, false);
            }
        }
        else {
            for (TQMapIterator<SoundStreamID,FileRingBuffer*> it = m_PreRecordingBuffers.begin(); it != m_PreRecordingBuffers.end(); ++it) {
                if (*it != NULL) {
                    sendStopCapture(it.key());
                    delete *it;
                }
            }
            m_PreRecordingBuffers.clear();
        }

        notifyPreRecordingChanged(enable, seconds);
    }
    return true;
}

void   Recording::getEncoderBuffer(size_t &BufferSize, size_t &BufferCount) const
{
    BufferSize  = m_config.m_EncodeBufferSize;
    BufferCount = m_config.m_EncodeBufferCount;
}

const SoundFormat &Recording::getSoundFormat () const
{
    return m_config.m_SoundFormat;
}

int Recording::getMP3Quality () const
{
    return m_config.m_mp3Quality;
}

float Recording::getOggQuality () const
{
    return m_config.m_oggQuality;
}

const TQString &Recording::getRecordingDirectory() const
{
    return m_config.m_Directory;
}

RecordingConfig::OutputFormat Recording::getOutputFormat() const
{
    return m_config.m_OutputFormat;
}

bool Recording::getPreRecording(int &seconds) const
{
    seconds = m_config.m_PreRecordingSeconds;
    return m_config.m_PreRecordingEnable;
}

const RecordingConfig &Recording::getRecordingConfig() const
{
    return m_config;
}

bool Recording::setRecordingConfig(const RecordingConfig &c)
{
    setEncoderBuffer     (c.m_EncodeBufferSize, c.m_EncodeBufferCount);
    setSoundFormat       (c.m_SoundFormat);
    setMP3Quality        (c.m_mp3Quality);
    setOggQuality        (c.m_oggQuality);
    setRecordingDirectory(c.m_Directory);
    setOutputFormat      (c.m_OutputFormat);
    setPreRecording      (c.m_PreRecordingEnable, c.m_PreRecordingSeconds);

    m_config = c;

    notifyRecordingConfigChanged(m_config);

    return true;
}


// ISoundStreamClient
bool Recording::startPlayback(SoundStreamID id)
{
    if (m_PreRecordingBuffers.contains(id))
        delete m_PreRecordingBuffers[id];
    m_PreRecordingBuffers[id] = NULL;
    if (m_config.m_PreRecordingEnable) {
        m_PreRecordingBuffers[id] = new FileRingBuffer(m_config.m_Directory + "/tderadio-prerecord-"+TQString::number(id.getID()), m_config.m_PreRecordingSeconds * m_config.m_SoundFormat.m_SampleRate * m_config.m_SoundFormat.frameSize());
        SoundFormat sf = m_config.m_SoundFormat;
        sendStartCaptureWithFormat(id, sf, sf, false);
    }
    return false;
}

bool Recording::stopPlayback(SoundStreamID id)
{
    if (m_PreRecordingBuffers.contains(id)) {
        if (m_PreRecordingBuffers[id])
            delete m_PreRecordingBuffers[id];
        m_PreRecordingBuffers.remove(id);
        sendStopCapture(id);
    }
    return false;
}

bool Recording::startRecording(SoundStreamID id)
{

/*    FileRingBuffer *test = new FileRingBuffer("/tmp/ringbuffertest", 2048);
    char buffer1[1024];
    char buffer2[1024];
    char buffer3[1024];
    for (int i = 0; i < 1024; ++i) {
        buffer1[i] = 'a';
        buffer2[i] = 'b';
        buffer3[i] = 'c';
    }
    test->addData(buffer1, 1024);
    test->addData(buffer2, 1024);
    test->removeData(1024);
    test->addData(buffer3, 1024);
*/

    SoundFormat realFormat = m_config.m_SoundFormat;
    return sendStartRecordingWithFormat(id, realFormat, realFormat);
}

bool Recording::startRecordingWithFormat(SoundStreamID id, const SoundFormat &sf, SoundFormat &real_format)
{
    if (!sendStartCaptureWithFormat(id, sf, real_format, /* force_format = */ true)) {
        logError(i18n("start capture not handled"));
        return false;
    }

    RecordingConfig cfg = m_config;
    cfg.m_SoundFormat   = real_format;

    logInfo(i18n("Recording starting"));
    if (!startEncoder(id, cfg)) {
        logError(i18n("starting encoding thread failed"));
        sendStopCapture(id);
        return false;
    }

    return true;
}


bool Recording::stopRecording(SoundStreamID id)
{
    if (m_EncodingThreads.contains(id)) {
        sendStopCapture(id);
        if (m_config.m_PreRecordingEnable) {
            if (!m_PreRecordingBuffers.contains(id)) {
                if (m_PreRecordingBuffers[id] != NULL) {
                    delete m_PreRecordingBuffers[id];
                }
                bool b = false;
                queryIsPlaybackRunning(id, b);
                if (b) {
                    m_PreRecordingBuffers[id] = new FileRingBuffer(m_config.m_Directory + "/tderadio-prerecord-"+TQString::number(id.getID()), m_config.m_PreRecordingSeconds * m_config.m_SoundFormat.m_SampleRate * m_config.m_SoundFormat.frameSize());
                } else {
                    m_PreRecordingBuffers[id] = NULL;
                }
            }
        }
        stopEncoder(id);
        return true;
    }
    return false;
}



bool Recording::noticeSoundStreamData(SoundStreamID id,
    const SoundFormat &/*sf*/, const char *data, size_t size, size_t &consumed_size,
    const SoundMetaData &md
)
{
    if (m_PreRecordingBuffers.contains(id) && m_PreRecordingBuffers[id] != NULL) {

        FileRingBuffer &fbuf = *m_PreRecordingBuffers[id];
        if (fbuf.getFreeSize() < size) {
            fbuf.removeData(size - fbuf.getFreeSize());
        }
        size_t n = fbuf.addData(data, size);
        consumed_size = (consumed_size == SIZE_T_DONT_CARE) ? n : min(consumed_size, n);
//         if (n != size) {
//             logDebug("recording packet: was not written completely to tmp buf");
//         }

//         //BEGIN DEBUG
//         char tmp[4096];
//         for (unsigned int i = 0; i < sizeof(tmp); ++i) { tmp[i] = 0; }
//         if (fbuf.getFreeSize() < sizeof(tmp)) {
//             fbuf.removeData(sizeof(tmp) - fbuf.getFreeSize());
//         }
//         fbuf.addData((char*)tmp, sizeof(tmp));
//         //END DEBUG

        if (m_EncodingThreads.contains(id)) {

            //logDebug("recording packet: " + TQString::number(size));

            RecordingEncoding *thread = m_EncodingThreads[id];

            //logDebug("noticeSoundStreamData thread = " + TQString::number((long long)thread, 16));

            size_t        remSize = fbuf.getFillSize();

            while (remSize > 0) {
                size_t  bufferSize = remSize;
                char *buf = thread->lockInputBuffer(bufferSize);
                if (!buf) {
                    // Encoder buffer is full and bigger than remaining data
                    break;
                }
                if (bufferSize > remSize) {
                    bufferSize = remSize;
                }
                if (fbuf.takeData(buf, bufferSize) != bufferSize) {
                    logError(i18n("could not read suffient data"));
                }

                thread->unlockInputBuffer(bufferSize, md);
                remSize -= bufferSize;
            }

            if (remSize == 0) {
                delete m_PreRecordingBuffers[id];
                m_PreRecordingBuffers.remove(id);
            }
        }

        return true;
    }

    else if (m_EncodingThreads.contains(id)) {

        //logDebug("recording packet: " + TQString::number(size));

        RecordingEncoding *thread = m_EncodingThreads[id];

        //logDebug("noticeSoundStreamData thread = " + TQString::number((long long)thread, 16));

        size_t        remSize = size;
        const char   *remData = data;

        while (remSize > 0) {
            size_t  bufferSize = remSize;
            char *buf = thread->lockInputBuffer(bufferSize);
            if (!buf) {
                logWarning(i18n("Encoder input buffer overflow (buffer configuration problem?). Skipped %1 input bytes").arg(TQString::number(remSize)));
                break;
            }
            if (bufferSize > remSize) {
                bufferSize = remSize;
            }
            memcpy(buf, remData, bufferSize);

            thread->unlockInputBuffer(bufferSize, md);
            remSize -= bufferSize;
            remData += bufferSize;
        }
        consumed_size = (consumed_size == SIZE_T_DONT_CARE) ? size - remSize : min(consumed_size, size - remSize);

        return true;
    }
    return false;
}




bool Recording::startEncoder(SoundStreamID ssid, const RecordingConfig &cfg)
{
    if (m_EncodingThreads.contains(ssid))
        return false;

    SoundStreamID encID = createNewSoundStream(ssid, false);
    m_RawStreams2EncodedStreams[ssid] = encID;
    m_EncodedStreams2RawStreams[encID] = ssid;

    TQString ext = ".wav";
    switch (m_config.m_OutputFormat) {
        case RecordingConfig::outputWAV:  ext = ".wav";  break;
        case RecordingConfig::outputAIFF: ext = ".aiff"; break;
        case RecordingConfig::outputAU:   ext = ".au";   break;
#ifdef HAVE_LAME
        case RecordingConfig::outputMP3:  ext = ".mp3";  break;
#endif
#ifdef HAVE_LAME
        case RecordingConfig::outputOGG:  ext = ".ogg";  break;
#endif
        case RecordingConfig::outputRAW:  ext = ".raw";  break;
        default:                          ext = ".wav";  break;
    }
    const RadioStation *rs = NULL;
    querySoundStreamRadioStation(ssid, rs);
    TQString station = rs  ? rs->name() + "-" : "";
    station.replace(TQRegExp("[/*?]"), "_");

    TQDate date = TQDate::currentDate();
    TQTime time = TQTime::currentTime();
    TQString sdate;

    sdate.sprintf("%d.%d.%d.%d.%d",date.year(),date.month(),date.day(),time.hour(),time.minute());

    TQString output = m_config.m_Directory
        + "/tderadio-recording-"
        + station
        + sdate
        + ext;

    logInfo(i18n("Recording::outputFile: ") + output);

    RecordingEncoding *thread = NULL;
    switch (m_config.m_OutputFormat) {
#ifdef HAVE_LAME
        case RecordingConfig::outputMP3:
            thread = new RecordingEncodingMP3(this, ssid, cfg, rs, output);
            break;
#endif
#ifdef HAVE_OGG
        case RecordingConfig::outputOGG:
            thread = new RecordingEncodingOgg(this, ssid, cfg, rs, output);
            break;
#endif
        default:
            thread = new RecordingEncodingPCM(this, ssid, cfg, rs, output);
    }

    //m_encodingThread->openOutput(output, rs);

    if (thread->error()) {
        //m_context.setError();
        logError(thread->errorString());
    } else {
        thread->start();
    }
    // store thread even if it has indicated an error
    m_EncodingThreads[ssid] = thread;

    //logDebug("startEncoder thread = " + TQString::number((long long)thread, 16));

    notifySoundStreamCreated(encID);
    return !thread->error();
}


void Recording::stopEncoder(SoundStreamID id)
{
    if (m_EncodingThreads.contains(id)) {

        RecordingEncoding *thread = m_EncodingThreads[id];

        thread->setDone();

        //logDebug("stopEncoder thread = " + TQString::number((long long)thread, 16));
        //logDebug("stopEncoder thread error = " + TQString::number(thread->error(), 16));

        // FIXME: set a timer and do waiting "in background"
        if (!thread->wait(5000)) {
            //m_context.setError();
            logError(i18n("The encoding thread did not finish. It will be killed now."));
            thread->terminate();
            thread->wait();
        } else {
            if (thread->error()) {
                //m_context.setError();
                logError(thread->errorString());
            } else {
                //TQ_UINT64 size = thread->encodedSize();
                //m_context.setEncodedSize(low, high);
                //notifyRecordingContextChanged(m_context);
            }
        }
        delete thread;
        m_EncodingThreads.remove(id);
        SoundStreamID encID = m_RawStreams2EncodedStreams[id];
        m_EncodedStreams2RawStreams.remove(encID);
        m_RawStreams2EncodedStreams.remove(id);
        sendStopPlayback(encID);
        closeSoundStream(encID);
        logInfo(i18n("Recording stopped"));
    }
}


bool Recording::event(TQEvent *_e)
{
    if (SoundStreamEvent::isSoundStreamEvent(_e)) {
        SoundStreamEvent *e = static_cast<SoundStreamEvent*>(_e);
        SoundStreamID id = e->getSoundStreamID();

        if (m_EncodingThreads.contains(id)) {

            RecordingEncoding *thread = m_EncodingThreads[id];

            //logDebug("Recording::event: thread = " + TQString::number((long long)thread, 16));

            if (thread->error()) {
                logError(thread->errorString());
                //m_context.setError();
                stopEncoder(id);
            } else {
                //TQ_UINT64 size = thread->encodedSize();
                //m_context.setEncodedSize(low, high);
                //notifyRecordingContextChanged(m_context);
                if (e->type() == EncodingTerminated) {
                    stopEncoder(id);
                } else if (e->type() == EncodingStep) {
                    SoundStreamEncodingStepEvent *step = static_cast<SoundStreamEncodingStepEvent*>(e);
                    size_t consumed_size = SIZE_T_DONT_CARE;
                    notifySoundStreamData(m_RawStreams2EncodedStreams[id], thread->config().m_SoundFormat,
                                          step->data(), step->size(), consumed_size, step->metaData());
                    if (consumed_size != SIZE_T_DONT_CARE && consumed_size < step->size()) {
                        logError(i18n("Recording::notifySoundStreamData(encoded data): Receivers skipped %1 Bytes").arg(step->size() - consumed_size));
                    }
                }
            }
        }
        return true;
    } else {
        return TQObject::event(_e);
    }
}


bool Recording::getSoundStreamDescription(SoundStreamID id, TQString &descr) const
{
    if (m_EncodedStreams2RawStreams.contains(id)) {
        if (querySoundStreamDescription(m_EncodedStreams2RawStreams[id], descr)) {
            descr = name() + " - " + descr;
            return true;
        }
    }
    return false;
}


bool Recording::getSoundStreamRadioStation(SoundStreamID id, const RadioStation *&rs) const
{
    if (m_EncodedStreams2RawStreams.contains(id)) {
        if (querySoundStreamRadioStation(m_EncodedStreams2RawStreams[id], rs)) {
            return true;
        }
    }
    return false;
}


bool Recording::enumerateSoundStreams(TQMap<TQString, SoundStreamID> &list) const
{
    TQMapConstIterator<SoundStreamID,SoundStreamID> end = m_RawStreams2EncodedStreams.end();
    for (TQMapConstIterator<SoundStreamID,SoundStreamID> it = m_RawStreams2EncodedStreams.begin(); it != end; ++it) {
        TQString tmp = TQString();
        getSoundStreamDescription(*it, tmp);
        list[tmp] = *it;
    }
    return m_RawStreams2EncodedStreams.count() > 0;
}


bool Recording::noticeSoundStreamChanged(SoundStreamID id)
{
    if (m_RawStreams2EncodedStreams.contains(id)) {
        notifySoundStreamChanged(m_RawStreams2EncodedStreams[id]);
        return true;
    }
    return false;
}


bool Recording::isRecordingRunning(SoundStreamID id, bool &b, SoundFormat &sf) const
{
    if (m_EncodingThreads.contains(id)) {
        b  = m_EncodingThreads[id]->running();
        sf = getSoundFormat();
        return true;
    }
    return false;
}


bool Recording::noticeSoundStreamClosed(SoundStreamID id)
{
    if (m_PreRecordingBuffers.contains(id)) {
        if (m_PreRecordingBuffers[id])
            delete m_PreRecordingBuffers[id];
        m_PreRecordingBuffers.remove(id);
    }

    if (m_EncodingThreads.contains(id)) {
        sendStopRecording(id);
        return true;
    }
    return false;
}


#include "recording.moc"
