/***************************************************************************************************
	------------------Javascript Reed-Solomon Universal Encoder/Decoder--------------------

	Written by : Merricx

	Heavily referenced from https://en.wikiversity.org/wiki/Reed%E2%80%93Solomon_codes_for_coders
	And ported partly from https://github.com/tomerfiliba/reedsolomon

***************************************************************************************************/

var gf_exp = new Uint8Array(512);
var gf_log = new Uint8Array(256);

//Source: http://stackoverflow.com/questions/2044760/default-array-values
Array.prototype.repeat = function(def, len){
	while(len)
		this[--len]= def;
	return this;
};

/*
======================================================
					GALOIS FIELD
======================================================
*/


function init_tables(prim){
	prim = prim || 0x11d;

	gf_exp = new Uint8Array(512);
	gf_log = new Uint8Array(256);

	var x = 1;
	for(var i=0; i < 255;i++){
		gf_exp[i] = x;
		gf_log[x] = i;
		x = gf_mult_noLUT(x,2,prim);
	}

	for(var i=255; i < 512; i++){
		gf_exp[i] = gf_exp[i - 255];
	}

	return [gf_log, gf_exp];
}

function gf_add(x,y){
	return x ^ y;
}

function gf_sub(x,y){
	return x ^ y;
}

function gf_mult_noLUT(x,y,prim){
	prim = prim || 0;

	function cl_mult(x,y){
		var z = 0;
		var i = 0;
		while((y>>i) > 0){
			if(y & (1<<i)){
				z ^= x<<i;
			}
			i++;
		}

		return z;
	}

	function bit_length(n){
		var bits = 0;
		while(n>>bits){
			bits += 1;
		}
		return bits;
	}

	function cl_div(dividend,divisor){

		var dl1 = bit_length(dividend);
		var dl2 = bit_length(divisor);

		if(dl1 < dl2){
			return dividend;
		}

		for(var i=dl1-dl2; i > -1; i--){
			if(dividend & (1 << i+dl2-1)){
				dividend ^= divisor << i;
			}
		}

		return dividend;
	}

	var result = cl_mult(x,y);

	if(prim > 0){
		result = cl_div(result, prim);
	}

	return result;
}

function gf_mul(x,y){
	if(x == 0 || y == 0)
		return 0;

	return gf_exp[gf_log[x] + gf_log[y]];
}

function gf_div(x,y){
	if(y == 0){
		throw "Division by zero";
	}
	if(x == 0){
		return 0;
	}

	return gf_exp[(gf_log[x] + 255 - gf_log[y]) % 255];
}

function gf_pow(x, power){
	return gf_exp[(((gf_log[x] * power) % 255) + 255) % 255];
}

function gf_inverse(x){
	return gf_exp[255 - gf_log[x]];
}

/*
======================================================
				POLYNOMIAL OPERATION
======================================================
*/

function gf_poly_scale(p,x){
	var r = [];
	for(var i=0; i < p.length; i++){
		r[i] = gf_mul(p[i], x);
	}
	return r;
}

function gf_poly_add(p,q){
	var r = new Array(Math.max(p.length, q.length));
	for(var i=0; i < p.length; i++){
		r[i+r.length-p.length] = p[i];
	}
	for(var i=0; i < q.length; i++){
		r[i+r.length-q.length] ^= q[i];
	}

	return r;
}

function gf_poly_mul(p,q){
	var r = new Array(p.length+q.length-1);

	for(var j=0; j < q.length;j++){
		for(var i=0; i < p.length; i++){
			r[j+i] ^= gf_mul(p[i], q[j]);
		}
	}

	return r;
}

function gf_poly_div(dividend, divisor){

	var msg_out = Array.prototype.slice.call(dividend);

	for(var i=0; i < dividend.length - (divisor.length-1); i++){
		coef = msg_out[i];
		if(coef != 0){
			for(var j=1; j < divisor.length; j++){
				if(divisor[j] != 0){
					msg_out[i+j] ^= gf_mul(divisor[j], coef);
				}
			}
		}
	}

	var separator = divisor.length - 1;
	var result = [[],[]];
	for(var i=0; i < msg_out.length-separator; i++){
		result[0][i] = msg_out[i];
	}
	
	for(var i=msg_out.length-separator; i < msg_out.length; i++){
		result[1].push(msg_out[i]);
	}
	
	return result;
}

