/*
    Copyright (C) 2001 Paul Davis
    Copyright (C) 2005 Karsten Wiese, Rui Nuno Capela

    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.

    This program 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 General Public License
    along with this program; if not, write to the Free Software
    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.

*/

#include "hardware.h"
#include "alsa_driver.h"
#include "usx2y.h"
#include <sys/mman.h>

#ifndef ARRAY_SIZE
#define ARRAY_SIZE(x) (sizeof(x) / sizeof((x)[0]))
#endif

//#define DBGHWDEP

#ifdef DBGHWDEP
int dbg_offset;
char dbg_buffer[8096];
#endif
static
int usx2y_set_input_monitor_mask (jack_hardware_t *hw, unsigned long mask)
{
	return -1;
}

static
int usx2y_change_sample_clock (jack_hardware_t *hw, SampleClockMode mode)
{
	return -1;
}

static void
usx2y_release (jack_hardware_t *hw)
{
	usx2y_t *h = (usx2y_t *) hw->private_hw;

	if (h == 0)
		return;

	if (h->hwdep_handle)
		snd_hwdep_close(h->hwdep_handle);

	free(h);
}

static int
usx2y_driver_get_channel_addresses_playback (alsa_driver_t *driver,
					snd_pcm_uframes_t *playback_avail)
{
	channel_t chn;
	int iso;
	snd_pcm_uframes_t playback_iso_avail;
	char *playback;

	usx2y_t *h = (usx2y_t *) driver->hw->private_hw;

	if (0 > h->playback_iso_start) {
		int bytes = driver->playback_sample_bytes * 2 * driver->frames_per_cycle *
			driver->user_nperiods;
		iso = h->hwdep_pcm_shm->playback_iso_start;
		if (0 > iso)
			return 0; /* FIXME: return -1; */
		if (++iso >= ARRAY_SIZE(h->hwdep_pcm_shm->captured_iso))
			iso = 0;
		while((bytes -= h->hwdep_pcm_shm->captured_iso[iso].length) > 0)
			if (++iso >= ARRAY_SIZE(h->hwdep_pcm_shm->captured_iso))
				iso = 0;
		h->playback_iso_bytes_done = h->hwdep_pcm_shm->captured_iso[iso].length + bytes;
#ifdef DBGHWDEP
		dbg_offset = sprintf(dbg_buffer, "first iso = %i %i@%p:%i\n",
					iso, h->hwdep_pcm_shm->captured_iso[iso].length,
					h->hwdep_pcm_shm->playback,
					h->hwdep_pcm_shm->captured_iso[iso].offset);
#endif
	} else {
		iso = h->playback_iso_start;
	}
#ifdef DBGHWDEP
	dbg_offset += sprintf(dbg_buffer + dbg_offset, "iso = %i(%i;%i); ", iso,
				h->hwdep_pcm_shm->captured_iso[iso].offset,
				h->hwdep_pcm_shm->captured_iso[iso].frame);
#endif
	playback = h->hwdep_pcm_shm->playback +
		h->hwdep_pcm_shm->captured_iso[iso].offset +
		h->playback_iso_bytes_done;
	playback_iso_avail = (h->hwdep_pcm_shm->captured_iso[iso].length -
		h->playback_iso_bytes_done) /
		(driver->playback_sample_bytes * 2);
	if (*playback_avail >= playback_iso_avail) {
		*playback_avail = playback_iso_avail;
		if (++iso >= ARRAY_SIZE(h->hwdep_pcm_shm->captured_iso))
			iso = 0;
		h->playback_iso_bytes_done = 0;
	} else
		h->playback_iso_bytes_done =
			*playback_avail * (driver->playback_sample_bytes * 2);
	h->playback_iso_start = iso;
	for (chn = 0; chn < driver->playback_nchannels; chn++) {
		const snd_pcm_channel_area_t *a = &driver->playback_areas[chn];
		driver->playback_addr[chn] = playback + a->first / 8;
	}
#ifdef DBGHWDEP
	if (dbg_offset < (sizeof(dbg_buffer) - 256))
		dbg_offset += sprintf(dbg_buffer + dbg_offset, "avail %li@%p\n", *playback_avail, driver->playback_addr[0]);
	else {
		printf(dbg_buffer);
		return -1;
	}
#endif

	return 0;
}

