/*
 * libraw1394 - library for raw access to the 1394 bus with the Linux subsystem.
 *
 * Copyright (C) 1999,2000,2001,2002 Andreas Bombe
 *        new ISO API by Dan Maas
 *
 * This library is licensed under the GNU Lesser General Public License (LGPL),
 * version 2.1 or later. See the file COPYING.LIB in the distribution for
 * details.
 */

#include <config.h>
#include <errno.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <malloc.h>

#include "raw1394.h"
#include "kernel-raw1394.h"
#include "raw1394_private.h"

/* This implements
   x = (x+1) % n
   using a branch based implementation
 */
static inline int increment_and_wrap(int x, int n)
{
  ++x;
  if (x >= n)
    x = 0;
  return x;
}

/* reset the dropped counter each time it is seen */
static unsigned int _iso_dropped(ieee1394handle_t handle)
{
	unsigned int retval = handle->iso_packets_dropped;
	handle->iso_packets_dropped = 0;
	return retval;
}


/* common code for iso_xmit_init and iso_recv_init */
static int do_iso_init(ieee1394handle_t handle,
		       unsigned int buf_packets,
		       unsigned int max_packet_size,
		       int channel,
		       enum raw1394_iso_speed speed,
		       enum raw1394_iso_dma_recv_mode mode,
		       int irq_interval,
		       int cmd)
{
	unsigned int stride;
	int result;

	/* already initialized? */
	if(handle->iso_mode != ISO_INACTIVE)
		return -1;

	/* choose a power-of-two stride for the packet data buffer,
	   so that an even number of packets fits on one page */
	for(stride = 4; stride < max_packet_size; stride *= 2);

	if(stride > getpagesize()) {
		errno = ENOMEM;
		return -1;
	}

	handle->iso_buf_stride = stride;

	handle->iso_status.config.data_buf_size = stride * buf_packets;
	handle->iso_status.config.buf_packets = buf_packets;
	handle->iso_status.config.channel = channel;
	handle->iso_status.config.speed = speed;
	handle->iso_status.config.irq_interval = irq_interval;
	handle->iso_status.config.dma_mode = mode;

	if(ioctl(handle->fd, cmd, &handle->iso_status))
		return -1;

	/* mmap the DMA buffer */
	/* (we assume the kernel sets buf_size to an even number of pages) */
	handle->iso_buffer = mmap(NULL,
				  handle->iso_status.config.data_buf_size,
				  PROT_READ | PROT_WRITE,
				  MAP_SHARED, handle->fd, 0);

	if(handle->iso_buffer == (unsigned char*) MAP_FAILED) {
		handle->iso_buffer = NULL;
		ioctl(handle->fd, RAW1394_IOC_ISO_SHUTDOWN, 0);
		return -1;
	}

	handle->iso_status.overflows = 0;
	handle->iso_packets_dropped = 0;

	handle->iso_xmit_handler = NULL;
	handle->iso_recv_handler = NULL;

	handle->iso_state = ISO_STOP;

	handle->iso_packet_infos = malloc(buf_packets * sizeof(struct raw1394_iso_packet_info));
	if(handle->iso_packet_infos == NULL) {
		munmap(handle->iso_buffer, handle->iso_status.config.data_buf_size);
		handle->iso_buffer = NULL;
		ioctl(handle->fd, RAW1394_IOC_ISO_SHUTDOWN, 0);
		return -1;
	}
#if _POSIX_MEMLOCK > 0
	result = mlock(handle->iso_packet_infos, buf_packets * sizeof(struct raw1394_iso_packet_info));
	/* Ignore a permission error - app is responsible for that, */
	if (result < 0 && result != -EPERM) {
		munmap(handle->iso_buffer, handle->iso_status.config.data_buf_size);
		handle->iso_buffer = NULL;
		ioctl(handle->fd, RAW1394_IOC_ISO_SHUTDOWN, 0);
		return -1;
	}
#endif
	return 0;
}

int ieee1394_iso_xmit_init(ieee1394handle_t handle,
			  raw1394_iso_xmit_handler_t handler,
			  unsigned int buf_packets,
			  unsigned int max_packet_size,
			  unsigned char channel,
			  enum raw1394_iso_speed speed,
			  int irq_interval)
{
	if (do_iso_init(handle, buf_packets, max_packet_size, channel, speed, RAW1394_DMA_DEFAULT,
		       irq_interval, RAW1394_IOC_ISO_XMIT_INIT))
		return -1;

	handle->iso_mode = ISO_XMIT;
	handle->iso_xmit_handler = handler;
	handle->next_packet = 0;

	return 0;
}

