import { ByteArrayBase } from './ByteArrayBase';

export class ByteArray extends ByteArrayBase {
	public maxlength: number;
	public arraybytes; //ArrayBuffer
	public unalignedarraybytestemp; //ArrayBuffer

	constructor(maxlength: number = 4) {
		super();
		this._mode = 'Typed array';
		this.maxlength = Math.max((maxlength + 255) & (~255), 4);
		this.arraybytes = new ArrayBuffer(this.maxlength);
		this.unalignedarraybytestemp = new ArrayBuffer(16);
	}

	public ensureWriteableSpace(n: number): void {
		this.ensureSpace(n + this.position);
	}

	public setArrayBuffer(aBuffer: ArrayBuffer): void {
		this.ensureSpace(aBuffer.byteLength);

		this.length = aBuffer.byteLength;

		const inInt8AView: Int8Array = new Int8Array(aBuffer);
		const localInt8View: Int8Array = new Int8Array(this.arraybytes, 0, this.length);

		localInt8View.set(inInt8AView);

		this.position = 0;
	}

	public writeArrayBuffer(aBuffer: ArrayBuffer): void {
		this.ensureWriteableSpace(aBuffer.byteLength);

		const inInt8AView: Int8Array = new Int8Array(aBuffer);
		const localInt8View: Int8Array = new Int8Array(this.arraybytes, this.length, aBuffer.byteLength);
		localInt8View.set(inInt8AView);
		this.length += aBuffer.byteLength;
		this.position = this.length;

	}

	public writeByteArray(ba: ByteArray): void {
		this.ensureWriteableSpace(ba.length);

		const inInt8AView: Int8Array = new Int8Array(ba.arraybytes, 0, ba.length);
		const localInt8View: Int8Array = new Int8Array(this.arraybytes, this.length, ba.length);
		localInt8View.set(inInt8AView);
		this.length += ba.length;
		this.position = this.length;

	}

	public getBytesAvailable(): number {
		return (this.length) - (this.position);
	}

	public ensureSpace(n: number): void {
		if (n > this.maxlength) {
			const newmaxlength: number = (n + 255) & (~255);
			const newarraybuffer = new ArrayBuffer(newmaxlength);
			const view = new Uint8Array(this.arraybytes, 0, this.length);
			const newview = new Uint8Array(newarraybuffer, 0, this.length);
			newview.set(view);      // memcpy
			this.arraybytes = newarraybuffer;
			this.maxlength = newmaxlength;
		}
	}

	public writeObject(object: any): void {
		console.log('writeObject not implemented yet in core/ByteArray');
	}

	public readObject(): any {
		console.log('readObject not implemented yet in core/ByteArray');
		return {};
	}

	public writeByte(b: number): void {
		this.ensureWriteableSpace(1);

		const view = new Int8Array(this.arraybytes);
		view[ this.position++ ] = (~~b); // ~~ is cast to int in js...
		if (this.position > this.length) {
			this.length = this.position;
		}
	}

	public readByte(): number {
		if (this.position >= this.length)
			throw 'ByteArray out of bounds read. Positon=' + this.position + ', Length=' + this.length;

		const view = new Int8Array(this.arraybytes);

		return view[ this.position++ ];
	}

	public readBytes(bytes: ByteArray, offset: number = 0, length: number = 0): void {
		if (length == null)
			length = bytes.length;

		bytes.ensureWriteableSpace(offset + length);

		const byteView: Int8Array = new Int8Array(bytes.arraybytes);
		const localByteView: Int8Array = new Int8Array(this.arraybytes);

		byteView.set(localByteView.subarray(this.position, this.position + length), offset);

		this.position += length;

		if (length + offset > bytes.length)
			bytes.length += (length + offset) - bytes.length;
	}

	public writeUnsignedByte(b: number): void {
		this.ensureWriteableSpace(1);

		const view = new Uint8Array(this.arraybytes);
		view[this.position++] = (~~b) & 0xff; // ~~ is cast to int in js...

		if (this.position > this.length)
			this.length = this.position;
	}

	public readUnsignedByte(): number {
		if (this.position >= this.length)
			throw 'ByteArray out of bounds read. Positon=' + this.position + ', Length=' + this.length;

		const view = new Uint8Array(this.arraybytes);
		return view[this.position++];
	}

	public writeUnsignedShort(b: number): void {
		this.ensureWriteableSpace(2);

		if ((this.position & 1) == 0) {
			const view = new Uint16Array(this.arraybytes);
			view[ this.position >> 1 ] = (~~b) & 0xffff; // ~~ is cast to int in js...
		} else {
			const view = new Uint16Array(this.unalignedarraybytestemp, 0, 1);
			view[0] = (~~b) & 0xffff;
			const view2 = new Uint8Array(this.arraybytes, this.position, 2);
			const view3 = new Uint8Array(this.unalignedarraybytestemp, 0, 2);
			view2.set(view3);
		}

		this.position += 2;

		if (this.position > this.length)
			this.length = this.position;
	}

	public readUTFBytes(len: number): string {
		let value: string = '';
		const max: number = this.position + len;
		const data: DataView = new DataView(this.arraybytes);
		// utf8-encode
		while (this.position < max) {

			const c: number = data.getUint8(this.position++);

			if (c < 0x80) {

				if (c == 0) break;
				value += String.fromCharCode(c);

			} else if (c < 0xE0) {

				value += String.fromCharCode(((c & 0x3F) << 6) | (data.getUint8(this.position++) & 0x7F));

			} else if (c < 0xF0) {

				const c2 = data.getUint8(this.position++);
				value += String.fromCharCode(((c & 0x1F) << 12)
											| ((c2 & 0x7F) << 6)
											| (data.getUint8(this.position++) & 0x7F));

			} else {

				const c2 = data.getUint8(this.position++);
				const c3 = data.getUint8(this.position++);

				value += String.fromCharCode(((c & 0x0F) << 18)
											| ((c2 & 0x7F) << 12)
											| ((c3 << 6) & 0x7F)
											| (data.getUint8(this.position++) & 0x7F));

			}

		}

		return value;
	}

