const gpac = require('..');
const filesys = require('fs');


//run in blocking mode
const RUN_SYNC=0;
//run in step mode but blocking node eventloop
const RUN_STEP=1;
//run in step mode and not blocking node eventloop
const RUN_PROMISE=2;

//test simple session, no custom filters
const TEST_SIMPLE=0;
//test sink
const TEST_SINK=1;
//test forward
const TEST_FORWARD=2;

let run_mode=RUN_SYNC;
let test_task=false;
let run_task_dur=0;
let test_type=TEST_SIMPLE;
let FILTERS=[];
let SINKS=[];
let SRCS=[];
let memtrack=0;
let is_verbose=false;
let start_time=0;
let custom_dash=false;
let custom_httpout=false;

let fileio_sources=false;
let fileio_sinks=false;

const PCKFWD_REF=0;
const PCKFWD_ALLOC=1;
const PCKFWD_SHARED=2;
const PCKFWD_CLONE=3;
const PCKFWD_COPY=4;
const PCKFWD_FWD=5;
const PCKFWD_ALL=6;


let pck_fwd_mode=PCKFWD_FWD;

print_usage = () => {
	console.log("node gpac.js [options]");
	console.log();
	console.log("This scripts tests usage of GPAC NodeJS bindings.\n\nPossible options are:");
	console.log("-run=MODE:    runs in given MODE:");
	console.log("\tsync (default): blocking filter session, Node EventLoop blocked");
	console.log("\tstep: non-blocking filter session, Node EventLoop blocked");
	console.log("\tasync: non-blocking filter session as promise, Node EventLoop active");
	console.log("-cust=MODE:   custom JS filter test mode:");
	console.log("\tsimple (default): runs source(s), sink(s) and filters");
	console.log("\tsink: runs source(s) into custom JS filter sink");
	console.log("\tfwd: runs source(s) into custom JS filter forwarding to sink(s) (-dst=) or to filter(s) (-f=)");
	console.log("-fwd=MODE:    packet forward mode for forward test mode:");
	console.log("\tfwd (default): uses packet forwarding");
	console.log("\tref: uses packet reference");
	console.log("\talloc: copies data");
	console.log("\tshared: use shared JS data and JS destruction callbacks");
	console.log("\tclone: uses packet cloning");
	console.log("\tcopy: uses packet copy");
	console.log("\tall: cycles through all above modes");

	console.log("-src=URL:     adds a source URL. Can be set multiple times for multiple sources");
	console.log("-dst=URL:     adds a destination URL. Can be set multiple times for multiple sinks");
	console.log("-f=FILTER:    adds a filter (e.g. 'avgen'). Can be set multiple times for filters, cf `gpac -h doc` for link options");
	console.log("-mt:          enables GPAC memory tracking");
	console.log("-p=PROF:      sets profile to PROF");
	console.log("-ib:          enables FileIO wrapper for source(s)");
	console.log("-ob:          enables FileIO wrapper for sink(s)");
	console.log("-task=N:      runs user test task every second for N seconds (0 means until end)");
	console.log("-play=N:      overrides PLAY event in forward mode to the given start time in seconds");
	console.log("-cdash:       test custom DASH algo");
	console.log("-chttp:       test custom HTTP request handler");
	console.log("-v:           enables info dump, property dump for PIDs and packets");
	console.log("-h:           prints help");
	console.log();
	console.log("Other options will be passed to GPAC");
	console.log();
	console.log("Examples");
	console.log("- Play MyURL to aout and vout:\nnode gpac.js -src=MyURL -f=aout -f=vout\n");
	console.log("- Remux MyURL to DASH and HLS (assumes directory dash exists):\nnode gpac.js -src=MyURL -dst=dash/live.mpd:dual\n");
	console.log("- Run avgen to aout and vout:\nnode gpac.js -f=avgen -f=aout -f=vout\n");
	console.log("- Play MyURL into custom node filter then to aout and vout:\nnode gpac.js -cust=fwd -src=MyURL -f=aout -f=vout\n");
	console.log("- Play MyURL into custom node filter, get input file from NodeJS, keep node eventloop active and use 2 extra threads:\nnode gpac.js -run=async -threads=2 -cust=fwd -ib -src=MyURL -f=aout -f=vout\n");
	console.log();
	console.log('GPAC NodeJS version ' + gpac.version + ' libgpac ' + gpac.abi_major + ':' + gpac.abi_minor + '.' + gpac.abi_micro);
	console.log('' + gpac.copyright_cite);

	process.exit(0);
}
bad_param = (v) => {
	console.log(v);
	process.exit(1);
};

