/*
 * Copyright 2014 Mozilla Foundation
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * This file implements the AMF0 and AMF3 serialization protocols secified in:
 * http://wwwimages.adobe.com/www.adobe.com/content/dam/Adobe/en/devnet/amf/pdf/amf-file-format-spec.pdf
 */

import { ASObject } from './nat/ASObject';
import { ASArray } from './nat/ASArray';
import { ByteArray } from './natives/byteArray';
import { assert } from '@awayjs/graphics';
import { release } from '@awayfl/swf-loader';
import { ByteArray as AwayByteArray } from '@awayjs/core';

import { StringUtilities, isNumeric } from '@awayfl/swf-loader';
import { ITraits } from './run/ITraits';
import { AXClass } from './run/AXClass';
import { AXBasePrototype } from './run/initializeAXBasePrototype';
import { forEachPublicProperty } from './run/forEachPublicProperty';
import { Float64Vector } from './natives/float64Vector';
import { Uint32Vector } from './natives/uint32Vector';
import { GenericVector } from './natives/GenericVector';
import { Int32Vector } from './natives/int32Vector';

class AMF3ReferenceTables {
	strings: any [] = [];
	objects: any [] = [];
	traits: ITraits [] = [];
	/**
   * Trait names are kept in sync with |traits| and are used to optimize fetching public trait names.
   */
	traitNames: string [] [] = [];
	dynamic: boolean [] = [];
}

export class ClassAliases {
	private _classMap: WeakMap<AXClass, string> = new WeakMap<AXClass, string>();
	private _nameMap: any = Object.create(null);
	getAliasByClass(axClass: AXClass): string {
		return this._classMap.get(axClass);
	}

	getClassByAlias(alias: string): AXClass {
		return this._nameMap[alias];
	}

	registerClassAlias(alias: string, axClass: AXClass) {
		this._classMap.set(axClass, alias);
		release || assert(!this._nameMap[alias] || (this._nameMap[alias] === axClass));
		this._nameMap[alias] = axClass;
	}
}

export const enum AMF0Marker {
	NUMBER = 0x00,
	BOOLEAN = 0x01,
	STRING = 0x02,
	OBJECT = 0x03,
	NULL = 0x05,
	UNDEFINED = 0x06,
	REFERENCE = 0x07,
	ECMA_ARRAY = 0x08,
	OBJECT_END = 0x09,
	STRICT_ARRAY = 0x0A,
	DATE = 0x0B,
	LONG_STRING = 0x0C,
	XML = 0x0F,
	TYPED_OBJECT = 0x10,
	AVMPLUS = 0x11
}

function writeString(ba: ByteArray, s: string) {
	if (s.length > 0xFFFF) {
		throw 'AMF short string exceeded';
	}
	if (!s.length) {
		ba.writeByte(0x00);
		ba.writeByte(0x00);
		return;
	}
	const bytes = StringUtilities.utf8decode(s);
	ba.writeByte((bytes.length >> 8) & 255);
	ba.writeByte(bytes.length & 255);
	for (let i = 0; i < bytes.length; i++) {
		ba.writeByte(bytes[i]);
	}
}

function readString(ba: ByteArray): string {
	const byteLength = (ba.readByte() << 8) | ba.readByte();
	if (!byteLength) {
		return '';
	}

	const buffer = new Uint8Array(byteLength);
	for (let i = 0; i < byteLength; i++) {
		buffer[i] = ba.readByte();
	}

	return StringUtilities.utf8encode(buffer);
}

function writeDouble(ba: ByteArray, value: number) {
	const buffer = new ArrayBuffer(8);
	const view = new DataView(buffer);
	view.setFloat64(0, value, false);
	for (let i = 0; i < buffer.byteLength; i++) {
		ba.writeByte(view.getUint8(i));
	}
}

function readDouble(ba: ByteArray): number {
	const buffer = new ArrayBuffer(8);
	const view = new DataView(buffer);
	for (let i = 0; i < buffer.byteLength; i++) {
		view.setUint8(i, ba.readByte());
	}
	return view.getFloat64(0, false);
}

