import { AXApplicationDomain } from '../../run/AXApplicationDomain';
import { assert } from '@awayjs/graphics';
import { release, unexpected, warning } from '@awayfl/swf-loader';
import { Namespace } from './Namespace';
import { Multiname } from './Multiname';
import { MetadataInfo } from './MetadataInfo';
import { MethodInfo } from './MethodInfo';
import { MethodBodyInfo } from './MethodBodyInfo';
import { ClassInfo } from './ClassInfo';
import { ScriptInfo } from './ScriptInfo';
import { InstanceInfo } from './InstanceInfo';
import { CONSTANT } from './CONSTANT';
import { Errors } from '../../errors';
import { NamespaceType } from './NamespaceType';
import { internNamespace } from './internNamespace';
import { METHOD } from './METHOD';
import { ParameterInfo } from './ParameterInfo';
import { Traits } from './Traits';
import { TraitInfo } from './TraitInfo';
import { TRAIT } from './TRAIT';
import { SlotTraitInfo } from './SlotTraitInfo';
import { MethodTraitInfo } from './MethodTraitInfo';
import { ClassTraitInfo } from './ClassTraitInfo';
import { ATTR } from './ATTR';
import { ExceptionInfo } from './ExceptionInfo';
import { IndentingWriter } from '@awayfl/swf-loader';
import { AbcStream } from '../stream';

export class ABCFile {
	public ints: Int32Array;
	public uints: Uint32Array;
	public doubles: Float64Array;

	/**
     * Environment this ABC is loaded into.
     * In the shell, this is just a wrapper around an applicationDomain, but in the
     * SWF player, it's a flash.display.LoaderInfo object.
     */
	public env: {app: AXApplicationDomain; url: string};

	public get applicationDomain() {
		release || assert(this.env.app);
		return this.env.app;
	}

	private _stream: AbcStream;

	private _strings: string [];

	private _namespaces: Namespace [];

	private _namespaceSets: Namespace [][];

	private _multinames: Multiname [];

	private _deferredMultinames: number[][] = [];

	private _metadata: MetadataInfo [];

	private _methods: MethodInfo [];
	private _methodBodies: MethodBodyInfo [];

	public classes: ClassInfo [];
	public scripts: ScriptInfo [];
	public instances: InstanceInfo [];

	constructor(
		env: {app: AXApplicationDomain; url: string},
		private _buffer: Uint8Array
	) {
		this.env = env;
		this._stream = new AbcStream(_buffer);
		this._checkMagic();

		this._parseNumericConstants();
		this._parseStringConstants();
		this._parseNamespaces();
		this._parseNamespaceSets();
		this._parseMultinames();

		this._parseMethodInfos();
		this._parseMetaData();
		this._parseInstanceAndClassInfos();
		this._parseScriptInfos();
		this._parseMethodBodyInfos();
	}

	private _parseNumericConstants() {
		const s = this._stream;

		// Parse Signed Integers
		let n = s.readU30();
		const ints = new Int32Array(n);
		ints[0] = 0;
		for (let i = 1; i < n; i++) {
			ints[i] = s.readS32();
		}
		this.ints = ints;

		// Parse Unsigned Integers
		n = s.readU30();
		const uints = new Uint32Array(n);
		uints[0] = 0;
		for (let i = 1; i < n; i++) {
			uints[i] = s.readS32();
		}
		this.uints = uints;

		// Parse Doubles
		n = s.readU30();
		const doubles = new Float64Array(n);
		doubles[0] = NaN;
		for (let i = 1; i < n; i++) {
			doubles[i] = s.readDouble();
		}
		this.doubles = doubles;
	}

	private _parseStringConstants() {
		const s = this._stream;
		const n = s.readU30();
		this._strings = new Array(n);
		this._strings[0] = null;

		// Record the offset of each string in |stringOffsets|. This array has one extra
		// element so that we can compute the length of the last string.
		for (let i = 1; i < n; i++) {
			const l = s.readU30();
			this._strings[i] = s.readUTFString(l);
		}
	}

