////////////////////////////////////////////////////////////////////////////////
/**
* @file StreamRecorder.cpp
* @author Mario Chirinos
* @date 2014-02-12
* @brief Streaming recorder with GStreamer
* @note streamRecorder http://streamingmovil.radioformula.com:8000/m1033 3000
*/
////////////////////////////////////////////////////////////////////////////////

#include "StreamRecorder.h"
#include <fstream>
#include <iostream>
#include <cmath>
#include <sstream>
#include <gst/gst.h>
#include <glib.h>
#include <unistd.h>
#include <cstring>
#include <cstdint>
#include <cstdio>

#include "FLAC/metadata.h"
#include "FLAC/stream_encoder.h"
#include <cstring>
using namespace std;

//------------------------------------------------------------------------------

/**
*	Constructor
*	@param source streaming url
*	@param time split recording in segments of time seconds.
*/

StreamRecorder::StreamRecorder(const char* source, int time)
{
    
    int nFrames = ceil(time*STREAMRECORDER_SAMPLERATE/READSIZE);
    recordTime = nFrames*READSIZE/STREAMRECORDER_SAMPLERATE;
    cout << "record time: " << recordTime << endl;
    
    // Required record time
    audioFileDuration = time;

    strcpy(pluginUri,source);
    
    bufferSize=nFrames*READSIZE*STREAMRECORDER_BYTESPERSAMPLE;
    audioBuffer = new unsigned char[bufferSize];
    
    // New buffer
    memset(audioBuffer, 0, bufferSize);
    audioBufferPosition=audioBuffer;
    nBytes=0;
    
    createMainPipeline();
    connect();
}

//------------------------------------------------------------------------------

/**
*	Connect to the stream
*	@return 0 if success
*/

int StreamRecorder::connect()
{
    disconnect();

    cout << "connecting to " << pluginUri << "..." <<  endl;
    
    gst_element_set_state (mainPipeline, GST_STATE_NULL);
    gst_element_set_state (mainPipeline, GST_STATE_READY);
    g_object_set (G_OBJECT (streamSrc), "uri", pluginUri, NULL);
    gst_element_link (streamSrc, audioConvert);
    gst_element_set_state (mainPipeline, GST_STATE_PLAYING);
    return 0;
}

//------------------------------------------------------------------------------

/**
*	disconnect from the stream
*	@return 0 if success
*/

int StreamRecorder::disconnect()
{
    gst_element_unlink (streamSrc, audioConvert);
    gst_element_set_state (mainPipeline, GST_STATE_NULL);
    return 0;
}

//------------------------------------------------------------------------------

/**
*	Create main pipeline
*	@return 0 on success else on error
*/

int StreamRecorder::createMainPipeline()
{
    mainPipeline = gst_pipeline_new("stream-recorder");
    
    GstBus* bus;
    bus = gst_pipeline_get_bus(GST_PIPELINE (mainPipeline));
    gst_bus_add_watch(bus, bus_callback, this);
    gst_object_unref(GST_OBJECT (bus));
    
    streamSrc = gst_element_factory_make("uridecodebin", "stream_source");
    audioConvert = gst_element_factory_make ("audioconvert", "audio_convert");
    //audioResample = gst_element_factory_make ("audioresample", "audio_resample");
    filterCaps = gst_element_factory_make("capsfilter", "filter_cap");
    
    GstCaps *fc = gst_caps_new_full(gst_structure_new ("audio/x-raw",
                                                       "channels", G_TYPE_INT, 1,
                                                       "rate", G_TYPE_INT, STREAMRECORDER_SAMPLERATE,
                                                       "format", G_TYPE_STRING, "S16LE",
                                                       "signed", G_TYPE_BOOLEAN, 1,	//MUST BE SIGNED
                                                       "endianness", G_TYPE_INT, 1234,
                                                       NULL),
                                    NULL);
    
    g_object_set(G_OBJECT (filterCaps), "caps", fc, NULL);
    
    queue0 = gst_element_factory_make("queue", "queue0");
    filter = gst_element_factory_make("identity", "audio_filter");
    g_signal_connect(filter, "handoff", G_CALLBACK (filter_handoff_callback), this);
    
    queue1 = gst_element_factory_make("queue", "queue1");
    fakeSink = gst_element_factory_make("fakesink", "fake_sink");
    //audioSink = gst_element_factory_make("autoaudiosink", "speaker");
    
    gst_bin_add_many (GST_BIN (mainPipeline), streamSrc, audioConvert, filterCaps, queue0, filter,queue1, fakeSink, NULL);
    if(!gst_element_link_many(audioConvert, filterCaps, queue0, filter, queue1, fakeSink,  NULL))
    {
        //gst_bin_add_many (GST_BIN (mainPipeline), streamSrc, audioConvert, filterCaps, queue0, filter, queue1, audioSink, NULL);
        // if(!gst_element_link_many(audioConvert, filterCaps, queue0, filter, queue1, audioSink,  NULL)){
        
        cerr << "mainPipeline: Failed to link elements in the pipeline" << endl;
        exit(0);
    }
    
    g_signal_connect(streamSrc, "pad-added", G_CALLBACK(srcNewPad_callback), this);
    gst_element_set_state (mainPipeline, GST_STATE_NULL);
    
    return 0;
}