export class AMF0 {
	public static write(ba: ByteArray, value: any) {
		switch (typeof value) {
			case 'boolean':
				ba.writeByte(AMF0Marker.BOOLEAN);
				ba.writeByte(value ? 0x01 : 0x00);
				break;
			case 'number':
				ba.writeByte(AMF0Marker.NUMBER);
				writeDouble(ba, value);
				break;
			case 'undefined':
				ba.writeByte(AMF0Marker.UNDEFINED);
				break;
			case 'string':
				ba.writeByte(AMF0Marker.STRING);
				writeString(ba, value);
				break;
			case 'object': {
				const object = (<ASObject>value);
				release || assert(object === null || AXBasePrototype.isPrototypeOf(object));
				if (object === null) {
					ba.writeByte(AMF0Marker.NULL);
				} else if (ba.sec.AXArray.axIsType(object)) {
					const array = (<ASArray>object).value;
					ba.writeByte(AMF0Marker.ECMA_ARRAY);
					ba.writeByte((array.length >>> 24) & 255);
					ba.writeByte((array.length >> 16) & 255);
					ba.writeByte((array.length >> 8) & 255);
					ba.writeByte(array.length & 255);
					// REDUX: What about sparse arrays?
					forEachPublicProperty(object, function (key: string, value: any) {
						writeString(ba, key);
						this.write(ba, value);
					}, this);
					ba.writeByte(0x00);
					ba.writeByte(0x00);
					ba.writeByte(AMF0Marker.OBJECT_END);
				} else {
					ba.writeByte(AMF0Marker.OBJECT);
					forEachPublicProperty(object, function (key: string, value: any) {
						writeString(ba, key);
						this.write(ba, value);
					}, this);
					ba.writeByte(0x00);
					ba.writeByte(0x00);
					ba.writeByte(AMF0Marker.OBJECT_END);
				}
				return;
			}
		}
	}

	public static read(ba: ByteArray): any {
		const marker = ba.readByte();
		switch (marker) {
			case AMF0Marker.NUMBER:
				return readDouble(ba);
			case AMF0Marker.BOOLEAN:
				return !!ba.readByte();
			case AMF0Marker.STRING:
				return readString(ba);
			case AMF0Marker.OBJECT: {
				const object = ba.sec.createObject();
				let key;
				while ((key = readString(ba)).length) {
					object.axSetPublicProperty(key, this.read(ba));
				}
				if (ba.readByte() !== AMF0Marker.OBJECT_END) {
					throw 'AMF0 End marker is not found';
				}
				return object;
			}
			case AMF0Marker.NULL:
				return null;
			case AMF0Marker.UNDEFINED:
				return undefined;
			case AMF0Marker.ECMA_ARRAY: {
				const array = ba.sec.createArray([]);
				array.length = (ba.readByte() << 24) | (ba.readByte() << 16) |
					(ba.readByte() << 8) | ba.readByte();
				let key;
				while ((key = readString(ba)).length) {
					array.axSetPublicProperty(key, this.read(ba));
				}
				if (ba.readByte() !== AMF0Marker.OBJECT_END) {
					throw 'AMF0 End marker is not found';
				}
				return array;
			}
			case AMF0Marker.STRICT_ARRAY: {
				const array = ba.sec.createArray([]);
				const length = array.length = (ba.readByte() << 24) | (ba.readByte() << 16) |
          (ba.readByte() << 8) | ba.readByte();
				for (let i = 0; i < length; i++) {
					array.axSetPublicProperty(i, this.read(ba));
				}
				return array;
			}
			case AMF0Marker.AVMPLUS:
				return readAMF3Value(ba, new AMF3ReferenceTables());
			default:
				throw 'AMF0 Unknown marker ' + marker;
		}
	}
}

export const enum AMF3Marker {
	UNDEFINED = 0x00,
	NULL = 0x01,
	FALSE = 0x02,
	TRUE = 0x03,
	INTEGER = 0x04,
	DOUBLE = 0x05,
	STRING = 0x06,
	XML_DOC = 0x07,
	DATE = 0x08,
	ARRAY = 0x09,
	OBJECT = 0x0A,
	XML = 0x0B,
	BYTEARRAY = 0x0C,
	VECTOR_INT = 0x0D,
	VECTOR_UINT = 0x0E,
	VECTOR_DOUBLE = 0x0F,
	VECTOR_OBJECT = 0x10,
	DICTIONARY = 0x11
}

