//Copyright 2015 <>< Charles Lohr under the ColorChord License.

//This file is really rough.  Full duplex doesn't seem to work hardly at all.


#include "sound.h"
#include "os_generic.h"
#include "parameters.h"
#include <stdlib.h>

#include <pulse/simple.h>
#include <pulse/pulseaudio.h>
#include <pulse/error.h>
#include <stdio.h>
#include <string.h>

#define BUFFERSETS 3


//from http://www.freedesktop.org/wiki/Software/PulseAudio/Documentation/Developer/Clients/Samples/AsyncPlayback/
//also http://maemo.org/api_refs/5.0/5.0-final/pulseaudio/pacat_8c-example.html


struct SoundDriverPulse
{
	void (*CloseFn)( struct SoundDriverPulse * object );
	int (*SoundStateFn)( struct SoundDriverPulse * object );
	SoundCBType callback;
	int channelsPlay;
	int spsPlay;
	int channelsRec;
	int spsRec;

	const char * sourceName;
	og_thread_t thread;
 	pa_stream *  	play;
 	pa_stream *  	rec;
	pa_context *  pa_ctx;
	pa_mainloop *pa_ml;
	int pa_ready;
	int buffer;
	//More fields may exist on a per-sound-driver basis
};



void CloseSoundPulse( struct SoundDriverPulse * r );

int SoundStatePulse( struct SoundDriverPulse * soundobject )
{
	return ((soundobject->play)?1:0) | ((soundobject->rec)?2:0);
}

void CloseSoundPulse( struct SoundDriverPulse * r )
{
	if( r )
	{
		if( r->play )
		{
			pa_stream_unref (r->play);
			r->play = 0;
		}

		if( r->rec )
		{
			pa_stream_unref (r->rec);
			r->rec = 0;
		}
		OGUSleep(2000);
		OGCancelThread( r->thread );
		free( r );
	}
}

static void * SoundThread( void * v )
{
	struct SoundDriverPulse * r = (struct SoundDriverPulse*)v;
	while(1)
	{
		pa_mainloop_iterate( r->pa_ml, 1, NULL );
	}
	return 0;
}
/*
	int i;
	int error;
	struct SoundDriverPulse * r = (struct SoundDriverPulse*)v;

	float * bufr[BUFFERSETS];
	float * bufp[BUFFERSETS];

	for(i = 0; i < BUFFERSETS; i++ )
	{
		bufr[i] = malloc( r->buffer * sizeof(float) * r->channelsRec );
		bufp[i] = malloc( r->buffer * sizeof(float) * r->channelsPlay  );
	}

	while( r->play || r->rec )
	{
		i = (i+1)%BUFFERSETS;

		if( r->rec )
		{
			if (pa_stream_read(r->rec, bufr[i], r->buffer * sizeof(float) * r->channelsRec, &error) < 0) {
				fprintf(stderr, __FILE__": pa_stream_write() failed: %s\n", pa_strerror(error));
				pa_stream_unref( r->play );
				r->rec = 0;
			}
		}

		int playbacksamples = 0;
		r->callback( bufp[i], bufr[i], r->buffer, &playbacksamples, (struct SoundDriver*)r );
		playbacksamples *= sizeof( float ) * r->channelsPlay;

		if( r->play )
		{
			if (pa_stream_write(r->play, bufp[i], playbacksamples, NULL, 0LL, PA_SEEK_RELATIVE) < 0) {
				fprintf(stderr, __FILE__": pa_stream_write() failed: %s\n", pa_strerror(error));
				pa_stream_unref( r->play );
				r->play = 0;
			}
		}
	}

}*/

static void stream_request_cb(pa_stream *s, size_t length, void *userdata) {
//	pa_usec_t usec;

	struct SoundDriverPulse * r = (struct SoundDriverPulse*)userdata;
	if( r->rec )
	{
		return;
	}

/*
	//Neat: You might want this:

	pa_stream_get_latency(s,&usec,&neg);

	if (sampleoffs*2 + length > sizeof(sampledata))
	{
		sampleoffs = 0;
	}

	if (length > sizeof(sampledata)) {
		length = sizeof(sampledata);
	}

*/


//	pa_stream_write(s, &sampledata[sampleoffs], length, NULL, 0LL, PA_SEEK_RELATIVE);
	int playbacksamples = 0;
	float bufr[r->buffer*r->channelsRec];
	float bufp[r->buffer*r->channelsPlay];

	r->callback( bufp, bufr, r->buffer, &playbacksamples, (struct SoundDriver*)r );
	//playbacksamples *= sizeof( float ) * r->channelsPlay;

	pa_stream_write(r->play, &bufp, playbacksamples, NULL, 0LL, PA_SEEK_RELATIVE);

}