//------------------------------------------------------------------------------

/**
 *
 * @param class instance
 * @return False if an error occurs
 */

gboolean StreamRecorder::reconnectURIStream(void* instance)
{
    cout << "Trying to reconnect with the stream..." << endl << endl;
    ((StreamRecorder*)instance)->connect();
    
    return FALSE;
}

//------------------------------------------------------------------------------

/**
*	disconnect from the stream
*	@param the GstBus that sent the message
*	@param message the GstMessage
*	@param user_data NULL
*	@return unimplemented
*/

int StreamRecorder::bus_callback (GstBus *bus, GstMessage *message, void *user_data)
{
    //printf("StreamRecorder got %s message\n", GST_MESSAGE_TYPE_NAME (message));
    switch (GST_MESSAGE_TYPE (message))
    {
        case GST_MESSAGE_EOS:
            cout << "** End of stream **" << endl << endl;
            
            ((StreamRecorder*)user_data)->isDisconnected = true;
            
            break;
        
        case GST_MESSAGE_ERROR:
    
            GError *error;
            gchar  *debug;
            gst_message_parse_error (message, &error, &debug);
            g_free (debug);
            
            cerr << "Error - code: (" << error->code << ") - Message:" <<  error->message << endl;
            
            switch (error->code)
            {
                case STREAM_ENOUGH_DATA_ERROR:
                    /** Last message in errors sequence*/
                    ((StreamRecorder*)user_data)->isDisconnected = true;
                   break;
                   
                default:
                    return  TRUE;
            }
        
            g_error_free (error);
            
            break;

        default:
            return TRUE;
    }
    

    if(((StreamRecorder*)user_data)->isDisconnected)
    {
        /** Try reconnect with the radio stream*/

        cout << "\nReconnection started..." << endl;
        g_timeout_add(RECONNECTION_DELAY*1000, reconnectURIStream, user_data);
    }
    
    return TRUE;
    
}

//------------------------------------------------------------------------------

/**
 * Do all the buffer saving and restart operations
 */

void StreamRecorder::saveBuffer()
{
    compressBuffer();
    // Restart buffer configuration
    audioBufferPosition=audioBuffer;
    memset (audioBuffer, 0, bufferSize);
    nBytes=0;
}

//------------------------------------------------------------------------------

/**
*	CallBack to link the pads created by uridecodebin
*	@param element The uridecodebin element
*	@param pad The pad added
*	@param data this
*/

void StreamRecorder::srcNewPad_callback(GstElement *element, GstPad *pad, void *data)
{
    
    cout << gst_element_get_name(element)<< " adding pad..." << endl;
    cout << "Pad Name: " << gst_pad_get_name (pad) << endl;
    
    GstPad *sinkpad;
    
    //gst_pad_get_caps is for gst v0.1
    // GstCaps *new_pad_caps = gst_pad_get_caps (pad);
    
    GstCaps *new_pad_caps = gst_pad_query_caps(pad, NULL);
    GstStructure *new_pad_struct = gst_caps_get_structure (new_pad_caps, 0);
    const gchar *new_pad_type = gst_structure_get_name (new_pad_struct);
    
    if (g_str_has_prefix (new_pad_type, "audio/x-raw"))
    {
        cout  << "linking " << new_pad_type << endl;
        GstElement *nextElement = ((StreamRecorder*)data)->audioConvert;
        sinkpad = gst_element_get_static_pad (nextElement, "sink");
        
        if (GST_PAD_LINK_FAILED (gst_pad_link (pad, sinkpad)))
        {
            cerr << "Type is "<< new_pad_type <<" but link failed." << endl;
            exit(0);
        }
        else
            cout <<"Link succeeded " << new_pad_type << endl;
    }
}

//------------------------------------------------------------------------------

/**
*	CallBack for handoff signal of identity filter
*	@param filter Identity filter
*	@param buffer The buffer that just has been received
*	@param user_data this
*	@return unimplemented
*/