let other_args=['nodegpac'];

if (is_verbose)
	console.log('GPAC NodeJS version ' + gpac.version + ' libgpac ' + gpac.abi_major + ':' + gpac.abi_minor + '.' + gpac.abi_micro);

let fio_factory = {
	open: function(url, mode) {

		this.file = null;
		this.size = 0;
		this.position = 0;
		this.read_mode = false;
		this.is_eof = false;
		this.url = url;
		mode = mode.replace('b', '');
		mode = mode.replace('t', '');

		try {
			this.file = filesys.openSync(url, mode);
		} catch (e) {
			console.log('Fail to open ' + url + ' in mode ' + mode + ': ' + e);
			return false;
		}
		if (mode.indexOf('w')<0) {
			let stats = filesys.fstatSync(this.file);
			this.size = stats.size;
			if (mode.indexOf('a+')>=0) {
				this.position = this.size;
			}
			this.read_mode = true;
		}
		return true;
	},
	close: function() {
		filesys.closeSync(this.file);
		this.file = null;
	},
	read: function(buf) {
		let nb_bytes = 0;
		try {
			nb_bytes = filesys.readSync(this.file, buf, 0, buf.length, this.position);
		} catch (e) {
			console.log('read error: ' + e);
			return 0;
		}
		if (!nb_bytes) this.is_eof = true;
		this.position += nb_bytes;
		return nb_bytes;
	},
	write: function(buf) {
		let nb_bytes = filesys.writeSync(this.file, buf, 0, buf.length, this.position);
		if (this.position == this.size) {
			this.size += nb_bytes;
		}
		this.position += nb_bytes;
		return nb_bytes;
	},
	seek: function(pos, whence) {
		this.is_eof = false;
		if (pos<0) return -1;
		//seek set
		if (whence==0) {
			this.position = pos;
		}
		//seek cur
		else if (whence==1) {
			this.position += pos;
		}
		//seek end
		else if (whence==2) {
			if (this.size < pos) return -1;
			this.position = this.size - pos;
		} else {
			return -1;
		}
		return 0;
	},
	tell: function() {
		return this.position;
	},
	eof: function() {
		return this.is_eof;
	},
	exists: function(url) {
		try {
			filesys.accessSync(url);
		} catch (err) {
			return false;
		}
		return true;
	}
};


function push_source(url)
{
	if (fileio_sources) {
		let fio = new gpac.FileIO(url, fio_factory);
		url = fio.url;
	}
	SRCS.push(url);
}

function push_sink(url)
{
	if (fileio_sinks) {
		let fio = new gpac.FileIO(url, fio_factory);
		url = fio.url;
	}
	SINKS.push(url);
}

//first pass, init libgpac mem track and profile if set
//this must be done before any new constructor (FileIO, FilterSession) otherwise it is ignored
let profile=null;
let args = process.argv.splice(1);
args.forEach( val => {
	if (val=='-h') print_usage();
	else if (val == '-mt') memtrack = gpac.GF_MemTrackerSimple;
	else if (val.startsWith('-p=')) {
		profile = val.substring(3);
	}
});

if (memtrack || profile)
	gpac.init(memtrack, profile);