static void stream_record_cb(pa_stream *s, size_t length, void *userdata) {
//	pa_usec_t usec;
//	int neg;

	struct SoundDriverPulse * r = (struct SoundDriverPulse*)userdata;

/*	pa_stream_get_latency(s,&usec,&neg);
	printf("  latency %8d us\n",(int)usec);

	if (sampleoffs*2 + length > sizeof(sampledata))
	{
		sampleoffs = 0;
	}

	if (length > sizeof(sampledata)) {
		length = sizeof(sampledata);
	}*/

	int playbacksamples = 0;
	float * bufr;

    if (pa_stream_peek(r->rec, (void*)&bufr, &length) < 0) {
        fprintf(stderr, ("pa_stream_peek() failed: %s\n"), pa_strerror(pa_context_errno(r->pa_ctx)));
        return;
    }

	float * buffer;
    buffer = pa_xmalloc(length);
    memcpy(buffer, bufr, length);
	pa_stream_drop(r->rec);

	float bufp[length*r->channelsPlay];
	r->callback( bufp, buffer, length/sizeof(float)/r->channelsRec, &playbacksamples, (struct SoundDriver*)r );
	//playbacksamples *= sizeof( float ) * r->channelsPlay;
	pa_xfree( buffer );
	if( r->play )
		pa_stream_write(r->play, &bufp, playbacksamples*sizeof(float)*r->channelsPlay, NULL, 0LL, PA_SEEK_RELATIVE);
}



static void stream_underflow_cb(pa_stream *s, void *userdata) {
  // We increase the latency by 50% if we get 6 underflows and latency is under 2s
  // This is very useful for over the network playback that can't handle low latencies
  printf("underflow\n");
//  underflows++;
/*  if (underflows >= 6 && latency < 2000000) {
    latency = (latency*3)/2;
    bufattr.maxlength = pa_usec_to_bytes(latency,&ss);
    bufattr.tlength = pa_usec_to_bytes(latency,&ss);  
    pa_stream_set_buffer_attr(s, &bufattr, NULL, NULL);
    underflows = 0;
    printf("latency increased to %d\n", latency);
  }*/
}


void pa_state_cb(pa_context *c, void *userdata) {
	pa_context_state_t state;
	int *pa_ready = userdata;
	state = pa_context_get_state(c);
	switch  (state) {
		// These are just here for reference
		case PA_CONTEXT_UNCONNECTED:
		case PA_CONTEXT_CONNECTING:
		case PA_CONTEXT_AUTHORIZING:
		case PA_CONTEXT_SETTING_NAME:
		default:
			break;
		case PA_CONTEXT_FAILED:
		case PA_CONTEXT_TERMINATED:
			*pa_ready = 2;
			break;
		case PA_CONTEXT_READY:
			*pa_ready = 1;
		break;
	}
}


