/***
  This file is part of PulseAudio.

  Copyright 2009 Intel Corporation
  Contributor: Pierre-Louis Bossart <pierre-louis.bossart@intel.com>

  PulseAudio is free software; you can redistribute it and/or modify
  it under the terms of the GNU Lesser General Public License as published
  by the Free Software Foundation; either version 2.1 of the License,
  or (at your option) any later version.

  PulseAudio 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.

  You should have received a copy of the GNU Lesser General Public License
  along with PulseAudio; if not, write to the Free Software
  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
  USA.
***/

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <stdio.h>
#include <math.h>

#include <pulse/xmalloc.h>

#include <pulsecore/sink-input.h>
#include <pulsecore/module.h>
#include <pulsecore/modargs.h>
#include <pulsecore/namereg.h>
#include <pulsecore/log.h>
#include <pulsecore/core-util.h>

#include <pulse/timeval.h>

#include "module-loopback-symdef.h"

PA_MODULE_AUTHOR("Pierre-Louis Bossart");
PA_MODULE_DESCRIPTION("Loopback from source to sink");
PA_MODULE_VERSION(PACKAGE_VERSION);
PA_MODULE_LOAD_ONCE(FALSE);
PA_MODULE_USAGE(
		"source=<source to connect to> sink=<sink to connect to> latency=<latency> nbchannels=<nb>");

#define MEMBLOCKQ_MAXLENGTH (256*1024*16) /* FIXME: adjust size to latency? */

struct userdata {
    pa_core *core;
    pa_module *module;
    pa_sink_input *sink_input;
    pa_source_output *source_output;

    pa_memchunk sink_memchunk; /* needed for the sink only */
    pa_memblockq *memblockq;
};

static const char* const valid_modargs[] = {
    "source",
    "sink",
    "latency",
    "nbchannels",
    NULL,
};

static void create_memchunk(pa_memchunk *c, pa_mempool *pool, uint32_t rate, uint32_t latency, uint32_t nchannels) {
    uint32_t samples;

    pa_memchunk_reset(c);

    samples = rate*latency; /* assume integer number of samples */
    samples /= 1000;
    samples *= nchannels;
    samples /= 2; /* FIXME: division added to align with source behavior, the number pushed by source is half the latency, unsure if this is normal */
 
    pa_log("allocating memchunk of %d", samples);

    c->length = samples * sizeof(float);
    c->memblock = pa_memblock_new(pool, c->length);

}


/* Called from input thread context */
static void source_output_push_cb(pa_source_output *o, const pa_memchunk *chunk) {
    struct userdata *u;

    pa_source_output_assert_ref(o);
    pa_assert_se(u = o->userdata);
    pa_assert(chunk);

#ifdef DEBUG_SOURCE_TIMING
    {
      struct timeval tv;
      static struct timeval old_tv;
      static int first_time=1;
      
      pa_gettimeofday(&tv);
      
      if (first_time ==1) {
	first_time = 0;
      } else {
	pa_usec_t diff = pa_timeval_diff(&tv, &old_tv);
	
	pa_log("source pushed buffer at time %ld size %d", (long)diff, chunk->length/sizeof(float));
	
      }
      old_tv = tv;
    }
#endif /* DEBUG_SOURCE_TIMING */

    /* push into queue */
    /* FIXME: no sure why number of samples is different for every push, with latency definition we should have the same number of samples all the time */
    if (pa_memblockq_push(u->memblockq,chunk)<0) {
      pa_log("could not push into queue");
    }
    else {
      pa_log("push: queue size %d",pa_memblockq_get_length(u->memblockq));
    }
}

static void source_output_process_rewind_cb(pa_source_output *o, size_t nbytes) {
    struct userdata *u;

    pa_source_output_assert_ref(o);
    pa_assert_se(u = o->userdata);

    /* FIXME: no idea what to do here */

}

/* Called from input IO thread context */
static void source_output_state_change_cb(pa_source_output *o, pa_source_output_state_t state) {
    struct userdata *u;

    pa_source_output_assert_ref(o);
    pa_assert_se(u = o->userdata);
    
    /* FIXME: no idea what to do */
}