function readU29(ba: ByteArray): number {
	const b1 = ba.readByte();
	if ((b1 & 0x80) === 0) {
		return (b1 & 0x7F);
	}
	const b2 = ba.readByte();
	if ((b2 & 0x80) === 0) {
		return ((b1 & 0x7F) << 7) | (b2 & 0x7F);
	}
	const b3 = ba.readByte();
	if ((b3 & 0x80) === 0) {
		return ((b1 & 0x7F) << 14) | ((b2 & 0x7F) << 7) | (b3 & 0x7F);
	}
	const b4 = ba.readByte();
	let val = ((b1 & 0x7f) << 22) | ((b2 & 0x7f) << 15) | ((b3 & 0x7f) << 8) | (b4 & 0xFF);

	// handle negative
	// https://github.com/Ventero/amf-cpp/blob/master/src/types/amfinteger.cpp#L92
	// https://github.com/yzh44yzh/scala-amf/blob/master/scala-amf-lib/src/com/yzh44yzh/scalaAmf/AmfInt.scala#L35

	if ((val & 0x10000000) !== 0) {
		val |= 0xe0000000;
	}

	return val;
}

function writeU29(ba: ByteArray, value: number) {
	// C++ version
	// https://github.com/Ventero/amf-cpp/blob/master/src/types/amfinteger.cpp#L13

	if (value < -0x10000000 || value >= 0x10000000) {
		throw 'AMF3 U29 range';
	}

	if ((value & 0xFFFFFF80) === 0) {
		ba.writeByte(value & 0x7F);
	} else if ((value & 0xFFFFC000) === 0) {
		ba.writeByte(0x80 | ((value >> 7) & 0x7F));
		ba.writeByte(value & 0x7F);
	} else if ((value & 0xFFE00000) === 0) {
		ba.writeByte(0x80 | ((value >> 14) & 0x7F));
		ba.writeByte(0x80 | ((value >> 7) & 0x7F));
		ba.writeByte(value & 0x7F);
	} else /*if ((value & 0xC0000000) === 0)*/{ // handle negative
		ba.writeByte(0x80 | ((value >> 22) & 0x7F));
		ba.writeByte(0x80 | ((value >> 15) & 0x7F));
		ba.writeByte(0x80 | ((value >> 8) & 0x7F));
		ba.writeByte(value & 0xFF);
	}
}

function readUTF8(ba: ByteArray, references: AMF3ReferenceTables) {
	const u29s = readU29(ba);
	if (u29s === 0x01) {
		return '';
	}
	const strings = references.strings;
	if ((u29s & 1) === 0) {
		return strings[u29s >> 1];
	}

	const byteLength = u29s >> 1;
	const buffer = new Uint8Array(byteLength);
	for (let i = 0; i < byteLength; i++) {
		buffer[i] = ba.readByte();
	}
	const value = StringUtilities.utf8encode(buffer);
	strings.push(value);
	return value;
}

function writeUTF8(ba: ByteArray, s: string, references: AMF3ReferenceTables) {
	if (s === '') {
		ba.writeByte(0x01); // empty string
		return;
	}

	const strings = references.strings;
	const index = strings.indexOf(s);
	if (index >= 0) {
		writeU29(ba, index << 1);
		return;
	}
	strings.push(s);

	const bytes = StringUtilities.utf8decode(s);
	writeU29(ba, 1 | (bytes.length << 1));

	// we can write faster
	if (ba instanceof AwayByteArray) {
		// view can look on biggest array, we should check this
		ba.writeArrayBuffer(
			bytes.byteLength !== bytes.buffer.byteLength
				? new Uint8Array(bytes).buffer
				: bytes.buffer);
		return;
	}

	for (let i = 0; i < bytes.length; i++) {
		ba.writeByte(bytes[i]);
	}
}