void * InitSoundPulse( SoundCBType cb )
{
	static pa_buffer_attr bufattr;
	static pa_sample_spec ss;
	int error;
	pa_mainloop_api *pa_mlapi;
	struct SoundDriverPulse * r = malloc( sizeof( struct SoundDriverPulse ) );

	r->pa_ml = pa_mainloop_new();
	pa_mlapi = pa_mainloop_get_api(r->pa_ml);
	const char * title = GetParameterS( "title", "PA Test" );
	r->pa_ctx = pa_context_new(pa_mlapi, title );
	pa_context_connect(r->pa_ctx, NULL, 0, NULL);

	//TODO: pa_context_set_state_callback

	r->CloseFn = CloseSoundPulse;
	r->SoundStateFn = SoundStatePulse;
	r->callback = cb;

	r->spsPlay = GetParameterI( "samplerate", 44100 );
	r->channelsPlay = GetParameterI( "channels", 2 );
	r->spsRec = r->spsPlay;
	r->channelsRec = r->channelsPlay;
	r->sourceName = GetParameterS( "sourcename", NULL );

	if( strcmp( r->sourceName, "default" ) == 0  )
	{
		r->sourceName = 0;
	}

	r->play = 0;
	r->rec = 0;
	r->buffer = GetParameterI( "buffer", 1024 );
	printf ("Pulse: from: %s (%s) / %dx%d (%d)\n", r->sourceName, title, r->spsPlay, r->channelsPlay, r->buffer );

	memset( &ss, 0, sizeof( ss ) );

	ss.format = PA_SAMPLE_FLOAT32NE;
	ss.rate = r->spsPlay;
	ss.channels = r->channelsPlay;

	r->pa_ready = 0;
	pa_context_set_state_callback(r->pa_ctx, pa_state_cb, &r->pa_ready);

	while (r->pa_ready == 0)
	{
		pa_mainloop_iterate(r->pa_ml, 1, NULL);
	}

	int bufbytes = r->buffer * sizeof(float) * r->channelsRec;

	if( GetParameterI( "play", 1 ) )
	{
		if (!(r->play = pa_stream_new(r->pa_ctx, "Play", &ss, NULL))) {
			error = -3; //XXX ??? TODO
			fprintf(stderr, __FILE__": pa_simple_new() failed: %s\n", pa_strerror(error));
			goto fail;
		}

		pa_stream_set_underflow_callback(r->play, stream_underflow_cb, NULL);
		pa_stream_set_write_callback(r->play, stream_request_cb, r );

		bufattr.fragsize = (uint32_t)-1;
		bufattr.maxlength = bufbytes*3; //XXX TODO Consider making this -1
		bufattr.minreq = 0;
		bufattr.prebuf =  (uint32_t)-1;
		bufattr.tlength = bufbytes*3;
		int ret = pa_stream_connect_playback(r->play, NULL, &bufattr,
				                    // PA_STREAM_INTERPOLATE_TIMING
				                    // |PA_STREAM_ADJUST_LATENCY //Some servers don't like the adjust_latency flag.
				                    // |PA_STREAM_AUTO_TIMING_UPDATE, NULL, NULL);
					0, NULL, NULL );
		printf( "Play stream.\n" );
		if( ret < 0 )
		{
			fprintf(stderr, __FILE__": (PLAY) pa_stream_connect_playback() failed: %s\n", pa_strerror(ret));
			goto fail;
		}
	}

	if( GetParameterI( "rec", 1 ) )
	{
		if (!(r->rec = pa_stream_new(r->pa_ctx, "Record", &ss, NULL))) {
			error = -3; //XXX ??? TODO
			fprintf(stderr, __FILE__": pa_simple_new() failed: %s\n", pa_strerror(error));
			goto fail;
		}

		pa_stream_set_read_callback(r->rec, stream_record_cb, r );

		bufattr.fragsize = bufbytes;
		bufattr.maxlength = (uint32_t)-1;//(uint32_t)-1; //XXX: Todo, should this be low?
		bufattr.minreq = bufbytes;
		bufattr.prebuf = (uint32_t)-1;
		bufattr.tlength = bufbytes*3;
		printf( "Source: %s\n", r->sourceName );
		int ret = pa_stream_connect_record(r->rec, r->sourceName, &bufattr, 0
//							       |PA_STREAM_INTERPOLATE_TIMING
			                       |PA_STREAM_ADJUST_LATENCY  //Some servers don't like the adjust_latency flag.
//		                     	|PA_STREAM_AUTO_TIMING_UPDATE
//				0 
				);

		printf( "Got handle: %d\n", ret );
		if( ret < 0 )
		{
			fprintf(stderr, __FILE__": (REC) pa_stream_connect_playback() failed: %s\n", pa_strerror(ret));
			goto fail;
		}
	}

	printf( "Pulse initialized.\n" );


//	SoundThread( r );
	r->thread = OGCreateThread( SoundThread, r );
	return r;

fail:
	if( r )
	{
		if( r->play ) pa_xfree (r->play);
		if( r->rec ) pa_xfree (r->rec);
		free( r );
	}
	return 0;
}



EXECUTE_AT_BOOT( PulseSoundReg, RegSound( 11, "PULSE", InitSoundPulse ) );