//second pass, parse all other args
args.forEach( val => {
	if (val=='-h') { }
	else if (val=='-mt') { }
	else if (val.startsWith('-p=')) { }

	else if (val=='-run=sync') run_mode=RUN_SYNC;
	else if (val=='-run=step') run_mode=RUN_STEP;
	else if (val=='-run=async') run_mode=RUN_PROMISE;
	else if (val.startsWith('-run=')) { bad_param("Unknown run mode "+val); }

	else if (val=='-cust=simple') test_type=TEST_SIMPLE;
	else if (val=='-cust=sink') test_type=TEST_SINK;
	else if (val=='-cust=fwd') test_type=TEST_FORWARD;
	else if (val.startsWith('-cust=')) { bad_param("Unknown custom test mode "+val); }

	else if (val=='-fwd=fwd') pck_fwd_mode=PCKFWD_FWD;
	else if (val=='-fwd=ref') pck_fwd_mode=PCKFWD_REF;
	else if (val=='-fwd=alloc') pck_fwd_mode=PCKFWD_ALLOC;
	else if (val=='-fwd=shared') pck_fwd_mode=PCKFWD_SHARED;
	else if (val=='-fwd=copy') pck_fwd_mode=PCKFWD_COPY;
	else if (val=='-fwd=clone') pck_fwd_mode=PCKFWD_CLONE;
	else if (val=='-fwd=all') pck_fwd_mode=PCKFWD_ALL;
	else if (val.startsWith('-fwd=')) { bad_param("Unknown packet forward mode "+val); }

	else if (val=='-task') { test_task = true; run_task_dur = -1; }
	else if (val.startsWith('-task=')) {
		run_task_dur = parseInt(val.substring(6));
		if (run_task_dur == 0) run_task_dur = -1;
		test_task = true;
	}
	else if (val.startsWith('-src=')) { push_source( val.substring(5) ); }
	else if (val.startsWith('-dst=')) { push_sink( val.substring(5) ); }
	else if (val.startsWith('-f=')) { FILTERS.push( val.substring(3)  ); }
	else if (val == '-v') is_verbose = true;
	else if (val.startsWith('-play=')) {
		start_time = parseFloat(val.substring(6));
	}
	else if (val == '-ib') fileio_sources = true;
	else if (val == '-ob') fileio_sinks = true;
	else if (val == '-cdash') custom_dash = true;
	else if (val == '-chttp') custom_httpout = true;

	else if (val.startsWith('-')) {
		other_args.push(val);
	}
});

if (other_args.length > 1)
	gpac.set_args(other_args);

if (!SRCS.length && !SINKS.length && !FILTERS.length) {
	console.log("No source, destination or filter specified, check usage (-h)");
	process.exit(1);
}

let dash_algo = null;
if (custom_dash) {
	dash_algo = {
		on_period_reset: function(type) {
			console.log('period reset type ' + type);
		},
		on_new_group: function(group) {
			console.log('Got new group ' + JSON.stringify(group) );
		},
		on_rate_adaptation: function(group, base_group, force_lower_complexity, stats) {
			console.log('Rate adaptation on group ' + group.idx + ' - stats ' + JSON.stringify(stats) );
			return 0;
		},
		on_download_monitor: function(group, stats) {
			console.log('Download monitor on group ' + group.idx + ' - stats ' + JSON.stringify(stats) );
			return -1;
		}
	};
}

let http_req = null;
if (custom_httpout) {
	http_req = {
		on_request: function (req) {
			console.log('got resquest ' + JSON.stringify(req));
			req.headers_out = [{name: "x-GPAC", value: "foo"}];
			req.close = function(error) {
				console.log("Closing: " + gpac.e2s(error));
			}
			req.replys=0;
			req.send();
		}
	};
}


let fs;
if (run_mode==RUN_SYNC) {
	fs = new gpac.FilterSession();
} else {
	fs = new gpac.FilterSession(gpac.GF_FS_FLAG_NON_BLOCKING);
}

fs.on_filter_new = function(f)
{
	if (is_verbose)
		console.log('New filter created: ' + f.name);
	if (dash_algo && (f.name=="dashin")) {
		try {
			f.bind(dash_algo);
		} catch (e) {
			console.log('Failed to bind dash algo: ' + e);
		}
	}
	else if (http_req && (f.name=="httpout")) {
		try {
			f.bind(http_req);
		} catch (e) {
			console.log('Failed to bind http request handler: ' + e);
		}
	}
}
fs.on_filter_del = function(f) {
	if (is_verbose)
		console.log('Filter deleted: ' + f.name);
}


