#define _FILE_OFFSET_BITS 64

#include <linux/dvb/frontend.h>
#include <linux/dvb/dmx.h>
#include <linux/dvb/audio.h>
#include <linux/dvb/version.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
#include <stdbool.h>
#include <string.h>
#include <ctype.h>
#include <unistd.h>

#define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0]))
#ifndef DTV_STREAM_ID
#define DTV_STREAM_ID DTV_ISDBS_TS_ID
#endif
#define DVB_READ_BUFFER_SIZE 188 * 128
#define DVB_WRITE_BUFFER_SIZE 1024 * 1024
#define DMX_BUFFER_SIZE 1024 * 1024
#define RING_BUFFER_SIZE 1024 * 1024 * 16

typedef struct {
	void* rb_ptr;
	int rb_wt;
	int rb_rt;
	int rb_ab;
	pthread_mutex_t rb_mtx;
} mrb;

typedef struct {
	int fin;
	int fout;
	int rectime;
	mrb rb;
	bool done;
} my_thread_arg;

void * record_read(void * priv);
void * record_write(void * priv);

// TODO: switchable between VRB(virtual ring buffer) and MRB(My Ring Buffer)

void mrb_init(mrb* rb, size_t size) {
	rb->rb_ptr = malloc(size);
	rb->rb_wt = rb->rb_rt = rb->rb_ab = 0;
	pthread_mutex_init(&rb->rb_mtx, NULL);
}

void mrb_destroy(mrb* rb) {
	free(rb->rb_ptr);
	pthread_mutex_destroy(&rb->rb_mtx);
}

size_t mrb_data_len(mrb* rb) {
	return rb->rb_ab;
}

bool mrb_put(mrb* rb, char *source, size_t size) {
	int partial;

	if ( rb->rb_ab + size > RING_BUFFER_SIZE ) {
		// RingBuffer overflow.
		return false;
	}
	if ( rb->rb_wt + size <= RING_BUFFER_SIZE ) {
		// written bytes + newly added bytes <= buffer max size
		memcpy(rb->rb_ptr + rb->rb_wt, source, size);
		rb->rb_wt = rb->rb_wt + size;
	}
	else {
		// written bytes + newly added bytes > buffer max size
		// wrap around.
		partial = RING_BUFFER_SIZE - rb->rb_wt;
		memcpy(rb->rb_ptr + rb->rb_wt, source, partial);
		memcpy(rb->rb_ptr, source + partial, size - partial);
		rb->rb_wt = rb->rb_wt + size - RING_BUFFER_SIZE;
	}
	pthread_mutex_lock(&rb->rb_mtx);
	rb->rb_ab += size;
	pthread_mutex_unlock(&rb->rb_mtx);

	return true;
}

bool mrb_get(mrb* rb, char *target, size_t size) {
	int partial;

	if ( rb->rb_ab < size ) {
		// RingBuffer underflow.
		return false;
	}
	if ( rb->rb_rt + size <= RING_BUFFER_SIZE ) {
		// read bytes + newly taken bytes <= buffer max size
		memcpy(target, rb->rb_ptr + rb->rb_rt, size);
		rb->rb_rt = rb->rb_rt + size;
	}
	else {
		// read bytes + newly taken bytes > buffer max size
		// wrap around.
		partial = RING_BUFFER_SIZE - rb->rb_rt;
		memcpy(target, rb->rb_ptr + rb->rb_rt, partial);
		memcpy(target + partial, rb->rb_ptr, size - partial);
		rb->rb_rt = rb->rb_rt + size - RING_BUFFER_SIZE;
	}
	pthread_mutex_lock(&rb->rb_mtx);
	rb->rb_ab -= size;
	pthread_mutex_unlock(&rb->rb_mtx);

	return true;
}