static int
usx2y_driver_get_channel_addresses_capture (alsa_driver_t *driver,
					snd_pcm_uframes_t *capture_avail)
{
	channel_t chn;
	int iso;
	snd_pcm_uframes_t capture_iso_avail;
	int capture_offset;

	usx2y_t *h = (usx2y_t *) driver->hw->private_hw;

	if (0 > h->capture_iso_start) {
		iso = h->hwdep_pcm_shm->capture_iso_start;
		if (0 > iso)
			return 0; /* FIXME: return -1; */
		h->capture_iso_bytes_done = 0;
#ifdef DBGHWDEP
		dbg_offset = sprintf(dbg_buffer, "cfirst iso = %i %i@%p:%i\n",
					iso, h->hwdep_pcm_shm->captured_iso[iso].length,
					h->hwdep_pcm_shm->capture0x8,
					h->hwdep_pcm_shm->captured_iso[iso].offset);
#endif
	} else {
		iso = h->capture_iso_start;
	}
#ifdef DBGHWDEP
	dbg_offset += sprintf(dbg_buffer + dbg_offset, "ciso = %i(%i;%i); ", iso,
				h->hwdep_pcm_shm->captured_iso[iso].offset,
				h->hwdep_pcm_shm->captured_iso[iso].frame);
#endif
	capture_offset =
		h->hwdep_pcm_shm->captured_iso[iso].offset +
			h->capture_iso_bytes_done;
	capture_iso_avail = (h->hwdep_pcm_shm->captured_iso[iso].length -
		h->capture_iso_bytes_done) /
		(driver->capture_sample_bytes * 2);
	if (*capture_avail >= capture_iso_avail) {
		*capture_avail = capture_iso_avail;
		if (++iso >= ARRAY_SIZE(h->hwdep_pcm_shm->captured_iso))
			iso = 0;
		h->capture_iso_bytes_done = 0;
	} else
		h->capture_iso_bytes_done =
			*capture_avail * (driver->capture_sample_bytes * 2);
	h->capture_iso_start = iso;
	for (chn = 0; chn < driver->capture_nchannels; chn++) {
		driver->capture_addr[chn] =
			(chn < 2 ? h->hwdep_pcm_shm->capture0x8 : h->hwdep_pcm_shm->capture0xA)
			+ capture_offset +
			((chn & 1) ? driver->capture_sample_bytes : 0);
	}
#ifdef DBGHWDEP
 {
	int f = 0;
	unsigned *u = driver->capture_addr[0];
	static unsigned last;
	dbg_offset += sprintf(dbg_buffer + dbg_offset, "\nvon %6u  bis %6u\n", last, u[0]);
	while (f < *capture_avail && dbg_offset < (sizeof(dbg_buffer) - 256)) {
		if (u[f] != last + 1)
			 dbg_offset += sprintf(dbg_buffer + dbg_offset, "\nooops %6u  %6u\n", last, u[f]);
		last = u[f++];
	}
 }
	if (dbg_offset < (sizeof(dbg_buffer) - 256))
		dbg_offset += sprintf(dbg_buffer + dbg_offset, "avail %li@%p\n", *capture_avail, driver->capture_addr[0]);
	else {
		printf(dbg_buffer);
		return -1;
	}
#endif

	return 0;
}