int ieee1394_iso_recv_init(ieee1394handle_t handle,
			  raw1394_iso_recv_handler_t handler,
			  unsigned int buf_packets,
			  unsigned int max_packet_size,
			  unsigned char channel,
			  enum raw1394_iso_dma_recv_mode mode,
			  int irq_interval)
{
	/* any speed will work */
	if (do_iso_init(handle, buf_packets, max_packet_size, channel, RAW1394_ISO_SPEED_100, mode,
		       irq_interval, RAW1394_IOC_ISO_RECV_INIT))
		return -1;

	handle->iso_mode = ISO_RECV;
	handle->iso_recv_handler = handler;
	return 0;
}

int ieee1394_iso_multichannel_recv_init(ieee1394handle_t handle,
				       raw1394_iso_recv_handler_t handler,
				       unsigned int buf_packets,
				       unsigned int max_packet_size,
				       int irq_interval)
{
	/* any speed will work */
	if (do_iso_init(handle, buf_packets, max_packet_size, -1, RAW1394_ISO_SPEED_100,
			RAW1394_DMA_BUFFERFILL,
		       irq_interval, RAW1394_IOC_ISO_RECV_INIT))
		return -1;

	handle->iso_mode = ISO_RECV;
	handle->iso_recv_handler = handler;
	return 0;
}

int ieee1394_iso_recv_listen_channel(ieee1394handle_t handle, unsigned char channel)
{
	if (handle->iso_mode != ISO_RECV) {
		errno = EINVAL;
		return -1;
	}

	return ioctl(handle->fd, RAW1394_IOC_ISO_RECV_LISTEN_CHANNEL, channel);
}

int ieee1394_iso_recv_unlisten_channel(ieee1394handle_t handle, unsigned char channel)
{
	if (handle->iso_mode != ISO_RECV) {
		errno = EINVAL;
		return -1;
	}

	return ioctl(handle->fd, RAW1394_IOC_ISO_RECV_UNLISTEN_CHANNEL, channel);
}

int ieee1394_iso_recv_flush(ieee1394handle_t handle)
{
	if (handle->iso_mode != ISO_RECV) {
		errno = EINVAL;
		return -1;
	}

	return ioctl(handle->fd, RAW1394_IOC_ISO_RECV_FLUSH, 0);
}

int ieee1394_iso_recv_set_channel_mask(ieee1394handle_t handle, u_int64_t mask)
{
	if (handle->iso_mode != ISO_RECV) {
		errno = EINVAL;
		return -1;
	}

	return ioctl(handle->fd, RAW1394_IOC_ISO_RECV_SET_CHANNEL_MASK, (void*) &mask);
}

int ieee1394_iso_recv_start(ieee1394handle_t handle, int start_on_cycle, int tag_mask, int sync)
{
	int args[3];

	if(handle->iso_mode != ISO_RECV) {
		errno = EINVAL;
		return -1;
	}

	args[0] = start_on_cycle;
	args[1] = tag_mask;
	args[2] = sync;

	if(ioctl(handle->fd, RAW1394_IOC_ISO_RECV_START, &args[0]))
		return -1;

	handle->iso_state = ISO_GO;
	return 0;
}