static void source_output_kill_cb(pa_source_output *o) {
    struct userdata *u;

    pa_source_output_assert_ref(o);
    pa_assert_se(u = o->userdata);

    pa_source_output_unlink(u->source_output);
    pa_source_output_unref(u->source_output);
    u->source_output = NULL;

    pa_module_unload_request(u->module, TRUE);
}

/* Called from output thread context */
static int sink_input_pop_cb(pa_sink_input *i, size_t nbytes, pa_memchunk *chunk) {
    struct userdata *u;

    pa_sink_input_assert_ref(i);
    pa_assert_se(u = i->userdata);
    pa_assert(chunk);

#ifdef DEBUG_SINK_TIMING
    {
      pa_gettimeofday(&tv);
      if (first_time ==1) {
	first_time = 0;
      } else {
	pa_usec_t diff = pa_timeval_diff(&tv, &old_tv);
	
	pa_log("sink popped buffer at time %ld", (long)diff);
	
      }
      old_tv = tv;
    }
#endif /* DEBUG_SINK_TIMING */

    *chunk = u->sink_memchunk;
    if (pa_memblockq_peek(u->memblockq, chunk) < 0) {
      pa_log("coud not peek into queue");
      return -1;
    } 
    pa_log("pop queue size %d",pa_memblockq_get_length(u->memblockq));

    pa_memblockq_drop(u->memblockq, chunk->length);

    return 0;
}

static void sink_input_process_rewind_cb(pa_sink_input *i, size_t nbytes) {
    struct userdata *u;

    pa_sink_input_assert_ref(i);
    pa_assert_se(u = i->userdata);

    /* FIXME: no idea what to do here */
}

static void sink_input_kill_cb(pa_sink_input *i) {
    struct userdata *u;

    pa_sink_input_assert_ref(i);
    pa_assert_se(u = i->userdata);

    pa_sink_input_unlink(u->sink_input);
    pa_sink_input_unref(u->sink_input);
    u->sink_input = NULL;

    pa_module_unload_request(u->module, TRUE);
}

/* Called from IO output thread context */
static void sink_input_state_change_cb(pa_sink_input *i, pa_sink_input_state_t state) {
    struct userdata *u;

    pa_sink_input_assert_ref(i);
    pa_assert_se(u = i->userdata);

    /* If we are added for the first time, ask for a rewinding so that
     * we are heard right-away. */
    if (PA_SINK_INPUT_IS_LINKED(state) &&
        i->thread_info.state == PA_SINK_INPUT_INIT)
        pa_sink_input_request_rewind(i, 0, FALSE, TRUE, TRUE);
}