	private _parseNamespaces() {
		const s = this._stream;
		const n = s.readU30();
		this._namespaces = new Array(n);
		this._namespaces[0] = Namespace.PUBLIC;
		for (let i = 1; i < n; i++) {
			const kind = s.readU8();
			const uriIndex = s.readU30();
			let uri = this._strings[uriIndex];
			let type: NamespaceType;
			switch (kind) {
				case CONSTANT.Namespace:
				case CONSTANT.PackageNamespace:
					type = NamespaceType.Public;
					break;
				case CONSTANT.PackageInternalNs:
					type = NamespaceType.PackageInternal;
					break;
				case CONSTANT.ProtectedNamespace:
					type = NamespaceType.Protected;
					break;
				case CONSTANT.ExplicitNamespace:
					type = NamespaceType.Explicit;
					break;
				case CONSTANT.StaticProtectedNs:
					type = NamespaceType.StaticProtected;
					break;
				case CONSTANT.PrivateNs:
					type = NamespaceType.Private;
					break;
				default:
					this.applicationDomain.sec.throwError('VerifierError',
						Errors.CpoolEntryWrongTypeError, i);
			}
			if (uri && type !== NamespaceType.Private) {
				// TODO: deal with API versions here. Those are suffixed to the uri. We used to
				// just strip them out, but we also had an assert against them occurring at all,
				// so it might be the case that we don't even need to do anything at all.
			} else if (uri === null) {
				// Only private namespaces gets the empty string instead of undefined. A comment
				// in Tamarin source code indicates this might not be intentional, but oh well.
				uri = '';
			}
			this._namespaces[i] = internNamespace(type, uri);
		}
	}

	private _parseNamespaceSets() {
		const s = this._stream;
		const n = s.readU30();
		this._namespaceSets = new Array(n);
		this._namespaceSets[0] = null;
		for (let i = 1; i < n; i++) {
			const c = s.readU30(); // Count
			const nss = this._namespaceSets[i] = new Array(c);
			for (let j = 0; j < c; j++) {
				nss[j] = this._namespaces[s.readU30()];
			}
		}
	}

	private _parseMultinames() {
		const s = this._stream;
		const n = s.readU30();
		this._multinames = new Array(n);
		this._multinames[0] = null;
		for (let i = 1; i < n; i++) {
			this._multinames[i] = this._parseMultiname(i);
		}
		const o = s.position;
		while (this._deferredMultinames.length) {
			const [i, o] = this._deferredMultinames.shift();
			s.seek(o);
			this._multinames[i] = this._parseMultiname(i);
		}
		s.seek(o);
	}

	private _parseMultiname(i: number): Multiname {
		const stream = this._stream;
		const o = stream.position;

		let namespaceIsRuntime = false;
		let namespaceIndex;
		let useNamespaceSet = true;
		let nameIndex = 0;

		const kind = stream.readU8();
		switch (kind) {
			case CONSTANT.QName:
			case CONSTANT.QNameA:
				namespaceIndex = stream.readU30();
				useNamespaceSet = false;
				nameIndex = stream.readU30();
				break;
			case CONSTANT.RTQName: case CONSTANT.RTQNameA:
				namespaceIsRuntime = true;
				nameIndex = stream.readU30();
				break;
			case CONSTANT.RTQNameL: case CONSTANT.RTQNameLA:
				namespaceIsRuntime = true;
				break;
			case CONSTANT.Multiname: case CONSTANT.MultinameA:
				nameIndex = stream.readU30();
				namespaceIndex = stream.readU30();
				break;
			case CONSTANT.MultinameL: case CONSTANT.MultinameLA:
				namespaceIndex = stream.readU30();
				if (!release && namespaceIndex === 0) {
					// TODO: figure out what to do in this case. What would Tamarin do?
					warning('Invalid multiname: namespace-set index is 0');
				}
				break;
				/**
         * This is undocumented, looking at Tamarin source for this one.
         */
			case CONSTANT.TypeName: {
				const nameIndex = stream.readU32();
				const typeParameterCount = stream.readU32();
				if (!release && typeParameterCount !== 1) {
					// TODO: figure out what to do in this case. What would Tamarin do?
					warning('Invalid multiname: bad type parameter count ' + typeParameterCount);
				}
				const typeParameter = this._multinames[stream.readU32()];
				const factory = this._multinames[nameIndex];

				if (typeParameter == null || factory == null) {
					this._deferredMultinames.push([i, o]);
					return;
				}
				return new Multiname(this, i, kind, factory.namespaces, factory.name, typeParameter);
			}
			default:
				unexpected();
				break;
		}

		// A name index of 0 means that it's a runtime name.
		const name = nameIndex === 0 ? null : this._strings[nameIndex];
		const namespaces = namespaceIsRuntime
			? null
			: useNamespaceSet
				? this._namespaceSets[namespaceIndex]
				: [this._namespaces[namespaceIndex]];

		return new Multiname(this, i, kind, namespaces, name);
	}