int StreamRecorder::filter_handoff_callback(GstElement* filter, GstBuffer* buffer, void* user_data)
{
    GstMapInfo info;
    if(!gst_buffer_map (buffer, &info, GST_MAP_READ))
        cout << "ERROR: MAPPING IS NOT VALID" << endl;
    
    long int currentTime;
    long int actualRecordTime;
    
    //GST_BUFFER_DATA is for gst v0.1
    // ((StreamRecorder*)user_data)->addToBuffer((unsigned char*)GST_BUFFER_DATA (buffer));
    
    // If we come from a disconnection
    if(((StreamRecorder*)user_data)->isDisconnected)
    {
        
        currentTime = time(NULL);
        actualRecordTime = currentTime - ((StreamRecorder*)user_data)->timestamp;
        
        if(actualRecordTime >= ((StreamRecorder*)user_data)->audioFileDuration)
        {
            /** Complete the buffer with missing bytes*/
            ((StreamRecorder*)user_data)->nBytes = ((StreamRecorder*)user_data)->bufferSize;
            ((StreamRecorder*)user_data)->saveBuffer();
    
            cout << "The buffer has been saved..." << endl;
        }
        else
        {
            long int bytesToAdd = actualRecordTime*STREAMRECORDER_SAMPLERATE*STREAMRECORDER_BYTESPERSAMPLE*NCHANNELS;
            printf("Bytes to add: %ld\n", bytesToAdd);

            // Adjust the bytes to add value to a buffer size divider
            bytesToAdd = ceil(bytesToAdd/(STREAMRECORDER_BYTESPERSAMPLE*READSIZE))*STREAMRECORDER_BYTESPERSAMPLE*READSIZE;
            printf("Adjusted to: %ld\n", bytesToAdd);
        
            ((StreamRecorder*)user_data)->audioBufferPosition = ((StreamRecorder*)user_data)->audioBuffer;
            ((StreamRecorder*)user_data)->audioBufferPosition += bytesToAdd;
            ((StreamRecorder*)user_data)->nBytes = bytesToAdd;
    
            cout << "The pointer has been updated..." << endl;
        }
    
        ((StreamRecorder*)user_data)->isDisconnected = false;
    }
    
    ((StreamRecorder*)user_data)->addToBuffer((unsigned char*)info.data, info.size);
    gst_buffer_unmap (buffer, &info);
    return 0;
}

//------------------------------------------------------------------------------

/**
*	Add audio data to audioBuffer
*	@param data Audio data to add
*	@param length Data length
*	@return Bytes writen
*/

int StreamRecorder::addToBuffer(unsigned char* data, int length) 
{
    
    int bytesRead = length;// READSIZE*STREAMRECORDER_BYTESPERSAMPLE;
    bool isNewAudioFile;
    
    /** Useful for obtain the filename*/
    isNewAudioFile = nBytes == 0;
    
    cout << "Data size: " << bytesRead << endl;
    
    /** add info to buffer*/
    memcpy((char*)audioBufferPosition, (char*)data, bytesRead);
    
    /** Update pointer*/
    nBytes+=bytesRead;
    audioBufferPosition+=bytesRead;
    
    if(isNewAudioFile)
    {
        cout << "New audio stream" << endl;
        
        /** filename and initial record time*/
        timestamp = time(NULL);
        cout << "Audio filename (timestamp): " << timestamp << endl;
    }
    else
    {
        long int currentTime;
        long int actualRecordTime;
        
        currentTime = time(NULL);
        actualRecordTime = currentTime-timestamp;
        cout << "Record time: " << actualRecordTime << endl;
        
        cout << "Bytes readed " << nBytes << endl;
        cout << "Buffer size " << bufferSize << endl;
        
        /** If the buffer is full, save it in flac file */
        if(nBytes >= bufferSize)
        {
            saveBuffer();
        }
    }
    
    return nBytes;
}

//------------------------------------------------------------------------------

/**
*	Save audio data (audioBuffer) in flac format
*	@return unimplemented
*/