static int _iso_xmit_queue_packets(raw1394handle_t handle)
{
	ieee1394handle_t ihandle = handle->mode.ieee1394;
	struct raw1394_iso_status *stat = &ihandle->iso_status;
	struct raw1394_iso_packets packets;
	int retval = -1;
	int stop_sync = 0;

	if(ihandle->iso_mode != ISO_XMIT) {
		errno = EINVAL;
		goto out;
	}

	/* ensure stat->n_packets is sane */
	if (stat->n_packets > stat->config.buf_packets)
		stat->n_packets = stat->config.buf_packets;

	/* we could potentially send up to stat->n_packets packets */
	packets.n_packets = 0;
	packets.infos = ihandle->iso_packet_infos;
	if(packets.infos == NULL)
		goto out;

	while(stat->n_packets > 1) {
		enum raw1394_iso_disposition disp;
		unsigned int len;
		
		struct raw1394_iso_packet_info *info = &packets.infos[packets.n_packets];

		info->offset = ihandle->iso_buf_stride * ihandle->next_packet;
		
		/* call handler */
		disp = ihandle->iso_xmit_handler(handle,
						ihandle->iso_buffer + info->offset,
						&len,
						&info->tag, &info->sy,
						stat->xmit_cycle,
						_iso_dropped(ihandle));
		info->len = len;
		
		/* advance packet cursors and cycle counter */
		stat->n_packets--;
		ihandle->next_packet = increment_and_wrap(ihandle->next_packet, stat->config.buf_packets);
		if(stat->xmit_cycle != -1)
			stat->xmit_cycle = increment_and_wrap(stat->xmit_cycle, 8000);
		packets.n_packets++;

		if(disp == RAW1394_ISO_DEFER) {
			/* queue an event so that we don't hang in the next read() */
			if(ioctl(ihandle->fd, RAW1394_IOC_ISO_QUEUE_ACTIVITY, 0))
				goto out_produce;
			break;
		} else if(disp == RAW1394_ISO_AGAIN) {
			/* the last packet was not ready, decrement counter */
			packets.n_packets--;
			
			/* queue an event so that we don't hang in the next read() */
			if(ioctl(ihandle->fd, RAW1394_IOC_ISO_QUEUE_ACTIVITY, 0))
				goto out_produce;
			break;
		} else if(disp == RAW1394_ISO_STOP) {
			stop_sync = 1;
			break;
		} else if(disp == RAW1394_ISO_STOP_NOSYNC) {
			ieee1394_iso_stop(ihandle);
			break;
		} else if(disp == RAW1394_ISO_ERROR) {
			goto out_produce;
		}
	}

	/* success */
	retval = 0;

out_produce:
	if(packets.n_packets > 0) {
		if(ioctl(ihandle->fd, RAW1394_IOC_ISO_XMIT_PACKETS, &packets))
			retval = -1;
	}
out:
	if(stop_sync) {
		if(ieee1394_iso_xmit_sync(ihandle))
			return -1;
		ieee1394_iso_stop(ihandle);
	}
	
	return retval;
}

int ieee1394_iso_xmit_write(raw1394handle_t handle, unsigned char *data, unsigned int len,
			   unsigned char tag, unsigned char sy)
{
	ieee1394handle_t ihandle = handle->mode.ieee1394;
	struct raw1394_iso_status *stat = &ihandle->iso_status;
	struct raw1394_iso_packets packets;
	struct raw1394_iso_packet_info info;

	if(ihandle->iso_mode != ISO_XMIT || ihandle->iso_xmit_handler != NULL) {
		errno = EINVAL;
		return -1;
	}

	/* wait until buffer space is available */
	while(ihandle->iso_status.n_packets <= 1) {
		/* if the file descriptor has been set non-blocking,
		   return immediately */
		if(fcntl(ihandle->fd, F_GETFL) & O_NONBLOCK) {
			errno = EAGAIN;
			return -1;
		}
			
		if(ieee1394_loop_iterate(handle)) {
			return -1;
		}
	}

	/* copy the data to the packet buffer */
	info.offset = ihandle->next_packet * ihandle->iso_buf_stride;
	info.len = len;
	info.tag = tag;
	info.sy = sy;
	
	memcpy(ihandle->iso_buffer + info.offset, data, len);
	
	packets.n_packets = 1;
	packets.infos = &info;

	if(ioctl(ihandle->fd, RAW1394_IOC_ISO_XMIT_PACKETS, &packets))
		return -1;

	stat->n_packets--;
	ihandle->next_packet = increment_and_wrap(ihandle->next_packet, stat->config.buf_packets);
	if(stat->xmit_cycle != -1)
		stat->xmit_cycle = increment_and_wrap(stat->xmit_cycle, 8000);

	return 0;
}

int ieee1394_iso_xmit_start(ieee1394handle_t handle, int start_on_cycle, int prebuffer_packets)
{
	int args[2];

	if(handle->iso_mode != ISO_XMIT) {
		errno = EINVAL;
		return -1;
	}

	args[0] = start_on_cycle;
	args[1] = prebuffer_packets;

	if(ioctl(handle->fd, RAW1394_IOC_ISO_XMIT_START, &args[0]))
		return -1;

	handle->iso_state = ISO_GO;
	return 0;
}

int ieee1394_iso_xmit_sync(ieee1394handle_t handle)
{
	if(handle->iso_mode != ISO_XMIT) {
		errno = EINVAL;
		return -1;
	}
	return ioctl(handle->fd, RAW1394_IOC_ISO_XMIT_SYNC, 0);
}

void ieee1394_iso_stop(ieee1394handle_t handle)
{
	if(handle->iso_mode == ISO_INACTIVE) {
		return;
	}

	ioctl(handle->fd, RAW1394_IOC_ISO_XMIT_RECV_STOP, 0);
	handle->iso_state = ISO_STOP;
}