	private _checkMagic() {
		const magic = this._stream.readWord();
		const flashPlayerBrannan = 46 << 16 | 15;
		if (magic < flashPlayerBrannan) {
			this.env.app.sec.throwError('VerifierError', Errors.InvalidMagicError, magic >> 16,
				magic & 0xffff);
		}
	}

	/**
     * String duplicates exist in practice but are extremely rare.
     */
	private _checkForDuplicateStrings(): boolean {
		const a = [];
		for (let i = 0; i < this._strings.length; i++) {
			a.push(this._strings[i]);
		}
		a.sort();
		for (let i = 0; i < a.length - 1; i++) {
			if (a[i] === a[i + 1]) {
				return true;
			}
		}
		return false;
	}

	/**
     * Returns the string at the specified index in the string table.
     */
	public getString(i: number): string {
		release || assert(i >= 0 && i < this._strings.length);
		return this._strings[i];
	}

	/**
     * Returns the multiname at the specified index in the multiname table.
     */
	public getMultiname(i: number): Multiname {
		if (i < 0 || i >= this._multinames.length) {
			this.applicationDomain.sec.throwError('VerifierError',
				Errors.CpoolIndexRangeError, i,
				this._multinames.length);
		}

		return (i !== 0) ? this._multinames[i] || (this._multinames[i] = this._parseMultiname(i)) : null;
	}

	/**
     * Returns the namespace at the specified index in the namespace table.
     */
	public getNamespace(i: number): Namespace {
		if (i < 0 || i >= this._namespaces.length) {
			this.applicationDomain.sec.throwError('VerifierError', Errors.CpoolIndexRangeError, i,
				this._namespaces.length);
		}

		return this._namespaces[i];
	}

	/**
     * Returns the namespace set at the specified index in the namespace set table.
     */
	public getNamespaceSet(i: number): Namespace [] {
		if (i < 0 || i >= this._namespaceSets.length) {
			this.applicationDomain.sec.throwError('VerifierError', Errors.CpoolIndexRangeError, i,
				this._namespaceSets.length);
		}

		return this._namespaceSets[i];
	}

	private _parseMethodInfos() {
		const s = this._stream;
		const n = s.readU30();
		this._methods = new Array(n);
		for (let i = 0; i < n; ++i) {
			this._methods[i] = this._parseMethodInfo(i);
		}
	}

	private _parseMethodInfo(j: number) {
		const s = this._stream;
		const parameterCount = s.readU30();
		const returnType = this._multinames[s.readU30()];
		const parameters = new Array<ParameterInfo>(parameterCount);
		for (let i = 0; i < parameterCount; i++) {
			parameters[i] = new ParameterInfo(this, this._multinames[s.readU30()]);
		}
		const name = this._strings[s.readU30()] || 'anonymous';
		const flags = s.readU8();
		let optionalCount = 0;
		if (flags & METHOD.HasOptional) {
			optionalCount = s.readU30();
			release || assert(parameterCount >= optionalCount);
			for (let i = parameterCount - optionalCount; i < parameterCount; i++) {
				parameters[i].optionalValueIndex = s.readU30();
				parameters[i].optionalValueKind = s.readU8();
			}
		}
		if (flags & METHOD.HasParamNames) {
			for (let i = 0; i < parameterCount; i++) {
				// NOTE: We can't get the parameter name as described in the spec because some SWFs have
				// invalid parameter names. Tamarin ignores parameter names and so do we.
				parameters[i].name = this._strings[s.readU30()];
			}
		}
		return new MethodInfo(this, j, name, returnType, parameters, optionalCount, flags);
	}

	/**
     * Returns the method info at the specified index in the method info table.
     */
	public getMethodInfo(i: number) {
		release || assert(i >= 0 && i < this._methods.length);
		return this._methods[i];
	}

	public getMethodBodyInfo(i: number) {
		return this._methodBodies[i];
	}

	private _parseMetaData() {
		const s = this._stream;
		const n = s.readU30();
		this._metadata = new Array(n);
		for (let i = 0; i < n; i++) {
			const name = this._strings[s.readU30()]; // Name
			const itemCount = s.readU30(); // Item Count
			const keys = new Array(itemCount);
			for (let j = 0; j < itemCount; j++) {
				keys[j] = this._strings[s.readU30()];
			}
			const values = new Array(itemCount);
			for (let j = 0; j < itemCount; j++) {
				values[j] = this._strings[s.readU30()];
			}
			this._metadata[i] = new MetadataInfo(this, name, keys, values);
		}
	}