static int
usx2y_driver_start (alsa_driver_t *driver)
{
	int err, i;
	snd_pcm_uframes_t poffset, pavail;

	usx2y_t *h = (usx2y_t *) driver->hw->private_hw;

	for (i = 0; i < driver->capture_nchannels; i++)
		// US428 channels 3+4 are on a separate 2 channel stream.
		// ALSA thinks its 1 stream with 4 channels.
		driver->capture_interleave_skip[i] = 2 * driver->capture_sample_bytes;


	driver->playback_interleave_skip[0] = 2 * driver->playback_sample_bytes;
	driver->playback_interleave_skip[1] = 2 * driver->playback_sample_bytes;

	driver->poll_last = 0;
	driver->poll_next = 0;

	if ((err = snd_pcm_prepare (driver->playback_handle)) < 0) {
		jack_error ("ALSA/USX2Y: prepare error for playback: %s", snd_strerror(err));
		return -1;
	}

	if (driver->midi && !driver->xrun_recovery)
		(driver->midi->start)(driver->midi);

	if (driver->playback_handle) {
/* 		int i, j; */
/* 		char buffer[2000]; */
		h->playback_iso_start =
			h->capture_iso_start = -1;
		snd_hwdep_poll_descriptors(h->hwdep_handle, &h->pfds, 1);
		h->hwdep_pcm_shm = (snd_usX2Y_hwdep_pcm_shm_t*)
			mmap(NULL, sizeof(snd_usX2Y_hwdep_pcm_shm_t),
			     PROT_READ,
			     MAP_SHARED, h->pfds.fd,
			     0);
		if (MAP_FAILED == h->hwdep_pcm_shm) {
			perror("ALSA/USX2Y: mmap");
			return -1;
		}
		if (mprotect(h->hwdep_pcm_shm->playback,
					sizeof(h->hwdep_pcm_shm->playback),
					PROT_READ|PROT_WRITE)) {
			perror("ALSA/USX2Y: mprotect");
			return -1;
		}
		memset(h->hwdep_pcm_shm->playback, 0, sizeof(h->hwdep_pcm_shm->playback));
/* 		for (i = 0, j = 0; i < 2000;) { */
/* 			j += sprintf(buffer + j, "%04hX ", */
/* 				     *(unsigned short*)(h->hwdep_pcm_shm->capture + i)); */
/* 			if (((i += 2) % 32) == 0) { */
/* 				jack_error(buffer); */
/* 				j = 0; */
/* 			} */
/* 		} */
	}

	if (driver->hw_monitoring) {
		driver->hw->set_input_monitor_mask (driver->hw,
						    driver->input_monitor_mask);
	}

	if (driver->playback_handle) {
		/* fill playback buffer with zeroes, and mark
		   all fragments as having data.
		*/

		pavail = snd_pcm_avail_update (driver->playback_handle);

		if (pavail != driver->frames_per_cycle * driver->playback_nperiods) {
			jack_error ("ALSA/USX2Y: full buffer not available at start");
			return -1;
		}

		if (snd_pcm_mmap_begin(
					driver->playback_handle,
					&driver->playback_areas,
					&poffset, &pavail) < 0) {
			return -1;
		}

		/* XXX this is cheating. ALSA offers no guarantee that
		   we can access the entire buffer at any one time. It
		   works on most hardware tested so far, however, buts
		   its a liability in the long run. I think that
		   alsa-lib may have a better function for doing this
		   here, where the goal is to silence the entire
		   buffer.
		*/
		{
/* 			snd_pcm_uframes_t frag, nframes = driver->buffer_frames; */
/* 			while (nframes) { */
/* 				frag = nframes; */
/* 				if (usx2y_driver_get_channel_addresses_playback(driver, &frag) < 0) */
/* 					return -1; */

/* 				for (chn = 0; chn < driver->playback_nchannels; chn++) */
/* 					alsa_driver_silence_on_channel (driver, chn, frag); */
/* 				nframes -= frag; */
/* 			} */
		}

		snd_pcm_mmap_commit (driver->playback_handle, poffset,
						driver->user_nperiods * driver->frames_per_cycle);

		if ((err = snd_pcm_start (driver->playback_handle)) < 0) {
			jack_error ("ALSA/USX2Y: could not start playback (%s)",
				    snd_strerror (err));
			return -1;
		}
	}

	if (driver->hw_monitoring &&
	    (driver->input_monitor_mask || driver->all_monitor_in)) {
		if (driver->all_monitor_in) {
			driver->hw->set_input_monitor_mask (driver->hw, ~0U);
		} else {
			driver->hw->set_input_monitor_mask (
				driver->hw, driver->input_monitor_mask);
		}
	}

	driver->playback_nfds =	snd_pcm_poll_descriptors_count (driver->playback_handle);
	driver->capture_nfds = snd_pcm_poll_descriptors_count (driver->capture_handle);

	if (driver->pfd) {
		free (driver->pfd);
	}

	driver->pfd = (struct pollfd *)
		malloc (sizeof (struct pollfd) *
			(driver->playback_nfds + driver->capture_nfds + 2));

	return 0;
}