function readAMF3Value(ba: ByteArray, references: AMF3ReferenceTables) {
	const marker = ba.readByte();
	switch (marker) {
		case AMF3Marker.NULL:
			return null;
		case AMF3Marker.UNDEFINED:
			return undefined;
		case AMF3Marker.FALSE:
			return false;
		case AMF3Marker.TRUE:
			return true;
		case AMF3Marker.INTEGER:
			return readU29(ba);
		case AMF3Marker.DOUBLE:
			return readDouble(ba);
		case AMF3Marker.STRING:
			return readUTF8(ba, references);
		case AMF3Marker.DATE: {
			const u29o = readU29(ba);
			release || assert((u29o & 1) === 1);
			return ba.sec.AXDate.axConstruct([readDouble(ba)]);
		}
		case AMF3Marker.XML:
			return ba.sec.AXXML.axConstruct([readUTF8(ba, references)]);
		case AMF3Marker.OBJECT: {
			const u29o = readU29(ba);
			if ((u29o & 1) === 0) {
				return references.objects[u29o >> 1];
			}
			let axClass: AXClass;
			let traits: ITraits;
			let isDynamic = true;
			let traitNames;
			if ((u29o & 2) === 0) {
				traits = references.traits[u29o >> 2];
				traitNames = references.traitNames[u29o >> 2];
				isDynamic = references.dynamic[u29o >> 2];
			} else {
				const alias = readUTF8(ba, references);
				if (alias) {
					traits = axClass = ba.sec.classAliases.getClassByAlias(alias);
				}
				isDynamic = (u29o & 8) !== 0;
				traitNames = [];
				for (let i = 0, j = u29o >> 4; i < j; i++) {
					traitNames.push(readUTF8(ba, references));
				}
				references.traits.push(traits);
				references.traitNames.push(traitNames);
				references.dynamic.push(isDynamic);
			}

			const object = axClass ? axClass.axConstruct([]) : ba.sec.createObject();
			references.objects.push(object);
			// Read trait properties.
			for (let i = 0; i < traitNames.length; i++) {
				const value = readAMF3Value(ba, references);
				object.axSetPublicProperty(traitNames[i], value);
			}
			// Read dynamic properties.
			if (isDynamic) {
				let key;
				while ((key = readUTF8(ba, references)) !== '') {
					const value = readAMF3Value(ba, references);
					object.axSetPublicProperty(key, value);
				}
			}
			return object;
		}
		case AMF3Marker.ARRAY: {
			const u29o = readU29(ba);
			if ((u29o & 1) === 0) {
				return references.objects[u29o >> 1];
			}
			const array = ba.sec.createArray([]);
			references.objects.push(array);
			const densePortionLength = u29o >> 1;
			let key;
			while ((key = readUTF8(ba, references)).length) {
				const value = readAMF3Value(ba, references);
				array.axSetPublicProperty(key, value);
			}
			for (let i = 0; i < densePortionLength; i++) {
				const value = readAMF3Value(ba, references);
				array.axSetPublicProperty(i, value);
			}
			return array;
		}
		case AMF3Marker.VECTOR_INT: {
			const u29o = readU29(ba);
			if ((u29o & 1) === 0) {
				return references.objects[u29o >> 1];
			}
			const length = u29o >> 1;
			const fixed = ba.readUnsignedInt();
			const vector: Int32Vector = ba.sec.Int32Vector.axClass.axConstruct([length, fixed]);
			references.objects.push(vector);
			for (let i = 0; i < length; i++) {
				vector.axSetPublicProperty(i, readU29(ba));
			}
			return vector;
		}
		case AMF3Marker.VECTOR_UINT: {
			const u29o = readU29(ba);
			if ((u29o & 1) === 0) {
				return references.objects[u29o >> 1];
			}
			const length = u29o >> 1;
			const fixed = ba.readUnsignedInt();
			const vector: Uint32Vector = ba.sec.Uint32Vector.axClass.axConstruct([length, fixed]);
			references.objects.push(vector);
			for (let i = 0; i < length; i++) {
				vector.axSetPublicProperty(i, readU29(ba));
			}
			return vector;
		}
		case AMF3Marker.VECTOR_DOUBLE: {
			const u29o = readU29(ba);
			if ((u29o & 1) === 0) {
				return references.objects[u29o >> 1];
			}
			const length = u29o >> 1;
			const fixed = ba.readUnsignedInt();
			const vector: Float64Vector = ba.sec.Float64Vector.axClass.axConstruct([length, fixed]);
			references.objects.push(vector);
			for (let i = 0; i < length; i++) {
				vector.axSetPublicProperty(i, readDouble(ba));
			}
			return vector;
		}
		case AMF3Marker.VECTOR_OBJECT: {
			const u29o = readU29(ba);
			if ((u29o & 1) === 0) {
				return references.objects[u29o >> 1];
			}

			const length = u29o >> 1;
			const fixed = ba.readUnsignedInt();
			const type = ba.sec.classAliases.getClassByAlias(readUTF8(ba, references));
			const vector: GenericVector = <any> ba.sec.getVectorClass(type).axConstruct([length, fixed]);
			references.objects.push(vector);
			for (let i = 0; i < length; i++) {
				vector.axSetPublicProperty(i, readAMF3Value(ba, references));
			}
			return vector;
		}
		default:
			throw 'AMF3 Unknown marker ' + marker;
	}
}