	public getMetadataInfo(i: number): MetadataInfo {
		release || assert(i >= 0 && i < this._metadata.length);
		return this._metadata[i];
	}

	private _parseInstanceAndClassInfos() {
		const s = this._stream;
		const n = s.readU30();
		const instances = this.instances = new Array(n);
		for (let i = 0; i < n; i++) {
			instances[i] = this._parseInstanceInfo();
		}
		this._parseClassInfos(n);
		for (let i = 0; i < n; i++) {
			instances[i].classInfo = this.classes[i];
		}
	}

	private _parseInstanceInfo(): InstanceInfo {
		const s = this._stream;
		const multiname = this._multinames[s.readU30()];
		const superName = this._multinames[s.readU30()];
		const flags = s.readU8();
		const protectedNs = (flags & CONSTANT.ClassProtectedNs) ? this._namespaces[s.readU30()] : Namespace.PUBLIC;
		const interfaceCount = s.readU30();
		const interfaces: Multiname[] = [];
		for (let i = 0; i < interfaceCount; i++) {
			interfaces[i] = this._multinames[s.readU30()];
		}
		const methodInfo = this._methods[s.readU30()];
		const traits = this._parseTraits();
		const instanceInfo = new InstanceInfo(this, multiname, superName, flags, protectedNs,
			interfaces, methodInfo, traits);
		traits.attachHolder(instanceInfo);
		return instanceInfo;
	}

	private _parseTraits(global: boolean = false) {
		const s = this._stream;
		const n = s.readU30();
		const traits = new Array(n);
		for (let i = 0; i < n; i++) {
			traits[i] = this._parseTrait();
		}
		return new Traits(traits, global);
	}

	private _parseTrait() {
		const s = this._stream;
		const multiname = this._multinames[s.readU30()];
		const tag = s.readU8();

		const kind = tag & 0x0F;
		const attributes = (tag >> 4) & 0x0F;

		let trait: TraitInfo;
		switch (kind) {
			case TRAIT.Slot:
			case TRAIT.Const: {
				const slot = s.readU30();
				const typeName = this._multinames[s.readU30()];
				const valueIndex = s.readU30();
				let valueKind = -1;
				if (valueIndex !== 0) {
					valueKind = s.readU8();
				}
				trait = new SlotTraitInfo(this, kind, multiname, slot, typeName, valueKind, valueIndex);
				break;
			}
			case TRAIT.Method:
			case TRAIT.Getter:
			case TRAIT.Setter: {
				s.readU30(); // Tamarin optimization.
				const methodInfo = this._methods[s.readU30()];
				trait = methodInfo.trait = new MethodTraitInfo(this, kind, multiname, methodInfo);
				break;
			}
			case TRAIT.Class: {
				const slot = s.readU30();
				const classInfo = this.classes[s.readU30()];
				trait = classInfo.trait = new ClassTraitInfo(this, kind, multiname, slot, classInfo);
				break;
			}
			default:
				this.applicationDomain.sec.throwError('VerifierError',
					Errors.UnsupportedTraitsKindError, kind);
		}

		if (attributes & ATTR.Metadata) {
			const n = s.readU30();
			const metadata: MetadataInfo[] = new Array(n);
			for (let i = 0; i < n; i++) {
				metadata[i] = this._metadata[s.readU30()];
			}
			trait.metadata = metadata;
		}
		return trait;
	}

	private _parseClassInfos(n: number) {
		const classes = this.classes = new Array(n);
		for (let i = 0; i < n; i++) {
			classes[i] = this._parseClassInfo(i);
		}
	}

	private _parseClassInfo(i: number) {
		const methodInfo = this._methods[this._stream.readU30()];
		const traits = this._parseTraits(true);
		const classInfo = new ClassInfo(this, this.instances[i], methodInfo, traits);
		traits.attachHolder(classInfo);
		return classInfo;
	}

	private _parseScriptInfos() {
		const n = this._stream.readU30();
		const scripts = this.scripts = new Array(n);
		for (let i = 0; i < n; i++) {
			scripts[i] = this._parseScriptInfo();
		}
	}

	private _parseScriptInfo() {
		const methodInfo = this._methods[this._stream.readU30()];
		const traits = this._parseTraits(true);
		const scriptInfo = new ScriptInfo(this, methodInfo, traits);
		traits.attachHolder(scriptInfo);
		return scriptInfo;
	}