int pa__init(pa_module*m) {
    pa_modargs *ma = NULL;
    struct userdata *u;

    pa_sink *sink;
    pa_sink_input_new_data sink_input_data;

    pa_source *source;
    pa_source_output_new_data source_output_data;

    pa_usec_t sink_input_latency;
    pa_usec_t source_output_latency;
    pa_usec_t actual_latency;
    uint32_t latency; /* parameter */

    pa_sample_spec ss;
    uint32_t nbchannels;

    /* get command line arguments */
    if (!(ma = pa_modargs_new(m->argument, valid_modargs))) {
        pa_log("Failed to parse module arguments");
        goto fail;
    }

    if (!(source = pa_namereg_get(m->core, pa_modargs_get_value(ma, "source", NULL), PA_NAMEREG_SOURCE))) {
        pa_log("No such source.");
        goto fail;
    }

    if (!(sink = pa_namereg_get(m->core, pa_modargs_get_value(ma, "sink", NULL), PA_NAMEREG_SINK))) {
        pa_log("No such sink.");
        goto fail;
    }

    latency = 20; /* 20 ms */
    if (pa_modargs_get_value_u32(ma, "latency", &latency) < 0 || latency < 1 || latency > 2000) {
        pa_log("Invalid latency specification");
        goto fail;
    }

    sink_input_latency = (pa_usec_t)latency;
    sink_input_latency *= 1000;
    
    source_output_latency = (pa_usec_t)latency;
    source_output_latency *= 1000;

    nbchannels = 2;
    if (pa_modargs_get_value_u32(ma, "nbchannels", &latency) < 0 || nbchannels < 1 || nbchannels > 2) {
        pa_log("Invalid number of channels");
        goto fail;
    }


    /* create userdata */
    m->userdata = u = pa_xnew0(struct userdata, 1);
    u->core = m->core;
    u->module = m;
    u->sink_input = NULL;
    u->source_output = NULL;


    /* create sink input*/
    ss.format = PA_SAMPLE_FLOAT32;
    ss.rate = sink->sample_spec.rate;
    ss.channels = nbchannels;


    /* create output memchuck for sink */
    create_memchunk(&u->sink_memchunk, m->core->mempool, ss.rate, latency, nbchannels);
    pa_silence_memchunk(&u->sink_memchunk, &ss);

    pa_sink_input_new_data_init(&sink_input_data);
    sink_input_data.driver = __FILE__;
    sink_input_data.module = m;
    sink_input_data.sink = sink;
    pa_proplist_setf(sink_input_data.proplist, PA_PROP_MEDIA_NAME, "loopback:playback");
    pa_proplist_sets(sink_input_data.proplist, PA_PROP_MEDIA_ROLE, "abstract");
    pa_sink_input_new_data_set_sample_spec(&sink_input_data, &ss);

    pa_sink_input_new(&u->sink_input, m->core, &sink_input_data, 0);
    pa_sink_input_new_data_done(&sink_input_data);


    if (!u->sink_input)
        goto fail;

    u->sink_input->pop = sink_input_pop_cb;
    u->sink_input->process_rewind = sink_input_process_rewind_cb;
    u->sink_input->kill = sink_input_kill_cb;
    u->sink_input->state_change = sink_input_state_change_cb;
    u->sink_input->userdata = u;

    actual_latency = pa_sink_input_set_requested_latency(u->sink_input,sink_input_latency);
    pa_log("sink latency: %ld",(long)actual_latency);

    /* create source output */
    pa_source_output_new_data_init(&source_output_data);
    source_output_data.driver = __FILE__;
    source_output_data.module = m;
    source_output_data.source = source;
    pa_proplist_setf(source_output_data.proplist, PA_PROP_MEDIA_NAME, "loopback:capture");
    pa_proplist_sets(source_output_data.proplist, PA_PROP_MEDIA_ROLE, "abstract");
    pa_source_output_new_data_set_sample_spec(&source_output_data, &ss);

    pa_source_output_new(&u->source_output, m->core, &source_output_data, 0);
    pa_source_output_new_data_done(&source_output_data);

    if (!u->source_output)
        goto fail;

    u->source_output->push = source_output_push_cb;
    u->source_output->process_rewind = source_output_process_rewind_cb;
    u->source_output->kill = source_output_kill_cb;
    u->source_output->state_change = source_output_state_change_cb;
    u->source_output->userdata = u;

    /* latency */
    actual_latency = pa_source_output_set_requested_latency(u->source_output,source_output_latency);
    pa_log("source latency: %ld",(long)actual_latency);

    u->memblockq = pa_memblockq_new(
				    0,                      // idx,	      
				    MEMBLOCKQ_MAXLENGTH,    //  maxlength,     
				    MEMBLOCKQ_MAXLENGTH,    // tlength,	      
				    pa_frame_size(&ss),	    // base,	      
				    10000,		    // prebuf FIXME: this should depend on latency	      
				    0,			    // minreq,	      
				    0,			    // maxrewind,     
				    NULL);                  // silence frame
    
    /* start */
    pa_sink_input_put(u->sink_input);
    pa_source_output_put(u->source_output);

    pa_modargs_free(ma);
    return 0;

fail:
    if (ma)
        pa_modargs_free(ma);

    pa__done(m);
    return -1;
}

void pa__done(pa_module*m) {
    struct userdata *u;

    pa_assert(m);

    if (!(u = m->userdata))
        return;

    if (u->sink_input) {
        pa_sink_input_unlink(u->sink_input);
        pa_sink_input_unref(u->sink_input);
    }

    if (u->source_output) {
        pa_source_output_unlink(u->source_output);
        pa_source_output_unref(u->source_output);
    }

    if (u->sink_memchunk.memblock)
      pa_memblock_unref(u->sink_memchunk.memblock);

   if (u->memblockq)
        pa_memblockq_free(u->memblockq);

    pa_xfree(u);
}