/**
 * Tries to write a reference to a previously written object.
 */
function tryWriteAndStartTrackingReference(ba: ByteArray, object: ASObject, references: AMF3ReferenceTables) {
	const objects = references.objects;
	const index = objects.indexOf(object);
	if (index < 0) {
		objects.push(object);
		return false;
	}
	writeU29(ba, index << 1);
	return true;
}

const MAX_INT =  268435456 - 1; // 2^28 - 1
const MIN_INT = -268435456; // -2^28

function writeAMF3Value(ba: ByteArray, value: any, references: AMF3ReferenceTables) {
	switch (typeof value) {
		case 'boolean':
			ba.writeByte(value ? AMF3Marker.TRUE : AMF3Marker.FALSE);
			break;
		case 'number': {
			let useInteger = value === (value | 0);
			if (useInteger) {
				if (value > MAX_INT || value < MIN_INT) {
					useInteger = false;
				}
			}
			if (useInteger) {
				ba.writeByte(AMF3Marker.INTEGER);
				writeU29(ba, value);
			} else {
				ba.writeByte(AMF3Marker.DOUBLE);
				writeDouble(ba, value);
			}
			break;
		}
		case 'undefined':
			ba.writeByte(AMF3Marker.UNDEFINED);
			break;
		case 'string':
			ba.writeByte(AMF3Marker.STRING);
			writeUTF8(ba, value, references);
			break;
		case 'object':
			if (value === null) {
				ba.writeByte(AMF3Marker.NULL);
			} else if (ba.sec.AXArray.axIsType(value)) {
				const array = (<ASArray>value);
				ba.writeByte(AMF3Marker.ARRAY);
				if (tryWriteAndStartTrackingReference(ba, array, references)) {
					break;
				}
				let densePortionLength = 0;
				while (array.axHasPublicProperty(densePortionLength)) {
					++densePortionLength;
				}
				writeU29(ba, (densePortionLength << 1) | 1);
				forEachPublicProperty(array, function (i: any, value: any) {
					if (isNumeric(i) && i >= 0 && i < densePortionLength) {
						return;
					}
					writeUTF8(ba, i, references);
					writeAMF3Value(ba, value, references);
				});
				writeUTF8(ba, '', references);
				for (let j = 0; j < densePortionLength; j++) {
					writeAMF3Value(ba, array.axGetPublicProperty(j), references);
				}
			} else if (ba.sec.AXDate.axIsType(value)) {
				ba.writeByte(AMF3Marker.DATE);
				if (tryWriteAndStartTrackingReference(ba, value, references))
					break;
				writeU29(ba, 1);
				writeDouble(ba, value.valueOf());
			} else if (ba.sec.AXXML.axIsType(value)) {
				ba.writeByte(AMF3Marker.XML);
				writeUTF8(ba, value.toString(), references);
			} else if (ba.sec.Int32Vector.axIsType(value)) {
				const vector = <Float64Vector>value;
				ba.writeByte(AMF3Marker.VECTOR_INT);
				if (tryWriteAndStartTrackingReference(ba, vector, references)) {
					break;
				}
				writeU29(ba, (vector.length << 1) | 1);
				ba.writeUnsignedInt(+vector.fixed);
				for (let i = 0; i < vector.length; i++) {
					writeU29(ba, vector.axGetPublicProperty(i));
				}
			} else if (ba.sec.Uint32Vector.axIsType(value)) {
				const vector = <Uint32Vector>value;
				ba.writeByte(AMF3Marker.VECTOR_UINT);
				if (tryWriteAndStartTrackingReference(ba, vector, references)) {
					break;
				}
				writeU29(ba, (vector.length << 1) | 1);
				ba.writeUnsignedInt(+vector.fixed);
				for (let i = 0; i < vector.length; i++) {
					writeU29(ba, vector.axGetPublicProperty(i));
				}
			} else if (ba.sec.Float64Vector.axIsType(value)) {
				const vector = <Float64Vector>value;
				ba.writeByte(AMF3Marker.VECTOR_DOUBLE);
				if (tryWriteAndStartTrackingReference(ba, vector, references)) {
					break;
				}
				writeU29(ba, (vector.length << 1) | 1);
				ba.writeUnsignedInt(+vector.fixed);
				for (let i = 0; i < vector.length; i++) {
					writeDouble(ba, vector.axGetPublicProperty(i));
				}
			} else if (ba.sec.ObjectVector.axIsType(value)) {
				const vector = <GenericVector>value;
				ba.writeByte(AMF3Marker.VECTOR_OBJECT);
				if (tryWriteAndStartTrackingReference(ba, vector, references)) {
					break;
				}
				writeU29(ba, (vector.length << 1) | 1);
				ba.writeUnsignedInt(+vector.fixed);
				writeUTF8(ba, ba.sec.classAliases.getAliasByClass(value.axClass.type) || '*', references);
				for (let i = 0; i < vector.length; i++) {
					writeAMF3Value(ba, vector.axGetPublicProperty(i), references);
				}

			} else {
				const object = <ASObject>value;

				// TODO Dictionary, ByteArray
				ba.writeByte(AMF3Marker.OBJECT);
				if (tryWriteAndStartTrackingReference(ba, object, references)) {
					break;
				}

				let isDynamic = true;

				const axClass: AXClass = object.axClass;
				if (axClass) {
					const classInfo = axClass.classInfo;
					isDynamic = !classInfo.instanceInfo.isSealed();
					const alias = ba.sec.classAliases.getAliasByClass(axClass) || '';
					const traitsRef = references.traits.indexOf(axClass);
					let traitNames: string [] = null;
					if (traitsRef < 0) {
						// Write traits since we haven't done so yet.
						traitNames = classInfo.instanceInfo.runtimeTraits.getPublicTraitNames();
						references.traits.push(axClass);
						references.traitNames.push(traitNames);
						writeU29(ba, (isDynamic ? 0x0B : 0x03) + (traitNames.length << 4));
						writeUTF8(ba, alias, references);
						// Write trait names.
						for (let i = 0; i < traitNames.length; i++) {
							writeUTF8(ba, traitNames[i], references);
						}
					} else {
						// Write a reference to the previously written traits.
						traitNames = references.traitNames[traitsRef];
						writeU29(ba, 0x01 + (traitsRef << 2));
					}
					// Write the actual trait values.
					for (let i = 0; i < traitNames.length; i++) {
						writeAMF3Value(ba, object.axGetPublicProperty(traitNames[i]), references);
					}
				} else {
					// REDUX: I don't understand in what situations we wouldn't have a class definition, ask Yury.
					// object with no class definition
					writeU29(ba, 0x0B);
					writeUTF8(ba, '', references); // empty alias name
				}

				// Write dynamic properties.
				if (isDynamic) {
					forEachPublicProperty(object, function (i, value) {
						writeUTF8(ba, i, references);
						writeAMF3Value(ba, value, references);
					});
					writeUTF8(ba, '', references);
				}
			}
			return;
	}
}

export class AMF3 {
	public static write(ba: ByteArray, object: ASObject) {
		writeAMF3Value(ba, object, new AMF3ReferenceTables());
	}

	public static read(ba: ByteArray) {
		return readAMF3Value(ba, new AMF3ReferenceTables());
	}
}