#include "paMat.h"
#include "mex.h"
#include "matrix.h"
#include "portaudio.h"


/* Initialize variables?? */
PaStream *matStream = NULL;
paMatData *data = NULL;


/*
 * A single function to call for all of the operations on port audio
 * stream.  This function dispatches the command to the proper
 * function.  Useage:
 *
 * open: paMat(0, numInputChannels, numOutputChannels, sampleRate, ...
 *       framesPerBuffer, callback)
 * start: paMat(1)
 * stop: paMat(2)
 * close: paMat(3)
 */
void mexFunction (int nlhs, mxArray *plhs[], int nrhs, const mxArray *prhs[])
{
  int command;
  command = (int) *mxGetPr(prhs[0]);

  if(command == 0) {
    /* open stream */
    openStream(nrhs-1, prhs+1);
  } else if(command == 1) {
    /* start stream */
    startStream();
  } else if(command == 2) {
    /* stop stream */
    stopStream();
  } else if(command == 3) {
    /* close stream */
    closeStream();
  } else {
    /* unknown command */
    mexErrMsgTxt("Unknown command");
  }  
}


/* open a portaudio stream and register a matlab callback with that
 * stream. The interface for this function is:
 *
 * function Pa_OpenDefaultStream(numInputChannels, ...
 *  numOutputChannels, sampleRate, framesPerBuffer, callback)
 *
 * Where everything is as you would expect except the "callback"
 * argument is just a string indicating the name of the function to
 * call, not any as fancy as a function pointer.
 */
void openStream(int nrhs, const mxArray *prhs[])
{
  /* if there is already a stream open, close it, issue a matlab
     warning message */
  if(matStream != NULL) {
    mexWarnMsgTxt("There was already another stream open. Closing it...");
    paMatClose();
  }

  /* create appropriate data structure to hold info */
  data = (paMatData*)mxMalloc(sizeof(paMatData));
  if(data == NULL) {
    mexErrMsgTxt("Couldn't allocate memory for data structure!");
    return;
  }

  /* Convert matlab arguments to C arguments */
  int nInC = (int)mxGetScalar(prhs[0]);
  int nOutC = (int)mxGetScalar(prhs[1]);
  int sr = (int)mxGetScalar(prhs[2]);
  int fpb = (int)mxGetScalar(prhs[3]);
  data->samplesPerInFrame = nInC;
  data->samplesPerOutFrame = nOutC;

  /* copy name of function */
  int strLen = mxGetN(prhs[4]) + 1;
  data->function = mxMalloc(strLen * sizeof(char));
  mxGetString(prhs[4], data->function, strLen);
  mexPrintf("function to call: %s\n", data->function);

  /* allocate matlab arrays */
  data->nrhs = 3;
  data->prhs[0] = mxCreateDoubleMatrix(1, data->samplesPerInFrame*fpb, mxREAL);
  data->prhs[1] = mxCreateDoubleMatrix(1, 1, mxREAL);
  data->prhs[2] = mxCreateDoubleMatrix(1, 1, mxREAL);

  mexMakeMemoryPersistent(data);
  mexMakeMemoryPersistent(data->function);
  mexMakeArrayPersistent(data->prhs[0]);
  mexMakeArrayPersistent(data->prhs[1]);
  mexMakeArrayPersistent(data->prhs[2]);
  mexAtExit(paMatClose);

  /* call real portaudio OpenDefaultStream function */
  Pa_OpenDefaultStream(&matStream, nInC, nOutC, paFloat32, sr, fpb, 
		       0, paMatCallback, data);

  if(matStream == NULL) {
    mexWarnMsgTxt("matStream is null after calling PA");
    paMatClose();
  }
}


void closeStream()
{
  if(okToGo())
    paMatClose();
}


void startStream()
{
  if(okToGo())
    Pa_StartStream(matStream);
}


void stopStream()
{
  if(okToGo())
    Pa_StopStream(matStream);
}


int okToGo()
{
  if(matStream == NULL) {
    mexWarnMsgTxt("Can't find an open stream to close...");
    return 0;
  } else if(data == NULL) {
    mexWarnMsgTxt("Data is null...");
    return 0;
  } else {
    return 1;
  }
}


/*
 * The interface to the matlab function must be:
 * function output = callback(input, framesPerBuffer, time)
 */
int paMatCallback(void *inputBuffer, void *outputBuffer,
		  unsigned long framesPerBuffer,
		  PaTimestamp outTime, void *userData )
{
  paMatData *cdata = (paMatData *)userData;
  float *out = (float*)outputBuffer;
  float *in = (float*)inputBuffer;
  int i;

  double *min = mxGetPr(cdata->prhs[0]);
  /* Copy data into the input matrix */
  for(i=0; i<framesPerBuffer*cdata->samplesPerInFrame; i++)
    min[i] = in[i];

  mxGetPr(cdata->prhs[1])[0] = (double)framesPerBuffer;
  mxGetPr(cdata->prhs[2])[0] = (double)outTime;

  mexCallMATLAB(cdata->nlhs, cdata->plhs, cdata->nrhs, cdata->prhs, 
		cdata->function);

  double *mout = mxGetPr(cdata->plhs[0]);
  /* Copy data out of the output matrix */
  for(i=0; i<framesPerBuffer*cdata->samplesPerOutFrame; i++)
    mout[i] = out[i];

  return 0;
}


/* Clean up the allocated data structures and close the stream */
void paMatClose()
{
  if(matStream != NULL) {
    /* call Pa_CloseStream */
    Pa_CloseStream(matStream);
    
    /* set stream to null */
    matStream = NULL;
  }

  if(data != NULL) {
    /* destroy the "data" structure */
    mxDestroyArray(data->prhs[2]);
    mxDestroyArray(data->prhs[1]);
    mxDestroyArray(data->prhs[0]);
    mxFree(data->function);
    mxFree(data);
    data = NULL;
  }
}