function gf_poly_eval(poly, x){
	var y = poly[0];
	for(var i=1; i < poly.length; i++){
		y = gf_mul(y,x) ^ poly[i];
	}

	return y;
}

/*
======================================================
				REED-SOLOMON ENCODING
======================================================
*/

function rs_generator_poly(nysm){
	var g = [1];
	for(var i=0; i < nysm; i++){
		g = gf_poly_mul(g, [1, gf_pow(2, i)])
	}

	return g;
}

function rs_encode_msg(msg_in, nysm){

	if(msg_in.length + nysm > 255)
		throw "Message is too long...";

	var gen = rs_generator_poly(nysm);
	var msg_out = [].repeat(0, (msg_in.length+gen.length)-1)
	for(var i=0; i < msg_in.length; i++){
		msg_out[i] = msg_in[i];
	}

	for(var i=0; i < msg_in.length; i++){
		var coef = msg_out[i];

		if(coef != 0){
			for(var j=1; j < gen.length; j++){
				msg_out[i+j] ^= gf_mul(gen[j], coef);
			}
		}
	}

	for(var i=0; i < msg_in.length; i++){
		msg_out[i] = msg_in[i];
	}

	return msg_out;
}

/*
======================================================
				REED-SOLOMON DECODING
======================================================
*/

function rs_calc_syndromes(msg, nysm){
	var synd = [].repeat(0, nysm);

	for(var i=0; i < nysm; i++){
		synd[i] = gf_poly_eval(msg, gf_pow(2,i));
	}

	return [0].concat(synd);
}

function rs_check(msg, nysm){
	var check = rs_calc_syndromes(msg, nysm);
	var no_error = true;
	for(var i=0; i < check.length; i++){
		if(check[i] !=  0){
			no_error = false;
			break;
		}
	}

	return no_error;
}

function rs_find_errata_locator(e_pos){
	var e_loc = [1];

	for(var i=0; i < e_pos.length; i++){
		e_loc = gf_poly_mul(e_loc, gf_poly_add([1], [gf_pow(2,e_pos[i]), 0]))
	}

	return e_loc;
}

function rs_find_error_evaluator(synd, err_loc, nysm){
	var x = [].repeat(0, nysm+1);
	var remainder = gf_poly_div(gf_poly_mul(synd, err_loc), ([1].concat(x)))[1];


	return remainder;
}

function rs_correct_errata(msg_in, synd, err_pos){
	var coef_pos = [];
	for(var i=0; i < err_pos.length; i++){
		coef_pos[i] = msg_in.length - 1 - err_pos[i];
	}
	
	var err_loc = rs_find_errata_locator(coef_pos);
	var err_eval = rs_find_error_evaluator(synd.reverse(), err_loc, err_loc.length-1);

	var X = [];
	for(var i=0; i < coef_pos.length; i++){
		var l = 255 - coef_pos[i];
		X.push(gf_pow(2, -l));
	}

	var E = [].repeat(0, msg_in.length);

	for(var i=0; i < X.length; i++){
		var Xi_inv = gf_inverse(X[i]);

		err_loc_prime_tmp = [];
		for(var j=0; j < X.length; j++){
			if(j != i){
				err_loc_prime_tmp.push(gf_sub(1, gf_mul(Xi_inv, X[j])));
			}
		}
		
		var err_loc_prime = 1;
		for(var j=0; j < err_loc_prime_tmp.length; j++){
			err_loc_prime = gf_mul(err_loc_prime, err_loc_prime_tmp[j]);
		}

		var y = gf_poly_eval(err_eval, Xi_inv);
		y = gf_mul(gf_pow(X[i], 1), y);

		var magnitude = gf_div(y, err_loc_prime);

		E[err_pos[i]] = magnitude;
	}

	msg_in = gf_poly_add(msg_in, E);

	return msg_in;
}