static int
usx2y_driver_stop (alsa_driver_t *driver)
{
	int err;
	JSList* node;
	int chn;

	usx2y_t *h = (usx2y_t *) driver->hw->private_hw;

	/* silence all capture port buffers, because we might
	   be entering offline mode.
	*/

	for (chn = 0, node = driver->capture_ports; node;
		node = jack_slist_next (node), chn++) {

		jack_port_t* port;
		char* buf;
		jack_nframes_t nframes = driver->engine->control->buffer_size;

		port = (jack_port_t *) node->data;
		buf = jack_port_get_buffer (port, nframes);
		memset (buf, 0, sizeof (jack_default_audio_sample_t) * nframes);
	}

	if (driver->playback_handle) {
		if ((err = snd_pcm_drop (driver->playback_handle)) < 0) {
			jack_error ("ALSA/USX2Y: channel flush for playback "
					"failed (%s)", snd_strerror (err));
			return -1;
		}
	}

	if (driver->hw_monitoring) {
		driver->hw->set_input_monitor_mask (driver->hw, 0);
	}

	munmap(h->hwdep_pcm_shm, sizeof(snd_usX2Y_hwdep_pcm_shm_t));

	if (driver->midi && !driver->xrun_recovery)
		(driver->midi->stop)(driver->midi);

	return 0;
}

static int
usx2y_driver_null_cycle (alsa_driver_t* driver, jack_nframes_t nframes)
{
	jack_nframes_t nf;
	snd_pcm_uframes_t offset;
	snd_pcm_uframes_t contiguous, contiguous_;
	int chn;

	VERBOSE(driver->engine,
		"usx2y_driver_null_cycle (%p, %i)", driver, nframes);

	if (driver->capture_handle) {
		nf = nframes;
		offset = 0;
		while (nf) {

			contiguous = (nf > driver->frames_per_cycle) ?
				driver->frames_per_cycle : nf;

			if (snd_pcm_mmap_begin (
					driver->capture_handle,
					&driver->capture_areas,
					(snd_pcm_uframes_t *) &offset,
					(snd_pcm_uframes_t *) &contiguous)) {
				return -1;
			}
			contiguous_ = contiguous;
			while (contiguous_) {
				snd_pcm_uframes_t frag = contiguous_;
				if (usx2y_driver_get_channel_addresses_capture(driver, &frag) < 0)
					return -1;
				contiguous_ -= frag;
			}

			if (snd_pcm_mmap_commit (driver->capture_handle,
						offset, contiguous) < 0) {
				return -1;
			}

			nf -= contiguous;
		}
	}

	if (driver->playback_handle) {
		nf = nframes;
		offset = 0;
		while (nf) {
			contiguous = (nf > driver->frames_per_cycle) ?
				driver->frames_per_cycle : nf;

			if (snd_pcm_mmap_begin (
				    driver->playback_handle,
				    &driver->playback_areas,
				    (snd_pcm_uframes_t *) &offset,
				    (snd_pcm_uframes_t *) &contiguous)) {
				return -1;
			}

			{
				snd_pcm_uframes_t frag, nframes = contiguous;
				while (nframes) {
					frag = nframes;
					if (usx2y_driver_get_channel_addresses_playback(driver, &frag) < 0)
						return -1;
					for (chn = 0; chn < driver->playback_nchannels; chn++)
						alsa_driver_silence_on_channel (driver, chn, frag);
					nframes -= frag;
				}
			}

			if (snd_pcm_mmap_commit (driver->playback_handle,
						offset, contiguous) < 0) {
				return -1;
			}

			nf -= contiguous;
		}
	}

	return 0;
}