static int search(int adapter_nr, unsigned int frequency, unsigned int ts_id)
{
	char file[256];
	int fd;
	struct dvb_frontend_info info;
	struct dtv_property prop[3];
	struct dtv_properties props;
	int i;
	fe_status_t status;

	sprintf(file, "/dev/dvb/adapter%d/frontend0", adapter_nr);
	if ((fd = open(file, (O_RDWR | O_CREAT | O_TRUNC), 0666)) < 0) {
		perror("open");
		return -1;
	}

	if (ioctl(fd, FE_GET_INFO, &info) < 0) {
		perror("ioctl FE_GET_INFO");
		goto out;
	}

	if (info.type == FE_QPSK || info.type == FE_OFDM) {
	} else {
		fprintf(stderr, "Unknown type of adapter\n");
		goto out;
	}

	prop[0].cmd = DTV_FREQUENCY;
	prop[0].u.data = frequency;
	prop[1].cmd = DTV_STREAM_ID;
	prop[1].u.data = ts_id;
	prop[2].cmd = DTV_TUNE;

	props.props = prop;
	props.num = 3;

	if ((ioctl(fd, FE_SET_PROPERTY, &props)) < 0) {
		perror("ioctl FE_SET_PROPERTY");
		goto out;
	}

	for (i = 0; i < 4; i++) {
		if (ioctl(fd, FE_READ_STATUS, &status) < 0) {
			perror("ioctl FE_READ_STATUS");
			goto out;
		}
		if (status & FE_HAS_LOCK) {
			fprintf(stderr, "Successfully tuned to %d(%d).\n",
				frequency, ts_id);
			return 0;
		}
		usleep(250 * 1000);
	}

	fprintf(stderr, "Failed to tune (status %02x).\n", status);

out:
	close(fd);
	return -1;
}

static int track(int adapter_nr)
{
	char file[256];
	int fd;
	struct dmx_pes_filter_params filter;

	filter.pid = 0x2000;
	filter.input = DMX_IN_FRONTEND;
	filter.output = DMX_OUT_TS_TAP;
	filter.pes_type =  DMX_PES_VIDEO;
	filter.flags = DMX_IMMEDIATE_START;

	sprintf(file, "/dev/dvb/adapter%d/demux0", adapter_nr);
	if ((fd = open(file, O_RDWR)) < 0) {
		perror("open");
		return -1;
	}

	if (ioctl(fd, DMX_SET_PES_FILTER, &filter) < 0) {
		perror("ioctl DMX_SET_PES_FILTER");
		close(fd);
		return -1;
	}
	return 0;
}

void record(int adapter_nr, char* output, int rectime) {
	int fin, fout;
	char input[256];
	pthread_t read_thread, write_thread;
	my_thread_arg arg;

	fout = open(output, (O_WRONLY | O_CREAT | O_TRUNC), 0666);
	if ( fout < 0 ) {
		printf("output file open failed\n");
		return;
	}
	sprintf(input, "/dev/dvb/adapter%d/dvr0", adapter_nr);
	fin = open(input, (O_RDONLY));
	ioctl(fin, DMX_SET_BUFFER_SIZE, DMX_BUFFER_SIZE);

	mrb_init(&arg.rb, RING_BUFFER_SIZE);
	arg.done = false;
	arg.rectime = rectime;
	arg.fin  = fin;
	arg.fout = fout;
	pthread_create(&read_thread , NULL, record_read , &arg);
	pthread_create(&write_thread, NULL, record_write, &arg);
	pthread_join(write_thread, NULL);
	pthread_join(read_thread, NULL);

	mrb_destroy(&arg.rb);
	close(fin);
	close(fout);
}

