/**
 *  ---------
 * |.##> <##.|  SmartCard-HSM Support Scripts
 * |#       #|
 * |#       #|  Copyright (c) 2011-2012 CardContact Software & System Consulting
 * |'##> <##'|  Andreas Schwier, 32429 Minden, Germany (www.cardcontact.de)
 *  ---------
 *
 *  This file is part of OpenSCDP.
 *
 *  OpenSCDP is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License version 2 as
 *  published by the Free Software Foundation.
 *
 *  OpenSCDP 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 OpenSCDP; if not, write to the Free Software
 *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 *
 * @fileoverview PaperKey Encoder and Decoder
 */



/**
 * Create a PaperKey encoder/decoder
 *
 * @class Encoder / Decoder for PaperKeys
 * @constructor
 */
function PaperKeyEncoding() {
	this.base  = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
	this.alpha = "ABCDEFGH!JKLMN*PQRSTUVWXYZabcdefghijk%mnopqrstuvwxyz0123456789+/";
}

exports.PaperKeyEncoding = PaperKeyEncoding;

PaperKeyEncoding.E_INV_LENGTH = "Input does not contain 12 characters";
PaperKeyEncoding.E_WRONG_SEGMENT = "Wrong segment number";
PaperKeyEncoding.E_CHECK_FAILED = "Check value failed";



/**
 * Encode an 8 byte segment in human readable form.
 *
 * Based on BASE64 encoding with visually confusable characters replaced.
 *
 * I is replaced by !
 * O is replaced by *
 * l is replaced by %
 *
 * A LUHN-64 character is appended.
 *
 * A segment identifier is encoded in 2 bit at the end of the encoded 64 bit string.
 *
 * @param {Number} segment
 * @param {ByteString} data
 * @type String
 * @return the encoded string
 */
PaperKeyEncoding.prototype.encodeSegment = function(segment, data) {
	if (typeof(segment) != "number" || segment < 0) {
		throw new GPError(module.id, GPError.INVALID_DATA, 0, "segment must be a positive number");
	}

	if (!(data instanceof ByteString)) {
		throw new GPError(module.id, GPError.INVALID_DATA, 0, "data must be ByteString");
	}

	if (data.length != 8) {
		throw new GPError(module.id, GPError.INVALID_DATA, 0, "data must be 8 byte");
	}

	var bb = new ByteBuffer();

	// Append segment number's less 2 bit
	data = data.concat(ByteString.valueOf((segment & 3) << 6));

//	print(data.toString(BASE64));

	var cs = 0;
	var cf = 1;

	for (var i = 0; i < data.length; i += 3) {
		var a = data.bytes(i, 3).toUnsigned();
		var s = (a >> 18) & 0x3F;

		cs += s << cf;
//		print("s = " + s + " cf = " + cf + " cs = " + cs);
		cf = cf ^ 1;

		bb.append(this.alpha.charAt(s));

		s = (a >> 12) & 0x3F;

		cs += s << cf;
//		print("s = " + s + " cf = " + cf + " cs = " + cs);
		cf = cf ^ 1;

		bb.append(this.alpha.charAt(s));

		s = (a >> 6) & 0x3F;

		cs += s << cf;
//		print("s = " + s + " cf = " + cf + " cs = " + cs);
		cf = cf ^ 1;

		bb.append(this.alpha.charAt(s));

		if (i <= 3) {
			s = a & 0x3F;

			cs += s << cf;
//			print("s = " + s + " cf = " + cf + " cs = " + cs);
			cf = cf ^ 1;

			bb.append(this.alpha.charAt(s));
		}
	}

	bb.append(this.alpha.charAt(cs & 0x3F));

	return bb.toString(ASCII);
}



/**
 * Decode an 8 byte segment from 12 input characters.
 *
 * Characters not in the alphabet are ignored.
 *
 * Throws GPError with GPError.INVALID_LENGTH if the input does not contain at least 12 usable characters.
 * Throws GPError with GPError.SIGNATURE_FAILED if the check digit based validation fails.
 * Throws GPError with GPError.INVALID_DATA if the segment number does not match the expected value.
 *
 * @param {Number} segment
 * @param {String} str the user input
 * @type ByteString
 * @return the 8 byte segment
 */
PaperKeyEncoding.prototype.decodeSegment = function(segment, str) {
	if (typeof(segment) != "number" || segment < 0) {
		throw new GPError(module.id, GPError.INVALID_DATA, 0, "segment must be a positive number");
	}

	if (typeof(str) != "string") {
		throw new GPError(module.id, GPError.INVALID_DATA, 0, "str must be String");
	}

//	print(str);
	var bb = new ByteBuffer();

	var cs = 0;
	var cf = 1;
	var a = 0;
	var p = 0;

	for (var i = 0; i < 12; i++) {
		do	{
			var ch = str.charAt(p++);
			if (ch == "") {
				throw new GPError(module.id, GPError.INVALID_LENGTH, 0, PaperKeyEncoding.E_INV_LENGTH);
			}
			var v = this.alpha.indexOf(ch);
			if (v < 0) {
				var v = this.base.indexOf(ch);
			}
		} while (v < 0);

		cs += v << cf;
//		print("v = " + v + " cf = " + cf + " cs = " + cs);
		cf = cf ^ 1;

		a = (a << 6) | v;

		if ((i + 1 & 3) == 0) {
			bb.append(ByteString.valueOf(a, 3));
			a = 0;
		}
	}

	bb.append(ByteString.valueOf(a, 3));

	cs -= v << (1 - cf);
	if (v != (cs & 0x3F)) {
		throw new GPError(module.id, GPError.SIGNATURE_FAILED, 0, PaperKeyEncoding.E_CHECK_FAILED);
	}

//	print(bb);
	if ((bb.byteAt(8) >> 6) != (segment & 3)) {
		throw new GPError(module.id, GPError.INVALID_DATA, 0, PaperKeyEncoding.E_WRONG_SEGMENT);
	}

	var r = bb.toByteString().left(8);
	bb.clear();
	return r;
}