if (is_verbose) {
	console.log('Error code GF_URL_ERROR means ' + gpac.e2s(gpac.GF_URL_ERROR) );
	console.log('Supported mime video/mp4: ' + fs.is_supported_mime('video/mp4') );
	if (SRCS.length)
		console.log('Supported file ' + SRCS[0] + ': ' + fs.is_supported_source(SRCS[0]) );
}


if (test_task) {
	t = {};
	t.nb_tasks = 0;
	t.execute = function() {
		if (fs.last_task) {
			return false;
		}
		this.nb_tasks++;
		if (run_task_dur > 0) {
			run_task_dur -= 1;
			if (run_task_dur <= 0) {
				console.log('Last task run #' + this.nb_tasks);
				return false;
			}
		}
		console.log('Task run #' + this.nb_tasks);
		return 1000;
	}
	fs.post_task(t);
}

fs.on_event = function(evt)
{
	//console.log('FS event ' + JSON.stringify(evt) );
	if (evt.ui_type == gpac.GF_EVENT_QUIT) {
		fs.abort(gpac.GF_FS_FLUSH_NONE);
		return true;
	}
	return false;
}

//load sources
SRCS.forEach( src => {
	console.log('Loading source URL ' + src);
	src_f = fs.load_src(src);
});

const dump_prop = (type, val, ptype) => {
	if (ptype==gpac.GF_PROP_DATA)
		console.log('\t' + type + ': raw data');
	else if (typeof val == 'object')
		console.log('\t' + type + ': ' + JSON.stringify(val) );
	else
		console.log('\t' + type + ': ' + val + ' (' + (typeof val) + ')');
}