void * record_read(void * priv) {
	ssize_t rt;
	time_t start_time, current_time;
	char* buf = malloc(DVB_READ_BUFFER_SIZE);
	my_thread_arg* p = (my_thread_arg*)priv;

	start_time = time(NULL);
	while ( 1 ) {
		rt = read(p->fin, buf, DVB_READ_BUFFER_SIZE);
		//printf("read %d %d\n", p->fin, rt);
		if ( rt == 0 ) {
			printf("dvr0 file read failed\n");
			goto error;
		}
		if ( rt < 0 ) {
			switch (errno) {
			case EOVERFLOW:
				printf("read overflow, continue\n");
				continue;
			default:
				printf("read finished. err=%d\n", errno);
				goto error;
			}
		}

		mrb_put(&p->rb, buf, rt);

		// TODO: time() at each DVB_READ_BUFFER_SIZE bytes read is not efficient, reduce frequency
		current_time = time(NULL);
		if ( current_time - start_time > p->rectime || p->done ) {
			break;
		}
	}
	error:
	p->done = true;
	return NULL;
}

void * record_write(void * priv) {
	ssize_t rt = 0, wt, shift;
	char* buf = malloc(DVB_WRITE_BUFFER_SIZE);
	my_thread_arg* p = (my_thread_arg*)priv;

	while ( 1 ) {
		if ( mrb_get(&p->rb, buf, DVB_WRITE_BUFFER_SIZE) ) {
			// sufficient buffer
			rt = DVB_WRITE_BUFFER_SIZE;
		}
		else if ( p->done && mrb_data_len(&p->rb) > 0) {
			// insufficient buffer, but no more buffer fill
			rt = mrb_data_len(&p->rb);
			mrb_get(&p->rb, buf, mrb_data_len(&p->rb));
		}
		else if ( p->done ) {
			// normal exit
			break;
		}
		else {
			usleep(100);
		}

		shift = 0;
		while ( ( wt = write(p->fout, buf + shift, rt) ) > 0) {
			//printf("write %d %d\n", p->fout, rt);
			rt -= wt;
			if ( rt == 0 ) break;
			shift += wt;
		}
		if ( rt > 0 ) {
			// [buf] is not correctly written to [fout]
			printf("output file write failed. err=%d\n", errno);
			goto error;
		}
	}
	error:
	p->done = true;
	return NULL;
}

int main(int argc, char *argv[]) {
	int adapter_nr;
	int channel_freq;
	int channel_phys;
	int channel_id;
	int fd;
	int ret;
	int rectime;

	if (argc != 6) {
		fprintf(stderr, "Usage  : %s adapter_nr channel tsid rectime output\n", argv[0]);
		fprintf(stderr, "    channel can be freqency or channel(TE1/BS1/CS1)\n");
		fprintf(stderr, "Version: 0.0.2\n");
		return 1;
	}
	adapter_nr = strtol(argv[1], NULL, 0);
	channel_freq = strtol(argv[2], NULL, 10);
	channel_id = strtol(argv[3], NULL, 10);
	rectime = atoi(argv[4]);

	if ( channel_freq == 0 ) {
		channel_phys = atoi(argv[2] + 2);
		if ( toupper(*argv[2]) == 'T' && toupper(*(argv[2] + 1)) == 'E' && channel_phys != 0 ) {
			channel_freq = ( 473 + (channel_phys - 13) * 6 ) * 1000000 + 142857;
		}
		else if ( toupper(*argv[2]) == 'B' && toupper(*(argv[2] + 1)) == 'S' && channel_phys != 0 ) {
			channel_freq = (channel_phys - 1) * 38360 / 2 + 1049480;
		}
		else if ( toupper(*argv[2]) == 'C' && toupper(*(argv[2] + 1)) == 'S' && channel_phys != 0 ) {
			channel_freq = (channel_phys - 2) * 40000 / 2 + 1613000;
		}
		fprintf(stderr, "channel_freq = %d\n", channel_freq);
	}

	fd = search(adapter_nr, channel_freq, channel_id);
	if (fd < 0)
		return 1;

	ret = track(adapter_nr);
	record(adapter_nr, argv[5], rectime);
	close(fd);

	return ret < 0;
}