static int
usx2y_driver_read (alsa_driver_t *driver, jack_nframes_t nframes)
{
	snd_pcm_uframes_t contiguous;
	snd_pcm_sframes_t nread;
	snd_pcm_uframes_t offset;
	jack_default_audio_sample_t* buf[4];
	channel_t chn;
	JSList *node;
	jack_port_t* port;
	int err;
	snd_pcm_uframes_t nframes_ = nframes;

	if (!driver->capture_handle || driver->engine->freewheeling) {
		return 0;
	}

    if (driver->midi)
        (driver->midi->read)(driver->midi, nframes);

	nread = 0;

	if (snd_pcm_mmap_begin (driver->capture_handle,
				&driver->capture_areas,
				&offset, &nframes_) < 0) {
		jack_error ("ALSA/USX2Y: %s: mmap areas info error",
			    driver->alsa_name_capture);
		return -1;
	}

	for (chn = 0, node = driver->capture_ports;
	     node; node = jack_slist_next (node), chn++) {
		port = (jack_port_t *) node->data;
		if (!jack_port_connected (port)) {
			continue;
		}
		buf[chn] = jack_port_get_buffer (port, nframes_);
	}

	while (nframes) {

		contiguous = nframes;
		if (usx2y_driver_get_channel_addresses_capture (
			    driver, &contiguous) < 0) {
			return -1;
		}
		for (chn = 0, node = driver->capture_ports;
		     node; node = jack_slist_next (node), chn++) {
			port = (jack_port_t *) node->data;
			if (!jack_port_connected (port)) {
				/* no-copy optimization */
				continue;
			}
			alsa_driver_read_from_channel (driver, chn,
						       buf[chn] + nread,
						       contiguous);
/* 			sample_move_dS_s24(buf[chn] + nread, */
/* 					   driver->capture_addr[chn], */
/* 					   contiguous, */
/* 					   driver->capture_interleave_skip); */
		}
		nread += contiguous;
		nframes -= contiguous;
	}

	if ((err = snd_pcm_mmap_commit (driver->capture_handle,
					offset, nframes_)) < 0) {
		jack_error ("ALSA/USX2Y: could not complete read of %"
			    PRIu32 " frames: error = %d", nframes_, err);
		return -1;
	}

	return 0;
}

static int
usx2y_driver_write (alsa_driver_t* driver, jack_nframes_t nframes)
{
	channel_t chn;
	JSList *node;
	jack_default_audio_sample_t* buf[2];
	snd_pcm_sframes_t nwritten;
	snd_pcm_uframes_t contiguous;
	snd_pcm_uframes_t offset;
	jack_port_t *port;
	int err;
	snd_pcm_uframes_t nframes_ = nframes;

	driver->process_count++;

	if (!driver->playback_handle || driver->engine->freewheeling) {
		return 0;
	}

    if (driver->midi)
        (driver->midi->write)(driver->midi, nframes);

	nwritten = 0;

	/* check current input monitor request status */

	driver->input_monitor_mask = 0;

	for (chn = 0, node = driver->capture_ports; node;
		node = jack_slist_next (node), chn++) {
		if (((jack_port_t *) node->data)->shared->monitor_requests) {
			driver->input_monitor_mask |= (1<<chn);
		}
	}

	if (driver->hw_monitoring) {
		if ((driver->hw->input_monitor_mask
			!= driver->input_monitor_mask)
			&& !driver->all_monitor_in) {
			driver->hw->set_input_monitor_mask (
				driver->hw, driver->input_monitor_mask);
		}
	}

	if (snd_pcm_mmap_begin(driver->playback_handle,
			       &driver->playback_areas,
			       &offset, &nframes_) < 0) {
		jack_error ("ALSA/USX2Y: %s: mmap areas info error",
			    driver->alsa_name_capture);
		return -1;
	}

	for (chn = 0, node = driver->playback_ports;
	     node; node = jack_slist_next (node), chn++) {
		port = (jack_port_t *) node->data;
		buf[chn] = jack_port_get_buffer (port, nframes_);
	}

	while (nframes) {

		contiguous = nframes;
		if (usx2y_driver_get_channel_addresses_playback (
			    driver, &contiguous) < 0) {
			return -1;
		}
		for (chn = 0, node = driver->playback_ports;
		     node; node = jack_slist_next (node), chn++) {
			port = (jack_port_t *) node->data;
			alsa_driver_write_to_channel (driver, chn,
						      buf[chn] + nwritten,
						      contiguous);
		}
		nwritten += contiguous;
		nframes -= contiguous;
	}

	if ((err = snd_pcm_mmap_commit (driver->playback_handle,
					offset, nframes_)) < 0) {
		jack_error ("ALSA/USX2Y: could not complete playback of %"
			    PRIu32 " frames: error = %d", nframes_, err);
		if (err != -EPIPE && err != -ESTRPIPE)
			return -1;
	}

	return 0;
}