int StreamRecorder::compressBuffer()
{
    
    //long int currentTime = time(NULL);
    stringstream ss;
    string fileNameStr, currentTimeStr;
    ss << timestamp << endl;
    getline(ss, currentTimeStr);
    
    ss << currentTimeStr << ".flac" << endl;
    getline(ss, fileNameStr);
    
    // Restart timestamp;
    timestamp = 0;
    
    int readsize = READSIZE;
    FLAC__bool ok = true;
    FLAC__StreamEncoder *encoder = 0;
    FLAC__StreamMetadata *metadata[2];
    FLAC__StreamMetadata_VorbisComment_Entry entry;
    FLAC__StreamEncoderInitStatus init_status;
    
    /* allocate the encoder */
    if((encoder = FLAC__stream_encoder_new()) == NULL)
    {
        fprintf(stderr, "ERROR: allocating encoder\n");
        return 1;
    }
    
    ok &= FLAC__stream_encoder_set_verify(encoder, true);
    ok &= FLAC__stream_encoder_set_compression_level(encoder, 5);
    ok &= FLAC__stream_encoder_set_channels(encoder, 1);
    ok &= FLAC__stream_encoder_set_bits_per_sample(encoder, STREAMRECORDER_BYTESPERSAMPLE*8);
    ok &= FLAC__stream_encoder_set_sample_rate(encoder, STREAMRECORDER_SAMPLERATE);
    ok &= FLAC__stream_encoder_set_total_samples_estimate(encoder, nBytes/STREAMRECORDER_BYTESPERSAMPLE);
    
    /* now add some metadata; we'll add some tags and a padding block */
    if(ok)
    {
        if(
            (metadata[0] = FLAC__metadata_object_new(FLAC__METADATA_TYPE_VORBIS_COMMENT)) == NULL ||
                (metadata[1] = FLAC__metadata_object_new(FLAC__METADATA_TYPE_PADDING)) == NULL ||
                /* there are many tag (vorbiscomment) functions but these are convenient for this particular use: */
                !FLAC__metadata_object_vorbiscomment_entry_from_name_value_pair(&entry, "ARTIST", "artist") ||
                !FLAC__metadata_object_vorbiscomment_append_comment(metadata[0], entry, /*copy=*/false) || /* copy=false: let metadata object take control of entry's allocated string */
                !FLAC__metadata_object_vorbiscomment_entry_from_name_value_pair(&entry, "YEAR", "year") ||
                !FLAC__metadata_object_vorbiscomment_append_comment(metadata[0], entry, /*copy=*/false)
            )
        {
            fprintf(stderr, "ERROR: out of memory or tag error\n");
            ok = false;
        }
        metadata[1]->length = 1234; /* set the padding length */
        ok = FLAC__stream_encoder_set_metadata(encoder, metadata, 2);
    }
    
    /* initialize encoder */
    if(ok)
    {
        init_status = FLAC__stream_encoder_init_file(encoder, fileNameStr.c_str(), NULL, /*client_data=*/NULL);
        if(init_status != FLAC__STREAM_ENCODER_INIT_STATUS_OK)
        {
            fprintf(stderr, "ERROR: initializing encoder: %s\n", FLAC__StreamEncoderInitStatusString[init_status]);
            ok = false;
        }
    }
    
    int channels = 1;
//	int bps = 16;
    unsigned char* audioBufferTmp = audioBuffer;
    unsigned char* buffer = new unsigned char[readsize*STREAMRECORDER_BYTESPERSAMPLE*channels];
    int* pcm = new int[readsize*channels];
    /* read blocks of samples from WAVE file and feed to encoder */
    
    if(ok)
    {
        int left = nBytes;
        while(left&&ok)
        {
            
            int need = (left>readsize? readsize : left);
            memcpy(buffer, audioBufferTmp, need);//*sizeof(short));
            audioBufferTmp+=need;//*sizeof(short);
            // /* convert the packed little-endian 16-bit PCM samples from WAVE into an interleaved FLAC__int32 buffer for libFLAC */
            for(int i=0; i < need*channels; i++)
            {
                /* inefficient but simple and works on big- or little-endian machines */
                pcm[i] = (FLAC__int32)(((FLAC__int16)(FLAC__int8)buffer[2*i+1] << 8) | (FLAC__int16)buffer[2*i]);
            }
            /* feed samples to encoder */
            ok = FLAC__stream_encoder_process_interleaved(encoder, pcm, need/STREAMRECORDER_BYTESPERSAMPLE);
            left -= need;
        }
    }
    else
        cout << "-ERROR-" << endl;
    
    ok &= FLAC__stream_encoder_finish(encoder);
    
    fprintf(stderr, "encoding: %s\n", ok? "succeeded" : "FAILED");
    fprintf(stderr, "   state: %s\n", FLAC__StreamEncoderStateString[FLAC__stream_encoder_get_state(encoder)]);
    
    /* now that encoding is finished, the metadata can be freed */
    FLAC__metadata_object_delete(metadata[0]);
    FLAC__metadata_object_delete(metadata[1]);
    FLAC__stream_encoder_delete(encoder);
    
    delete[] buffer;
    delete[] pcm;
    return 0;
}

//------------------------------------------------------------------------------