//load custom filter
let cust = null;
if (test_type != TEST_SIMPLE) {
	cust = fs.new_filter("NodeJS_Test");
	//we accept any number of input pids
	cust.set_max_pids(-1);
	cust.pids = [];

	if (test_type == TEST_FORWARD) {
		cust.push_cap('StreamType', 'Visual', gpac.GF_CAPS_INPUT_OUTPUT);
		cust.push_cap('StreamType', 'Audio', gpac.GF_CAPS_INPUT_OUTPUT);
	} else {
		cust.push_cap('StreamType', 'Visual', gpac.GF_CAPS_INPUT);
		cust.push_cap('StreamType', 'Audio', gpac.GF_CAPS_INPUT);
	}

	cust.configure_pid = function(pid, is_remove)
	{
		if (this.pids.indexOf(pid) < 0) {
			this.pids.push(pid);
			pid.nb_pck = 0;

			if (test_type == TEST_SINK) {
				let evt = new gpac.FilterEvent(gpac.GF_FEVT_PLAY);
				pid.send_event(evt);
				if (is_verbose) {
					console.log('Input PID Props: ');
					pid.enum_props( dump_prop );
				}
				pid.opid = null;
			}

			else {
				//create output pid
				pid.opid = this.new_pid();
				pid.opid.copy_props(pid);
				if (is_verbose) {
					console.log('Output PID props: ');
					pid.opid.enum_props( dump_prop );
				}
			}
		} else if (is_remove) {
			console.log('PID remove !');
		} else {
			console.log('PID reconfigure !');
			pid.opid.copy_props(pid);
		}
		return gpac.GF_OK;
	}
	cust.pck_clone_cache = null;
	if (pck_fwd_mode==PCKFWD_ALL) {
		cust.fwd_mode = 0;
		cust.fwd_loop = true;
	} else {
		cust.fwd_mode = pck_fwd_mode;
		cust.fwd_loop = false;
	}

	cust.process = function() {
		let nb_eos=0;

		this.pids.forEach(pid =>{
			if (pid.eos) {
				nb_eos++;
				if (pid.opid) pid.opid.eos = true;
				return;
			}
			//begin of packet fetch loop
			while (1) {

			let pck = pid.get_packet();
			if (!pck) return;

			if (is_verbose) {
				console.log('PCK props: ' + JSON.stringify(pck) );
				pck.enum_props( dump_prop );
			}
			pid.nb_pck ++;
			if (test_type == TEST_SINK) {
				pid.drop_packet();
				continue;
			}
			//send by reference
			if (this.fwd_mode==PCKFWD_REF) {
				let dst = pid.opid.new_pck_ref(pck);
				dst.copy_props(pck);
				dst.send();
			}
			//full copy mode
			else if (this.fwd_mode==PCKFWD_ALLOC) {
				let dst = pid.opid.new_pck(pck.size);
				dst.copy_props(pck);
				new Uint8Array(dst.data).set(new Uint8Array(pck.data));
				dst.send();
			}
			//keep ref to packet and send new packet using shared data
			else if (this.fwd_mode==PCKFWD_SHARED) {
				/*
					Ultimately we would like to test ab = pck.data + pck.ref() and pck.unref() in destructor
					however this crashes node:
						https://github.com/node-ffi-napi/ref-napi/issues/54
				*/
				let ab = new ArrayBuffer(pck.size);
				new Uint8Array(ab).set(new Uint8Array(pck.data));
				//keep ref
				pck.ref();
				let dst = pid.opid.new_pck_shared(ab, () => { pck.unref();});
				dst.copy_props(pck);
				dst.send();
			}
			//pck_clone
			else if (this.fwd_mode==PCKFWD_CLONE) {
				let dst = pid.opid.new_pck_clone(pck);
				dst.send();
			}
			//pck_copy
			else if (this.fwd_mode==PCKFWD_COPY) {
				let dst = pid.opid.new_pck_copy(pck);
				dst.send();
			} else {
				pid.opid.forward(pck);
			}
			if (this.fwd_loop) {
				this.fwd_mode++;
				if (this.fwd_mode==6) this.fwd_mode=0;
			}
			pid.drop_packet();
			} //end of packet fetch loop
		});

		if (nb_eos == this.pids.length)
			return gpac.GF_EOS;

		return gpac.GF_OK;
	}
	cust.process_event = function(evt)
	{
		//shortcut play event, canceling it and sending a play event with a different start
		if ((evt.type==gpac.GF_FEVT_PLAY) && start_time) {
			let anevt = new gpac.FilterEvent(gpac.GF_FEVT_PLAY);
			anevt.start_range = start_time;
			this.pids.forEach(pid => pid.send_event(anevt));
			return true;
		}
		return false;
	}
}

//load sinks
if (test_type != TEST_SINK) {
	SINKS.forEach( sink => {
		console.log('Loading dest ' + sink);
		let sink_f = fs.load_dst(sink);
		//set sink source to be our custom filter
		if (cust && !FILTERS.length)
			sink_f.set_source(cust);
	});
}

//load filters
FILTERS.forEach( fname => {
	console.log('Loading filter ' + fname);
	let f = fs.load(fname);

	if (test_type != TEST_SINK) {
		//set source to be our custom filter
		if (cust && !SINKS.length)
			f.set_source(cust);
	}
});

//print custom filter connections
if (cust && is_verbose) {
	console.log('CustomFilter connections ');
	cust.print_connections();
}

console.log('');

const session_done = () => {
	console.log('Session done running, graph: ');
	fs.print_graph();
};

//runin sync mode
if (run_mode==RUN_SYNC) {
	console.log('Running in blocking mode');
	fs.run();
	session_done();
	return;
}

//runin step mode
if (run_mode==RUN_STEP) {
	console.log('Running in non-blocking mode');
	while (true) {
		fs.run();
		if (fs.last_task) break;
	}
	session_done();
	return;
}

//Run session step as Promise
const FilterSessionPromise = (fs_run_task) => {
  var fsrun_promise = () => {
    return (fs.last_task==false) ? fs_run_task().then(fsrun_promise) : Promise.resolve();
  }
  return fsrun_promise();
};

const run_task = () => {
	return new Promise((resolve, reject) => {
	  resolve( fs.run() );
	});
}
FilterSessionPromise(run_task).then( ).finally( session_done );

console.log('Entering NodeJS EventLoop');