	private _parseMethodBodyInfos() {
		const s = this._stream;
		const methodBodies = this._methodBodies = new Array(this._methods.length);
		const n = s.readU30();
		for (let i = 0; i < n; i++) {
			const methodInfo = s.readU30();
			const maxStack = s.readU30();
			const localCount = s.readU30();
			const initScopeDepth = s.readU30();
			const maxScopeDepth = s.readU30();
			const code = s.viewU8s(s.readU30());

			const e = s.readU30();
			const exceptions = new Array(e);
			for (let j = 0; j < e; ++j) {
				exceptions[j] = this._parseException();
			}
			const traits = this._parseTraits();
			methodBodies[methodInfo]
				= new MethodBodyInfo(maxStack, localCount, initScopeDepth, maxScopeDepth, code, exceptions, traits);
			traits.attachHolder(methodBodies[methodInfo]);
		}
	}

	private _parseException() {
		const s = this._stream;
		const start = s.readU30();
		const end = s.readU30();
		const target = s.readU30();
		const typeIndex = s.readU30();
		const nameIndex = s.readU30();
		return new ExceptionInfo(this, start, end, target, this._multinames[nameIndex], this._multinames[typeIndex]);
	}

	public getConstant(kind: CONSTANT, i: number): any {
		switch (kind) {
			case CONSTANT.Int:
				return this.ints[i];
			case CONSTANT.UInt:
				return this.uints[i];
			case CONSTANT.Double:
				return this.doubles[i];
			case CONSTANT.Utf8:
				return this._strings[i];
			case CONSTANT.True:
				return true;
			case CONSTANT.False:
				return false;
			case CONSTANT.Null:
				return null;
			case CONSTANT.Undefined:
				return undefined;
			case CONSTANT.Namespace:
			case CONSTANT.PackageInternalNs:
				return this._namespaces[i];
			case CONSTANT.QName:
			case CONSTANT.MultinameA:
			case CONSTANT.RTQName:
			case CONSTANT.RTQNameA:
			case CONSTANT.RTQNameL:
			case CONSTANT.RTQNameLA:
			case CONSTANT.NameL:
			case CONSTANT.NameLA:
				return this._multinames[i];
			case CONSTANT.Float:
				warning('TODO: CONSTANT.Float may be deprecated?');
				break;
			default:
				release || assert(false, 'Not Implemented Kind ' + kind);
		}
	}

	trace(writer: IndentingWriter) {
		writer.writeLn('Multinames: ' + this._multinames.length);

		writer.indent();
		for (let i = 0; i < this._multinames.length; i++) {
			writer.writeLn(i + ' ' + this._multinames[i]);
		}
		writer.outdent();

		writer.writeLn('Namespace Sets: ' + this._namespaceSets.length);

		writer.indent();
		for (let i = 0; i < this._namespaceSets.length; i++) {
			writer.writeLn(i + ' ' + this._multinames[i]);
		}
		writer.outdent();

		writer.writeLn('Namespaces: ' + this._namespaces.length);

		writer.indent();
		for (let i = 0; i < this._namespaces.length; i++) {
			writer.writeLn(i + ' ' + this._namespaces[i]);
		}
		writer.outdent();

		writer.writeLn('Strings: ' + this._strings.length);

		writer.indent();
		for (let i = 0; i < this._strings.length; i++) {
			writer.writeLn(i + ' ' + this._strings[i]);
		}
		writer.outdent();

		writer.writeLn('MethodInfos: ' + this._methods.length);

		writer.indent();
		for (let i = 0; i < this._methods.length; i++) {
			writer.writeLn(i + ' ' + this.getMethodInfo(i));
			if (this._methodBodies[i]) {
				this._methodBodies[i].trace(writer);
			}
		}
		writer.outdent();

		writer.writeLn('InstanceInfos: ' + this.instances.length);

		writer.indent();
		for (let i = 0; i < this.instances.length; i++) {
			writer.writeLn(i + ' ' + this.instances[i]);
			this.instances[i].trace(writer);
		}
		writer.outdent();

		writer.writeLn('ClassInfos: ' + this.classes.length);

		writer.indent();
		for (let i = 0; i < this.classes.length; i++) {
			this.classes[i].trace(writer);
		}
		writer.outdent();

		writer.writeLn('ScriptInfos: ' + this.scripts.length);

		writer.indent();
		for (let i = 0; i < this.scripts.length; i++) {
			this.scripts[i].trace(writer);
		}
		writer.outdent();
	}
}