void ieee1394_iso_shutdown(ieee1394handle_t handle)
{
	if(handle->iso_buffer) {
		munmap(handle->iso_buffer, handle->iso_status.config.data_buf_size);
		handle->iso_buffer = NULL;
	}
	
	if(handle->iso_mode != ISO_INACTIVE) {
		ieee1394_iso_stop(handle);
		ioctl(handle->fd, RAW1394_IOC_ISO_SHUTDOWN, 0);
	}

	if(handle->iso_packet_infos) {
#if _POSIX_MEMLOCK > 0
		munlock(handle->iso_packet_infos,
			handle->iso_status.config.buf_packets * 
			sizeof(struct raw1394_iso_packet_info));
#endif
		free(handle->iso_packet_infos);
		handle->iso_packet_infos = NULL;
	}

	handle->iso_mode = ISO_INACTIVE;
}

int ieee1394_read_cycle_timer(ieee1394handle_t handle,
			     u_int32_t *cycle_timer, u_int64_t *local_time)
{
	int err;
	struct raw1394_cycle_timer ctr = { 0 };

	err = ioctl(handle->fd, RAW1394_IOC_GET_CYCLE_TIMER, &ctr);
	if (!err) {
		*cycle_timer = ctr.cycle_timer;
		*local_time  = ctr.local_time;
	}
	return err;
}

static int _iso_recv_packets(raw1394handle_t handle)
{
	ieee1394handle_t ihandle = handle->mode.ieee1394;
	struct raw1394_iso_status *stat = &ihandle->iso_status;
	struct raw1394_iso_packets packets;

	int retval = -1, packets_done = 0;

	if(ihandle->iso_mode != ISO_RECV) {
		errno = EINVAL;
		return -1;
	}
	
	/* ask the kernel to fill an array with packet info structs */
	packets.n_packets = stat->n_packets;
	packets.infos = ihandle->iso_packet_infos;
	if(packets.infos == NULL)
		goto out;

	if(ioctl(ihandle->fd, RAW1394_IOC_ISO_RECV_PACKETS, &packets) < 0)
		goto out;

	while(stat->n_packets > 0) {
		struct raw1394_iso_packet_info *info;
		enum raw1394_iso_disposition disp;

		info = &packets.infos[packets_done];

		/* call handler */
		disp = ihandle->iso_recv_handler(handle,
						ihandle->iso_buffer + info->offset,
						info->len, info->channel,
						info->tag, info->sy,
						info->cycle,
						_iso_dropped(ihandle));

		/* advance packet cursors */
		stat->n_packets--;
		packets_done++;
		
		if(disp == RAW1394_ISO_DEFER) {
			/* queue an event so that we don't hang in the next read() */
			if(ioctl(ihandle->fd, RAW1394_IOC_ISO_QUEUE_ACTIVITY, 0))
				goto out_consume;
			break;
		} else if(disp == RAW1394_ISO_STOP || disp == RAW1394_ISO_STOP_NOSYNC) {
			ieee1394_iso_stop(ihandle);
			break;
		} else if(disp == RAW1394_ISO_ERROR) {
			goto out_consume;
		}
	}

	/* success */
	retval = 0;

out_consume:
	if(packets_done > 0) {
		if(ioctl(ihandle->fd, RAW1394_IOC_ISO_RECV_RELEASE_PACKETS, packets_done))
			retval = -1;
	}
out:	
	return retval;
}

/* run the ISO state machine; called from raw1394_loop_iterate()  */
int _ieee1394_iso_iterate(raw1394handle_t handle)
{
	ieee1394handle_t ihandle = handle->mode.ieee1394;
	int err;

	if(ihandle->iso_mode == ISO_INACTIVE)
		return 0;

	err = ioctl(ihandle->fd, RAW1394_IOC_ISO_GET_STATUS, &ihandle->iso_status);
	if(err != 0)
		return err;

	ihandle->iso_packets_dropped += ihandle->iso_status.overflows;

	if(ihandle->iso_state == ISO_GO) {
		if(ihandle->iso_mode == ISO_XMIT) {
			if(ihandle->iso_xmit_handler) {
				return _iso_xmit_queue_packets(handle);
			}
		}

		if(ihandle->iso_mode == ISO_RECV) {
			if(ihandle->iso_recv_handler) {
				return _iso_recv_packets(handle);
			}
		}
	}

	return 0;
}