/**
 * Format a segment by splitting the input after each 4 characters and inserting " - "
 *
 * @param {String} str the unformatted string
 * @type String
 * @return the formatted string
 */
PaperKeyEncoding.formatSegment = function(str) {
	var res = "";
	var j = 0;
	while(j < str.length) {
		if (j > 0) {
			res += " - ";
		}
		res += str.substr(j, 4);
		j += 4;
	}
	return res;
}



/**
 * Dump a key
 *
 * @param {ByteString} data the key value (length must be a multiple of 8)
 * @type String
 * @return the formatted key with 3 groups of 4 digits per line
 */
PaperKeyEncoding.dumpKey = function(data) {
	if (!(data instanceof ByteString)) {
		throw new GPError(module.id, GPError.INVALID_DATA, 0, "data must be ByteString");
	}

	if (data.length & 7 != 0) {
		throw new GPError(module.id, GPError.INVALID_DATA, 0, "data must be a multiple of 8 byte");
	}

	var pc = new PaperKeyEncoding();

	var segments = data.length >> 3;

	var str = "";
	for (var i = 0; i < segments; i++) {
		var s = pc.encodeSegment(i, data.bytes(i * 8, 8));
		str += PaperKeyEncoding.formatSegment(s) + "\n";
	}
	return str;
}



PaperKeyEncoding.test = function() {
	var pc = new PaperKeyEncoding();

	var ref = ByteString.valueOf(0, 8);
	var enc = pc.encodeSegment(0, ref);
	assert("AAAAAAAAAAAA" == enc, "All zero seg 0 encoding failed");
	var res = pc.decodeSegment(0, enc);
	assert(ref.equals(res), "Decoding failed");

	var enc = pc.encodeSegment(3, ref);
	assert("AAAAAAAAAADG" == enc, "All zero seg 3 encoding failed");
	var res = pc.decodeSegment(3, enc);
	assert(ref.equals(res), "Decoding failed");

	var enc = pc.encodeSegment(7, ref);
	assert("AAAAAAAAAADG" == enc, "All zero seg 7 encoding failed");

	try {
		var res = pc.decodeSegment(6, enc);
		assert(false, "Did not detect wrong segment");
	}
	catch(e) {
		assert(e.error == GPError.INVALID_DATA, "Must be GPError.INVALID_DATA");
		assert(e.message == PaperKeyEncoding.E_WRONG_SEGMENT, "Must be PaperKeyEncoding.E_WRONG_SEGMENT");
	}

	try {
		var res = pc.decodeSegment(7, "AAAAAAAAAADA");
		assert(false, "Did not detect wrong check digit");
	}
	catch(e) {
		assert(e.error == GPError.SIGNATURE_FAILED, "Must be GPError.SIGNATURE_FAILED");
		assert(e.message == PaperKeyEncoding.E_CHECK_FAILED, "Must be PaperKeyEncoding.E_CHECK_FAILED");
	}

	var ref = ref.not();
	var enc = pc.encodeSegment(0, ref);
	assert("//////////8p" == enc, "All one seg 0 encoding failed");
	var res = pc.decodeSegment(0, enc);
	assert(ref.equals(res), "Decoding failed");
	var enc = pc.encodeSegment(3, ref);
	assert("///////////v" == enc, "All one seg 3 encoding failed");
	var res = pc.decodeSegment(3, enc);
	assert(ref.equals(res), "Decoding failed");

	var ref = ByteString.valueOf(0x04).concat(ByteString.valueOf(0, 7));
	var enc = pc.encodeSegment(0, ref);
	assert("BAAAAAAAAAAC" == enc, "First one encoding failed");
	var res = pc.decodeSegment(0, enc);
	assert(ref.equals(res), "Decoding failed");

	var ref = ByteString.valueOf(0x10, 2).concat(ByteString.valueOf(0, 6));
	var enc = pc.encodeSegment(0, ref);
	assert("ABAAAAAAAAAB" == enc, "Second one encoding failed");
	var res = pc.decodeSegment(0, enc);
	assert(ref.equals(res), "Decoding failed");

	var ref = ByteString.valueOf(0xFC).concat(ByteString.valueOf(0, 7));
	var enc = pc.encodeSegment(0, ref);
	assert("/AAAAAAAAAA+" == enc, "First max encoding failed");
	var res = pc.decodeSegment(0, enc);
	assert(ref.equals(res), "Decoding failed");

	var ref = ByteString.valueOf(0x03F0, 2).concat(ByteString.valueOf(0, 6));
	var enc = pc.encodeSegment(0, ref);
	assert("A/AAAAAAAAA/" == enc, "Second max encoding failed");
	var res = pc.decodeSegment(0, enc);
	assert(ref.equals(res), "Decoding failed");



	var crypto = new Crypto();

	var kv = crypto.generateRandom(32);
	print(PaperKeyEncoding.dumpKey(kv));

	while (true) {
		var kv = crypto.generateRandom(8);
		var str = PaperKeyEncoding.formatSegment(pc.encodeSegment(0, kv));
		print(str);
		var inp = Dialog.prompt("Enter segment " + str, "");
		if (inp == null) {
			throw new Error("User abort");
		}
		try	{
			var val = pc.decodeSegment(0, inp);
			if (!kv.equals(val)) {
				throw new GPError(module.id, GPError.INVALID_DATA, 0, "Entered value does not match key");
			}
		}
		catch(e) {
			Dialog.prompt(e.message);
		}
	}
}