function rs_find_error_locator(synd, nysm, erase_loc, erase_count){

	erase_loc = erase_loc || undefined;
	erase_count = erase_count || 0;

	if(erase_loc != undefined){
		err_loc = Array.prototype.slice.call(erase_loc);
		old_loc = Array.prototype.slice.call(erase_loc);
	} else {
		err_loc = [1];
		old_loc = [1];
	}

	synd_shift = 0
	if(synd.length > nysm){
		synd_shift = synd.length - nysm;
	}

	for(var i=0; i < nysm-erase_count;i++){
		if(erase_loc != undefined){
			K = erase_count+i+synd_shift;
		} else {
			K = i+synd_shift;
		}

		delta = synd[K];
		
		for(var j=1;j < err_loc.length; j++){
			delta ^= gf_mul(err_loc[err_loc.length-(j+1)], synd[K-j]);
		}
		
		old_loc = old_loc.concat([0]);
		
		if(delta != 0){
			if(old_loc.length > err_loc.length){
				new_loc = gf_poly_scale(old_loc, delta);
				old_loc = gf_poly_scale(err_loc, gf_inverse(delta));
				err_loc = new_loc;
			}
			err_loc = gf_poly_add(err_loc, gf_poly_scale(old_loc, delta));
		}
	}

	while(err_loc.length != 0 && err_loc[0] == 0){
		err_loc.shift();
	}
	errs = err_loc.length - 1;
	if((errs - erase_count) * 2 + erase_count > nysm){
		throw new Error("Too many errors to correct");
	}

	return err_loc;

}

function rs_find_errors(err_loc, nmess){
	errs = err_loc.length - 1;
	err_pos = [];
	for(var i=0; i < nmess; i++){
		if(gf_poly_eval(err_loc, gf_pow(2, i)) == 0){
			err_pos.push(nmess - 1 - i);
		}
	}

	if(err_pos.length != errs){
		throw new Error("Could not locate error!");
	}

	return err_pos;
}

function rs_forney_syndromes(synd, pos, nmess){

	erase_pos_reversed = [];
	for(var i=0; i < pos.length; i++){
		erase_pos_reversed.push(nmess-1-pos[i]);
	}

	fsynd = Array.prototype.slice.call(synd);
	fsynd.shift();

	for(var i=0; i < pos.length; i++){
		x = gf_pow(2, erase_pos_reversed[i]);
		for(var j=0; j < fsynd.length-1; j++){
			fsynd[j] = gf_mul(fsynd[j], x) ^ fsynd[j+1];
		}
	}

	return fsynd;
}

function rs_correct_msg(msg_in, nysm, erase_pos){

	console.log(msg_in);

	erase_pos = erase_pos || undefined;

	var msg_len = msg_in.length - nysm;

	if(msg_in.length > 255){
		return "Message is too long...";
	}

	msg_out = Array.prototype.slice.call(msg_in);

	if(erase_pos == undefined){
		erase_pos = [];
	} else {
		for(var i=0; i < erase_pos.length; i++){
			msg_out[erase_pos[i]] = 0;
		}
	}

	if(erase_pos.length > nysm)
		return "Too many erasures to correct";

	synd = rs_calc_syndromes(msg_out, nysm);

	if(rs_check(msg_out, nysm)){
		msg_in = msg_in.slice(0, msg_len);
		return msg_in;
	}

	fsynd = rs_forney_syndromes(synd, erase_pos, msg_out.length);
	try {
		err_loc = rs_find_error_locator(fsynd, nysm, undefined ,erase_pos.length);
	}
	catch (error){
		return error.message;
	}
	
	try {
		err_pos = rs_find_errors(err_loc.reverse(), msg_out.length);
	} catch (error) {
		return error.message;
	}
	

	msg_out = rs_correct_errata(msg_out, synd, erase_pos.concat(err_pos));
	synd = rs_calc_syndromes(msg_out, nysm);

	if(!rs_check(msg_out, nysm)){
		return "Could not correct message";
	}

	return msg_out.slice(0, msg_len);
}


/*
======================================================
					MAIN FUNCTION
======================================================
*/

//Generate pre-computed tables
init_tables();
var RS = {
	//Encoding
	encode: function(data, nysm){
		return rs_encode_msg(data, nysm);
	},

	//Decoding
	decode: function(data, nysm, erase_pos){
		return rs_correct_msg(data, nysm, erase_pos);
	}

}