	public writeUTFBytes(s: string) {
		const escstr = encodeURIComponent(s);
		const binstr = escstr.replace(/%([0-9A-F]{2})/g, function(match, p1) {
			return String.fromCharCode(parseInt('0x' + p1));
		});

		this.ensureWriteableSpace(4 + binstr.length);
		this.writeInt(binstr.length);
		for (let i = 0; i < binstr.length; i++) {
			this.writeUnsignedByte(binstr.charCodeAt(i)); //todo: there are probably faster ways to do this
		}
		if (binstr.length % 4) {
			const paddingbytes: number = binstr.length % 4;
			for (let i = 0; i < paddingbytes; i++) {
				this.writeUnsignedByte(0);
			}

		}
		return binstr.length;
	}

	public readInt(): number {
		const data: DataView = new DataView(this.arraybytes);
		const int: number = data.getInt32(this.position, true);

		this.position += 4;

		return int;
	}

	public readShort(): number {
		const data: DataView = new DataView(this.arraybytes);
		const short: number = data.getInt16(this.position, true);

		this.position += 2;

		return short;
	}

	public readDouble(): number {
		const data: DataView = new DataView(this.arraybytes);
		const double: number = data.getFloat64(this.position, true);

		this.position += 8;

		return double;
	}

	public readUnsignedShort(): number {
		if (this.position > this.length + 2)
			throw 'ByteArray out of bounds read. Position=' + this.position + ', Length=' + this.length;

		if ((this.position & 1) == 0) {
			const view = new Uint16Array(this.arraybytes);
			const pa: number = this.position >> 1;
			this.position += 2;
			return view[ pa ];
		} else {
			const view = new Uint16Array(this.unalignedarraybytestemp, 0, 1);
			const view2 = new Uint8Array(this.arraybytes, this.position, 2);
			const view3 = new Uint8Array(this.unalignedarraybytestemp, 0, 2);
			view3.set(view2);
			this.position += 2;
			return view[0];
		}
	}

	public writeUnsignedInt(b: number): void {
		this.ensureWriteableSpace(4);

		if ((this.position & 3) == 0) {
			const view = new Uint32Array(this.arraybytes);
			view[ this.position >> 2 ] = (~~b) & 0xffffffff; // ~~ is cast to int in js...
		} else {
			const view = new Uint32Array(this.unalignedarraybytestemp, 0, 1);
			view[0] = (~~b) & 0xffffffff;
			const view2 = new Uint8Array(this.arraybytes, this.position, 4);
			const view3 = new Uint8Array(this.unalignedarraybytestemp, 0, 4);
			view2.set(view3);
		}
		this.position += 4;

		if (this.position > this.length)
			this.length = this.position;
	}

	public writeInt(b: number): void {
		this.ensureWriteableSpace(4);

		if ((this.position & 3) == 0) {
			const view = new Int32Array(this.arraybytes);
			view[ this.position >> 2 ] = (~~b); // ~~ is cast to int in js...
		} else {
			const view = new Int32Array(this.unalignedarraybytestemp, 0, 1);
			view[0] = (~~b);
			const view2 = new Uint8Array(this.arraybytes, this.position, 4);
			const view3 = new Uint8Array(this.unalignedarraybytestemp, 0, 4);
			view2.set(view3);
		}
		this.position += 4;

		if (this.position > this.length)
			this.length = this.position;
	}

	public readUnsignedInt(): number {
		if (this.position > this.length + 4)
			throw 'ByteArray out of bounds read. Position=' + this.position + ', Length=' + this.length;

		if ((this.position & 3) == 0) {
			const view = new Uint32Array(this.arraybytes);
			const pa: number = this.position >> 2;
			this.position += 4;
			return view[ pa ];
		} else {
			const view = new Uint32Array(this.unalignedarraybytestemp, 0, 1);
			const view2 = new Uint8Array(this.arraybytes, this.position, 4);
			const view3 = new Uint8Array(this.unalignedarraybytestemp, 0, 4);
			view3.set(view2);
			this.position += 4;
			return view[0];
		}
	}

	public writeFloat(b: number): void {
		this.ensureWriteableSpace(4);

		if ((this.position & 3) == 0) {
			const view = new Float32Array(this.arraybytes);
			view[ this.position >> 2 ] = b;
		} else {
			const view = new Float32Array(this.unalignedarraybytestemp, 0, 1);
			view[0] = b;
			const view2 = new Uint8Array(this.arraybytes, this.position, 4);
			const view3 = new Uint8Array(this.unalignedarraybytestemp, 0, 4);
			view2.set(view3);
		}
		this.position += 4;

		if (this.position > this.length)
			this.length = this.position;
	}

	public readFloat(): number {
		if (this.position > this.length + 4)
			throw 'ByteArray out of bounds read. Positon=' + this.position + ', Length=' + this.length;

		if ((this.position & 3) == 0) {
			const view = new Float32Array(this.arraybytes);
			const pa = this.position >> 2;
			this.position += 4;
			return view[pa];
		} else {
			const view = new Float32Array(this.unalignedarraybytestemp, 0, 1);
			const view2 = new Uint8Array(this.arraybytes, this.position, 4);
			const view3 = new Uint8Array(this.unalignedarraybytestemp, 0, 4);
			view3.set(view2);
			this.position += 4;
			return view[ 0 ];
		}
	}
}