static void
usx2y_driver_setup (alsa_driver_t *driver)
{
	driver->nt_start = (JackDriverNTStartFunction) usx2y_driver_start;
	driver->nt_stop = (JackDriverNTStopFunction) usx2y_driver_stop;
	driver->read = (JackDriverReadFunction) usx2y_driver_read;
	driver->write = (JackDriverReadFunction) usx2y_driver_write;
	driver->null_cycle =
		(JackDriverNullCycleFunction) usx2y_driver_null_cycle;
}

jack_hardware_t *
jack_alsa_usx2y_hw_new (alsa_driver_t *driver)
{
	jack_hardware_t *hw;
	usx2y_t *h;

	int   hwdep_cardno;
    int   hwdep_devno;
	char *hwdep_colon;
	char  hwdep_name[9];
	snd_hwdep_t *hwdep_handle;

    hw = (jack_hardware_t *) malloc (sizeof (jack_hardware_t));

	hw->capabilities = 0;
	hw->input_monitor_mask = 0;
	hw->private_hw = 0;

	hw->set_input_monitor_mask = usx2y_set_input_monitor_mask;
	hw->change_sample_clock = usx2y_change_sample_clock;
	hw->release = usx2y_release;

	/* Derive the special USB US-X2Y hwdep pcm device name from
	 * the playback one, thus allowing the use of the "rawusb"
	 * experimental stuff if, and only if, the "hw:n,2" device
	 * name is specified. Otherwise, fallback to generic backend.
	 */
	hwdep_handle = NULL;
	hwdep_cardno = hwdep_devno = 0;
	if ((hwdep_colon = strrchr(driver->alsa_name_playback, ':')) != NULL)
		sscanf(hwdep_colon, ":%d,%d", &hwdep_cardno, &hwdep_devno);
	if (hwdep_devno == 2) {
		snprintf(hwdep_name, sizeof(hwdep_name), "hw:%d,1", hwdep_cardno);
		if (snd_hwdep_open (&hwdep_handle, hwdep_name, O_RDWR) < 0) {
			jack_error ("ALSA/USX2Y: Cannot open hwdep device \"%s\"", hwdep_name);
		} else {
			/* Allocate specific USX2Y hwdep pcm struct. */
			h = (usx2y_t *) malloc (sizeof (usx2y_t));
			h->driver = driver;
			h->hwdep_handle = hwdep_handle;
			hw->private_hw = h;
			/* Set our own operational function pointers. */
			usx2y_driver_setup(driver);
			jack_info("ALSA/USX2Y: EXPERIMENTAL hwdep pcm device %s"
				" (aka \"rawusb\")", driver->alsa_name_playback);
		}
	}

	return hw;
}
