/*
 *  Copyright 2010, 2011, 2012, 2013, 2014, 2015 Vladimir Panteleev <vladimir@thecybershadow.net>
 *  This file is part of RABCDAsm.
 *
 *  RABCDAsm is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  RABCDAsm 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 RABCDAsm.  If not, see <http://www.gnu.org/licenses/>.
 */

module disassembler;

import std.algorithm;
import std.array;
import std.conv;
import std.digest.md;
import std.exception;
import std.file;
import std.format;
import std.path;
import std.stdio;
import std.string;
import abcfile;
import asprogram;
import autodata;
import common;

alias std.array.join join;

final class StringBuilder
{
	enum BUF_SIZE = 256*1024;

	static char[] buf;
	static size_t pos;

	string filename;
	File file;

	this(string filename)
	{
		this.filename = filename;
		if (exists(longPath(filename)))
			throw new Exception(filename ~ " exists");

		string[] dirSegments = split(filename, "/");
		for (int l=0; l<dirSegments.length-1; l++)
		{
			auto subdir = join(dirSegments[0..l+1], "/");
			if (subdir.length && !exists(longPath(subdir)))
				mkdir(longPath(subdir));
		}
		file = openFile(filename, "wb");
		assert(!pos, "Opening new file with unflushed buffer");
	}

	static this()
	{
		buf = new char[BUF_SIZE];
	}

	void put(in char[] s)
	{
		checkIndent();
		auto end = pos + s.length;
		if (end > buf.length)
		{
			flush();
			end = s.length;
			while (end > buf.length)
				buf.length = buf.length*2;
		}
		buf[pos..end] = s[];
		pos = end;
	}

	void put(char c)
	{
		if (pos == buf.length) // speed hack: no indent check
			flush();
		buf[pos++] = c;
	}

	void opOpAssign(string op : "~", V)(V s) {
		put(s);
	}
	alias put opCatAssign;

	void write(T)(T v)
	{
		checkIndent();
		formattedWrite(this, "%s", v);
	}

	void flush()
	{
		if (pos)
		{
			file.rawWrite(buf[0..pos]);
			pos = 0;
		}
	}

	void save()
	{
		flush();
		file.close();
	}

	int indent;
	bool indented;
	string linePrefix;

	void newLine()
	{
		this ~= '\n';
		indented = false;
	}

	void noIndent()
	{
		indented = true;
	}

	void checkIndent()
	{
		if (!indented)
		{
			for (int i=0; i<indent; i++)
				this ~= ' ';
			indented = true;
			if (linePrefix)
				this ~= linePrefix;
		}
	}
}

final class RefBuilder : ASTraitsVisitor
{
	bool[uint][string][ASType.Max] homonyms;
	debug bool homonymsBuilt;

	bool hasHomonyms(ASProgram.Namespace ns)
	{
		debug assert(homonymsBuilt);
		auto pnsHomonyms = ns.name in homonyms[ns.kind];
		auto nsHomonyms = pnsHomonyms ? *pnsHomonyms : null;
		return nsHomonyms.length > 1;
	}

	void addHomonym(ASProgram.Namespace ns)
	{
		debug assert(!homonymsBuilt);
		homonyms[ns.kind][ns.name][ns.id] = true;
	}

	/// Represents a link in a "context chain", which represents the context in which an object is encountered.
	/// The common root of contexts is used to build ref strings, namespace labels, file paths etc.
	/// Expansion is recursive and delayed until decompilation (an object may be encountered inside a private namespace,
	/// all contexts of which are unknown until the whole program is scanned).
	struct ContextItem
	{
		enum Type
		{
			Multiname, /// An ASProgram.Multiname (may be a private namespace, etc.)
			String,    /// Fixed string
			Group,     /// Multiple ContextItem[]s (which are expanded and the common root is taken as the result)
		}
		Type type;

		union
		{
			ASProgram.Multiname multiname;
			struct
			{
				string str;
				bool filenameSuffix;
			}
			struct
			{
				ContextItem[] group;
				string groupFallback;
			}
		}

		struct Segment
		{
			char delim;
			string str;
		}

		ContextItem[] reduceGroup(RefBuilder refs)
		{
			assert(type==Type.Group);

			ContextItem[][] contexts;
			foreach (context; group)
				contexts ~= ContextItem.expand(refs, [context]);

			ContextItem[] context;
			if (contexts.length)
				context = reduce!contextRoot(contexts);
			if (!context.length)
				context = /*null*/[ContextItem(groupFallback)];
			return context;
		}

		Segment[] toSegments(RefBuilder refs, bool filename)
		out(result)
		{
			debug(CONTEXTS) std.stdio.writefln("Segmented:\n\t%s\nto\n\t%s\n", this, result);
		}
		body
		{
			final switch(type)
			{
				case Type.Multiname:
				{
					assert(multiname.kind == ASType.QName);
					auto ns = multiname.vQName.ns;
					auto nsName = ns.name;
				//	if (refs.hasHomonyms(ns))
				//		nsName ~= '#' ~ to!string(ns.id);
					if (nsName.length)
						if (multiname.vQName.name.length)
							return [Segment('/', nsName), Segment(filename ? '/' : ':', multiname.vQName.name)];
						else
							return [Segment('/', nsName)];
					else
						if (multiname.vQName.name.length)
							return [Segment('/', multiname.vQName.name)];
						else
							assert(0);
				}
				case Type.String:
					return [Segment((filename && filenameSuffix) ? '.' : '/', str)];
				case Type.Group:
				{
					Segment[] segments;
					foreach (context; reduceGroup(refs))
						segments ~= context.toSegments(refs, filename);
					return segments;
				}
			}
		}

		static ContextItem[] expand(RefBuilder refs, ContextItem[] context)
		{
			ContextItem[] newContext;
			foreach (ref c; context)
			{
				auto cEx = c.expand(refs);
				if (cEx)
					newContext ~= cEx;
			}
			return newContext;
		}

		bool expanding;

		/// Recursively expand contexts (lower to Type.String ContextItems)
		ContextItem[] expand(RefBuilder refs)
		in
		{
			debug(CONTEXTS) std.stdio.writefln("Expanding:\n\t%s\n", this);
		}
		out(result)
		{
			debug(CONTEXTS) std.stdio.writefln("Expanded:\n\t%s\nto\n\t%s\n", this, result);
		}
		body
		{
			if (expanding)
				final switch (type)
				{
					case Type.String:
						assert(0);
					case Type.Multiname:
						return (&this)[0..1];
					case Type.Group:
						return [ContextItem(groupFallback)];
				}

			assert(!expanding);
			expanding = true;
			scope(exit) expanding = false;

			final switch (type)
			{
				case Type.Multiname:
					switch (multiname.kind)
					{
						case ASType.QName:
						{
							auto ns = multiname.vQName.ns;
							if (ns.kind == ASType.PrivateNamespace)
							{
							//	auto pcontext = ns.id in refs.namespaces[ns.kind].contexts;
							//	if (pcontext is null)
							//		return (&this)[0..1];
							//	assert(pcontext);
								auto context = refs.namespaces[ns.kind].getContext(refs, ns.id);
								debug(CONTEXTS) std.stdio.writefln("Context of namespace %s is:\n\t%s\n", ns, context);
							//	auto expanded = expand(refs, context);
							//	if (expanded is null) return null;
								return /*expanded*/context ~ (multiname.vQName.name.length ? [ContextItem(multiname.vQName.name)] : null); // hack
							}
							break;
						}
						case ASType.Multiname:
							return multiname.vMultiname.name.length ? [ContextItem(multiname.vMultiname.name)] : null;
						default:
							debug
								assert(false, text(multiname.kind));
							else
								break;
					}
					break;
				case Type.String:
					break;
				case Type.Group:
					return reduceGroup(refs);
			}

			return (&this)[0..1];
		}

		this(ASProgram.Multiname m)
		{
			this.type = Type.Multiname;
			this.multiname = m;
		}

		this(string s, bool filenameSuffix = false)
		{
			this.type = Type.String;
			this.str = s;
			this.filenameSuffix = filenameSuffix;
		}

		this(ContextItem[] group, string groupFallback)
		{
			this.type = Type.Group;
			this.group = group;
			this.groupFallback = groupFallback;
		}

		mixin AutoCompare;
		mixin AutoToString;

		R processData(R, string prolog, string epilog, H)(ref H handler) const
		{
			mixin(prolog);
			mixin(addAutoField("type"));
			final switch (type)
			{
				case Type.Multiname:
					mixin(addAutoField("multiname"));
					break;
				case Type.String:
					mixin(addAutoField("str"));
					mixin(addAutoField("filenameSuffix"));
					break;
				case Type.Group:
					mixin(addAutoField("group"));
					mixin(addAutoField("groupFallback"));
					break;
			}
			mixin(epilog);
		}

		static bool similar(ref ContextItem i1, ref ContextItem i2)
		{
			if (i1.type != i2.type) return false;
			final switch (i1.type)
			{
				case ContextItem.Type.String:
					return i1.str == i2.str;
				case ContextItem.Type.Multiname:
					assert(i1.multiname.kind == ASType.QName && i2.multiname.kind == ASType.QName);
					if (i1.multiname.vQName.name != i2.multiname.vQName.name) return false;
					return nsSimilar(i1.multiname.vQName.ns, i2.multiname.vQName.ns);
				case ContextItem.Type.Group:
					return i1.group == i2.group;
			}
		}

		// truncate=true  -> return partial ContextItem
		// truncate=false -> return null on partial match
		static ContextItem[] combine(bool truncate)(ref ContextItem c1, ref ContextItem c2)
		{
			if (similar(c1, c2))
				return [c1];

			if (c1.type != ContextItem.Type.Multiname || c2.type != ContextItem.Type.Multiname)
				return null;
			if (c1.multiname.kind != ASType.QName || c2.multiname.kind != ASType.QName)
				return null;

			auto name1 = c1.multiname.vQName.name;
			auto name2 = c2.multiname.vQName.name;
			auto ns1   = c1.multiname.vQName.ns;
			auto ns2   = c2.multiname.vQName.ns;

			if (nsSimilar(ns1, ns2) && ns1.name.length && truncate)
			{
				auto m = new ASProgram.Multiname;
				m.kind = ASType.QName;
				m.vQName.ns = ns1;
				return [ContextItem(m)];
			}

			if (name1 && !name2 && truncate)
			{
				swap(c1, c2);
				swap(ns1, ns2);
				swap(name1, name2);
			}

			if (!name1 && name2 && nsSimilar(ns1, ns2))
			{
				if (truncate)
				{
					auto m = new ASProgram.Multiname;
					m.kind = ASType.QName;
					m.vQName.ns = ns1;
					return [ContextItem(m)];
				}
				else
					return [c2];
			}

			if (ns1.name.length && ns2.name.length)
			{
				if (nsSimilar(ns1, ns2))
				{
					assert(name1 != name2); // handled by similar() check
					static if (truncate)
						assert(false); // handled above
					else
					{
						if (name2.length)
							return [c1, ContextItem(name2)];
						else
							return [c1];
					}
				}

				if (ns1.name.length > ns2.name.length && truncate)
				{
					swap(c1, c2);
					swap(ns1, ns2);
					swap(name1, name2);
				}

				auto fullName1 = ns1.name ~ (name1 ? ':' ~ name1 : "");
				auto fullName2 = ns2.name ~ (name2 ? ':' ~ name2 : "");
				if (fullName2.startsWith(fullName1 ~ ":"))
					return [truncate ? c1 : c2];
			}

			return null;
		}

		alias combine!true commonRoot;
		alias combine!false deduplicate;
	}

	ContextItem[] context; // potential optimization: use array-based stack

	void pushContext(T...)(T v) { context ~= ContextItem(v); }
	void popContext() { context = context[0..$-1]; }

	enum ContextPriority
	{
		declaration,
		usage,
		orphan,
		max
	}

	struct ContextSet(T, bool ALLOW_DUPLICATES)
	{
		ContextItem[][T] contexts;
		ContextItem[][][ContextPriority.max][T] contextSets;
		debug bool contextsSealed;

		string[T] names, filenames;
		debug bool coagulated;

		bool add(U)(U obj, ContextItem[] context, ContextPriority priority)
		{
			debug assert(!coagulated);
			debug assert(!contextsSealed);

			auto p = cast(T)obj;
			auto pset = p in contextSets;

			if (!pset)
			{
				contextSets[p] = contextSets[p].init;
				contextSets[p][priority] ~= context.dup;
				return true;
			}
			else
			{
				static bool rawEqual(T)(T[] arr1, T[] arr2) { return cast(ubyte[])arr1 == cast(ubyte[])arr2; }
				if ((*pset)[priority].length==0 || !rawEqual((*pset)[priority][$-1], context)) // Optimization: don't add contexts identical to the last added
					(*pset)[priority] ~= context.dup;
				return false;
			}
		}

		bool addIfNew(U)(U obj, ContextItem[] context, ContextPriority priority)
		{
			if (isAdded(obj))
				return false;
			else
				return add(obj, context, priority);
		}

		void coagulate(RefBuilder refs)
		{
			debug assert(!coagulated);

			int[string] collisionCounter;
			T[string] first;

			foreach (obj, set; contextSets)
				if (obj !in contexts)
					getContext(refs, obj);

			foreach (obj; contexts.keys.sort())
			{
				auto context = contexts[obj];
				auto bname = refs.contextToString(context, false);
				auto bfilename = refs.contextToString(context, true);
				auto pcounter = bname in collisionCounter;
				int counter = pcounter ? *pcounter : 0;
				if (counter==1)
				{
					auto firstObj = first[bname];
					names[firstObj] ~= "#0";
					filenames[firstObj] ~= "#0";
				}

				string suffix;
				if (counter==0)
					first[bname] = cast(T)obj;
				else
					suffix = '#' ~ to!string(counter);
				names[obj] = bname ~ suffix;
				filenames[obj] = bfilename ~ suffix;
				collisionCounter[bname] = counter+1;
			}

			debug coagulated = true;
		}

		bool isAdded(U)(U obj) { return (cast(T)obj in contextSets) ? true : false; }

		ContextItem[] getContext(U)(RefBuilder refs, U obj)
		{
		//	debug assert(coagulated);
			debug contextsSealed = true;

			auto p = cast(T)obj;
			auto pcontext = p in contexts;
			if (pcontext)
				return *pcontext;

			ContextItem[][] set;
			foreach (prioritySet; contextSets[p])
				if (prioritySet)
					set = prioritySet;

			static if (ALLOW_DUPLICATES)
			{
				auto context = ContextItem.expand(refs, set[0]);
				foreach (setContext; set[1..$])
					context = contextRoot(context, ContextItem.expand(refs, setContext));
				return contexts[p] = context;
			}
			else
			{
				if (set.length > 1)
					return contexts[p] = [ContextItem("multireferenced")];
				else
					return contexts[p] = ContextItem.expand(refs, set[0]);
			}
		}

		string getName(U)(U obj)
		{
			debug assert(coagulated);

			auto pname = cast(T)obj in names;
		//	return pname?*pname:"##" ~ format(obj);
			assert(pname, format("Unscanned object: ", obj));
			return *pname;
		}

		static string[string] filenameMappings;

		string getFilename(U)(U obj, string suffix)
		{
			debug assert(coagulated);

			auto pname = cast(T)obj in filenames;
			assert(pname, format("Unscanned object: ", obj));
			auto filename = *pname;

			string[] dirSegments = split(filename, "/");
			for (int l=0; l<dirSegments.length; l++)
			{
			again:
				string subpath = join(dirSegments[0..l+1], "/");
				string subpathl = toLower(subpath);
				string* canonicalp = subpathl in filenameMappings;
				if (canonicalp && *canonicalp != subpath)
				{
					dirSegments[l] = dirSegments[l] ~ "_"; // not ~=
					goto again;
				}
				filenameMappings[subpathl] = subpath;
			}
			filename = join(dirSegments, "/");

			return filename ~ "." ~ suffix ~ ".asasm";
		}
	}

	ContextSet!(uint, true)[ASType.Max] namespaces;
	ContextSet!(void*, false) objects, scripts;

	this(ASProgram as)
	{
		super(as);
	}

	bool[void*] orphans;
	void addOrphan(T)(T obj) { orphans[cast(void*)obj] = true; }
	bool isOrphan(T)(T obj) { return (cast(void*)obj in orphans) ? true : false; }

	override void run()
	{
		foreach (i, vclass; as.orphanClasses)
			addOrphan(vclass);
		foreach (i, method; as.orphanMethods)
			addOrphan(method);

		super.run();

		foreach (i, v; as.scripts)
		{
			ContextItem[] classContexts;

			foreach (trait; v.traits)
			{
				if (trait.name.kind == ASType.QName && trait.name.vQName.ns.kind != ASType.PrivateNamespace)
					classContexts ~= ContextItem(trait.name);
			}

			if (!classContexts.length)
				foreach (trait; v.traits)
					classContexts ~= ContextItem(trait.name);

			context = [ContextItem(classContexts, "script_" ~ to!string(i))];
			scripts.add(v, context, ContextPriority.declaration);
			pushContext("init", true);
			addMethod(v.sinit, ContextPriority.declaration);
			context = null;
		}
		foreach (i, vclass; as.orphanClasses)
			if (!objects.isAdded(vclass))
			{
				pushContext("orphan_class_" ~ to!string(i));
				addClass(vclass, ContextPriority.orphan);
				popContext();
			}
		foreach (i, method; as.orphanMethods)
			if (!objects.isAdded(method))
			{
				pushContext("orphan_method_" ~ to!string(i));
				addMethod(method, ContextPriority.orphan);
				popContext();
			}

		scripts.coagulate(this);

		// add namespaces referenced only at script level
		foreach (v; as.scripts)
			foreach (trait; v.traits)
				if (trait.name.kind == ASType.QName)
					namespaces[trait.name.vQName.ns.kind].addIfNew(trait.name.vQName.ns.id, scripts.getContext(this, v), ContextPriority.declaration);

		foreach (id, b; possibleOrphanPrivateNamespaces)
			if (!namespaces[ASType.PrivateNamespace].isAdded(id))
			{
				pushContext("orphan_namespace_" ~ to!string(id));
				namespaces[ASType.PrivateNamespace].add(id, context, ContextPriority.orphan);
				popContext();
			}

		debug homonymsBuilt = true;
		foreach (ref namespace; namespaces)
			namespace.coagulate(this);
		objects.coagulate(this);
	}

	override void visitTrait(ref ASProgram.Trait trait)
	{
		auto m = trait.name;

	//	if (m.kind != ASType.QName)
	//		throw new Exception("Trait name is not a QName");

		pushContext(m);
		visitMultiname(m, ContextPriority.declaration);
		switch (trait.kind)
		{
			case TraitKind.Slot:
			case TraitKind.Const:
				visitMultiname(trait.vSlot.typeName, ContextPriority.usage);

				super.visitTrait(trait);
				break;
			case TraitKind.Class:
				addClass(trait.vClass.vclass, ContextPriority.declaration);

				pushContext("class", true);
				visitTraits(trait.vClass.vclass.traits);
				popContext();

				pushContext("instance", true);
				visitTraits(trait.vClass.vclass.instance.traits);
				popContext();

				break;
			case TraitKind.Function:
				addMethod(trait.vFunction.vfunction, ContextPriority.declaration);

				super.visitTrait(trait);
				break;
			case TraitKind.Method:
				addMethod(trait.vMethod.vmethod, ContextPriority.declaration);

				super.visitTrait(trait);
				break;
			case TraitKind.Getter:
				pushContext("getter");
				addMethod(trait.vMethod.vmethod, ContextPriority.declaration);
				popContext();

				super.visitTrait(trait);
				break;
			case TraitKind.Setter:
				pushContext("setter");
				addMethod(trait.vMethod.vmethod, ContextPriority.declaration);
				popContext();

				super.visitTrait(trait);
				break;
			default:
				super.visitTrait(trait);
				break;
		}
		popContext();
	}

	static ContextItem[] contextRoot(ContextItem[] c1, ContextItem[] c2)
	{
		static bool uninteresting(ContextItem[] c)
		{
			// Do some validation while we're at it
			foreach (cc; c)
				assert(cc.type != ContextItem.Type.Group, "Groups should be expanded by now");

			return
				(c.length==1 && c[0].type==ContextItem.Type.String && c[0].str.startsWith("script_") && c[0].str.endsWith("_sinit")) ||
				(c.length==1 && c[0].type==ContextItem.Type.String && c[0].str.startsWith("orphan_method_")) ||
				false;
		}

		if (uninteresting(c1)) return c2;
		if (uninteresting(c2)) return c1;

		ContextItem[] c;
		while (c.length<c1.length && c.length<c2.length)
		{
			auto root = ContextItem.commonRoot(c1[c.length], c2[c.length]);
			assert(root.length <= 1);
			if (root.length)
				c ~= root;
			else
				break;
		}
		return c;
	}

	bool[uint] possibleOrphanPrivateNamespaces;

	void visitNamespace(ASProgram.Namespace ns, ContextPriority priority)
	{
		if (ns is null) return;

		// Add to homonyms

		addHomonym(ns);

		// Add to namespaces (for context)

		assert(context.length > 0, "No context");

		auto myPos = context.length;
		foreach (i, ref item; context)
			if (item.type == ContextItem.Type.Multiname && item.multiname.kind == ASType.QName && item.multiname.vQName.ns == ns)
			{
				myPos = i;
				break;
			}
		if (ns.kind == ASType.PrivateNamespace && myPos == 0)
		{
			possibleOrphanPrivateNamespaces[ns.id] = true;
			return;
		}

		auto myContext = context[0..myPos];
		namespaces[ns.kind].add(ns.id, myContext, priority);
	}

	void visitNamespaceSet(ASProgram.Namespace[] nsSet, ContextPriority priority)
	{
		foreach (ns; nsSet)
			visitNamespace(ns, priority);
	}

	void visitMultiname(ASProgram.Multiname m, ContextPriority priority)
	{
		if (m is null) return;
		with (m)
			switch (kind)
			{
				case ASType.QName:
				case ASType.QNameA:
					visitNamespace(vQName.ns, priority);
					break;
				case ASType.Multiname:
				case ASType.MultinameA:
					visitNamespaceSet(vMultiname.nsSet, priority);
					break;
				case ASType.MultinameL:
				case ASType.MultinameLA:
					visitNamespaceSet(vMultinameL.nsSet, priority);
					break;
				case ASType.TypeName:
					visitMultiname(vTypeName.name, priority);
					foreach (param; vTypeName.params)
						visitMultiname(param, priority);
					break;
				default:
					break;
			}
	}

	void visitMethodBody(ASProgram.MethodBody b)
	{
		foreach (ref instruction; b.instructions)
			foreach (i, type; opcodeInfo[instruction.opcode].argumentTypes)
				switch (type)
				{
					case OpcodeArgumentType.Namespace:
						visitNamespace(instruction.arguments[i].namespacev, ContextPriority.usage);
						break;
					case OpcodeArgumentType.Multiname:
						visitMultiname(instruction.arguments[i].multinamev, ContextPriority.usage);
						break;
					case OpcodeArgumentType.Class:
						pushContext("inline_class");
						if (isOrphan(instruction.arguments[i].classv))
							addClass(instruction.arguments[i].classv, ContextPriority.usage);
						popContext();
						break;
					case OpcodeArgumentType.Method:
						pushContext("inline_method");
						if (isOrphan(instruction.arguments[i].methodv))
							addMethod(instruction.arguments[i].methodv, ContextPriority.usage);
						popContext();
						break;
					default:
						break;
				}
	}

	string contextToString(ContextItem[] context, bool filename)
	in
	{
		debug(CONTEXTS) std.stdio.writefln("Stringizing:\n\t%s\n", context);
	}
	out(result)
	{
		debug(CONTEXTS) std.stdio.writefln("Stringized:\n\t%s\nto\n\t%s\n------------------\n", context, result);
	}
	body
	{
		context = ContextItem.expand(this, context);
		if (!context.length)
			return null;

		foreach_reverse (i; 0..context.length-1)
		{
			auto root = ContextItem.deduplicate(context[i], context[i+1]);
			if (root.length)
				context = context[0..i] ~ root ~ context[i+2..$];
		}

		ContextItem.Segment[] segments;
		foreach (ci; context)
			segments ~= ci.toSegments(this, filename);

		string escape(string s)
		{
			if (!filename)
				return s;

			string result;
			foreach (c; s)
				if (c == '.' || c == ':')
					result ~= '/';
				else
				if (c == '\\' || c == '*' || c == '?' || c == '"' || c == '<' || c == '>' || c == '|' || c < 0x20 || c >= 0x7F || c == ' ' || c == '%')
					result ~= format("%%%02X", c);
				else
					result ~= c;

			auto pathSegments = result.split("/");
			if (!pathSegments.length)
				pathSegments = [""];
			foreach (ref pathSegment; pathSegments)
			{
				if (pathSegment == "")
					pathSegment = "%";

				static const reservedNames = ["CON", "PRN", "AUX", "NUL", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9"];
				auto pathSegmentU = pathSegment.toUpper();
				foreach (reservedName; reservedNames)
					if (pathSegmentU.startsWith(reservedName) && (pathSegmentU.length == reservedName.length || pathSegmentU[reservedName.length]=='.'))
					{
						pathSegment = "%" ~ pathSegment;
						break;
					}

				if (pathSegment.length > 240)
					pathSegment = assumeUnique(pathSegment[0..200] ~ '-' ~ toHexString(md5Of(pathSegment)));
			}

			return arrayJoin(pathSegments, "/");
		}

		string[] strings = new string[segments.length];
		foreach (i, ref s; segments)
			strings[i] = (i>0 ? cast(string)[s.delim] : null) ~ escape(s.str);

		return arrayJoin(strings);
	}

	bool addObject(T)(T obj, ContextPriority priority) { return objects.add(obj, context, priority); }

	void addClass(ASProgram.Class vclass, ContextPriority priority)
	{
		addObject(vclass, priority);

		pushContext("class", true);
		pushContext("init", true);
		addMethod(vclass.cinit, ContextPriority.declaration);
		popContext(); // init
		popContext(); // class

		pushContext("instance", true);
		pushContext("init", true);
		addMethod(vclass.instance.iinit, ContextPriority.declaration);
		popContext(); // init

		visitMultiname(vclass.instance.name, ContextPriority.declaration);
		visitMultiname(vclass.instance.superName, ContextPriority.usage);
		visitNamespace(vclass.instance.protectedNs, ContextPriority.declaration);
		foreach (iface; vclass.instance.interfaces)
			visitMultiname(iface, ContextPriority.usage);
		popContext(); // instance
	}

	void addMethod(ASProgram.Method method, ContextPriority priority)
	{
		if (addObject(method, priority))
		{
			foreach (paramType; method.paramTypes)
				visitMultiname(paramType, ContextPriority.usage);
			visitMultiname(method.returnType, ContextPriority.usage);
			if (method.vbody)
				visitMethodBody(method.vbody);
		}
	}
}

final class Disassembler
{
	ASProgram as;
	string name, dir;
	RefBuilder refs;
	bool dumpRaw = true;

	void newInclude(StringBuilder mainsb, string filename, void delegate(StringBuilder) callback, bool doInline = true)
	{
		if (doInline)
		{
			string base = dirName(mainsb.filename);
			string full = dir ~ "/" ~ filename;
			uint up = 0;
			while (!full.startsWith(base))
				base = dirName(base), up++;
			string rel  = replicate("../", up) ~ full[base.length+1..$];

			mainsb.flush();
			StringBuilder sb = new StringBuilder(full);
			callback(sb);
			sb.save();

			mainsb ~= "#include ";
			dumpString(mainsb, rel);
			mainsb.newLine();
		}
		else
			callback(mainsb);
	}

	this(ASProgram as, string dir, string name)
	{
		this.as = as;
		this.name = name;
		this.dir = dir;
	}

	void disassemble()
	{
		refs = new RefBuilder(as);
		refs.run();

		StringBuilder sb = new StringBuilder(dir ~ "/" ~ name ~ ".main.asasm");

		sb ~= "#version 4";
		sb.newLine();

		sb ~= "program";
		sb.indent++; sb.newLine();

		sb ~= "minorversion ";
		sb ~= to!string(as.minorVersion);
		sb.newLine();
		sb ~= "majorversion ";
		sb ~= to!string(as.majorVersion);
		sb.newLine();
		sb.newLine();

		foreach (uint i, script; as.scripts)
		{
			auto ii = i; // http://d.puremagic.com/issues/show_bug.cgi?id=8526
			newInclude(sb, refs.scripts.getFilename(script, "script"), (StringBuilder sb) {
				dumpScript(sb, script, ii);
			});
		}
		sb.newLine();

		if (as.orphanClasses.length)
		{
			sb ~= "; ============================= Orphan classes ==============================";
			sb.newLine();
			sb.newLine();

			foreach (i, vclass; as.orphanClasses)
				newInclude(sb, refs.objects.getFilename(vclass, "class"), (StringBuilder sb) {
					dumpClass(sb, vclass);
				});

			sb.newLine();
		}

		if (as.orphanMethods.length)
		{
			sb ~= "; ============================= Orphan methods ==============================";
			sb.newLine();
			sb.newLine();

			foreach (i, method; as.orphanMethods)
				newInclude(sb, refs.objects.getFilename(method, "method"), (StringBuilder sb) {
					dumpMethod(sb, method, "method");
				});

			sb.newLine();
		}

		sb.indent--;
		sb ~= "end ; program"; sb.newLine();

		sb.save();
	}

	void dumpInt(StringBuilder sb, long v)
	{
		if (v == ABCFile.NULL_INT)
			sb ~= "null";
		else
			sb.write(v);
	}

	void dumpUInt(StringBuilder sb, ulong v)
	{
		if (v == ABCFile.NULL_UINT)
			sb ~= "null";
		else
			sb.write(v);
	}

	static struct StaticBuf(T, size_t size)
	{
		T[size] buf;
		size_t pos;
		void put(T v) { buf[pos++] = v; }
		void put(in T[] v) { buf[pos..pos+v.length] = v[]; pos+=v.length; }
		T[] data() { return buf[0..pos]; }
	}

	void dumpDouble(StringBuilder sb, double v)
	{
		if (v == ABCFile.NULL_DOUBLE)
			sb ~= "null";
		else
		{
			StaticBuf!(char, 64) buf;
			formattedWrite(&buf, "%.18g", v);
			char[] s = buf.data();

			static double forceDouble(double d) { static double n; n = d; return n; }
			if (s != "nan" && s != "inf" && s != "-inf")
			{
				if (forceDouble(to!double(s)) == v)
				{
					foreach_reverse (i; 1..s.length)
						if (s[i]>='0' && s[i]<='8')
						{
							s[i]++;
							if (forceDouble(to!double(s[0..i+1]))==v)
								s = s[0..i+1];
							else
								s[i]--;
						}
					while (s.length>2 && s[$-1]!='.' && forceDouble(to!double(s[0..$-1]))==v)
						s = s[0..$-1];
				}
				else
				{
					buf.pos = 0;
					formattedWrite(&buf, "%.13a", v);
					s = buf.data();
					auto n = forceDouble(to!double(s));
					assert(n == v,
						"Can't print precise representation of double: %(%02X %) (%.18g) => %s => %(%02X %) (%.18g)".format(
							cast(ubyte[])cast(void[])[v], v,
							s,
							cast(ubyte[])cast(void[])[n], n,
						));
				}
			}
			sb ~= s;
		}
	}

	void dumpString(StringBuilder sb, string str)
	{
		if (str is null)
			sb ~= "null";
		else
		{
			static const char[16] hexDigits = "0123456789ABCDEF";

			sb ~= '"';
			foreach (c; str)
				if (c == 0x0A)
					sb ~= `\n`;
				else
				if (c == 0x0D)
					sb ~= `\r`;
				else
				if (c == '\\')
					sb ~= `\\`;
				else
				if (c == '"')
					sb ~= `\"`;
				else
				if (c < 0x20)
				{
					sb ~= `\x`;
					sb ~= hexDigits[c / 0x10];
					sb ~= hexDigits[c % 0x10];
				}
				else
					sb ~= c;
			sb ~= '"';
		}
	}

	void dumpNamespace(StringBuilder sb, ASProgram.Namespace namespace)
	{
		if (namespace is null)
			sb ~= "null";
		else
		{
			sb ~= ASTypeNames[namespace.kind];
			sb ~= '(';
			dumpString(sb, namespace.name);
			if (refs.hasHomonyms(namespace))
			{
				sb ~= ", ";
				auto label = refs.namespaces[namespace.kind].getName(namespace.id);
			//	label ~= format(" (%d)", namespace.id);
				dumpString(sb, label);
			}
			sb ~= ')';
		}
	}

	void dumpNamespaceSet(StringBuilder sb, ASProgram.Namespace[] set)
	{
		if (set is null)
			sb ~= "null";
		else
		{
			sb ~= '[';
			foreach (i, ns; set)
			{
				dumpNamespace(sb, ns);
				if (i < set.length-1)
					sb ~= ", ";
			}
			sb ~= ']';
		}
	}

	void dumpMultiname(StringBuilder sb, ASProgram.Multiname multiname)
	{
		if (multiname is null)
			sb ~= "null";
		else
		with (multiname)
		{
			sb ~= ASTypeNames[kind];
			sb ~= '(';
			switch (kind)
			{
				case ASType.QName:
				case ASType.QNameA:
					dumpNamespace(sb, vQName.ns);
					sb ~= ", ";
					dumpString(sb, vQName.name);
					break;
				case ASType.RTQName:
				case ASType.RTQNameA:
					dumpString(sb, vRTQName.name);
					break;
				case ASType.RTQNameL:
				case ASType.RTQNameLA:
					break;
				case ASType.Multiname:
				case ASType.MultinameA:
					dumpString(sb, vMultiname.name);
					sb ~= ", ";
					dumpNamespaceSet(sb, vMultiname.nsSet);
					break;
				case ASType.MultinameL:
				case ASType.MultinameLA:
					dumpNamespaceSet(sb, vMultinameL.nsSet);
					break;
				case ASType.TypeName:
					dumpMultiname(sb, vTypeName.name);
					sb ~= '<';
					foreach (i, param; vTypeName.params)
					{
						dumpMultiname(sb, param);
						if (i < vTypeName.params.length-1)
							sb ~= ", ";
					}
					sb ~= '>';
					break;
				default:
					throw new .Exception("Unknown Multiname kind");
			}
			sb ~= ')';
		}
	}

	void dumpTraits(StringBuilder sb, ASProgram.Trait[] traits, bool inScript = false)
	{
		foreach (/*ref*/ trait; traits)
		{
			sb ~= "trait ";
			sb ~= TraitKindNames[trait.kind];
			sb ~= ' ';
			dumpMultiname(sb, trait.name);
			if (trait.attr)
				dumpFlags!(true)(sb, trait.attr, TraitAttributeNames);
			bool inLine = false;
			switch (trait.kind)
			{
				case TraitKind.Slot:
				case TraitKind.Const:
					if (trait.vSlot.slotId)
					{
						sb ~= " slotid ";
						dumpUInt(sb, trait.vSlot.slotId);
					}
					if (trait.vSlot.typeName)
					{
						sb ~= " type ";
						dumpMultiname(sb, trait.vSlot.typeName);
					}
					if (trait.vSlot.value.vkind)
					{
						sb ~= " value ";
						dumpValue(sb, trait.vSlot.value);
					}
					inLine = true;
					break;
				case TraitKind.Class:
					if (trait.vClass.slotId)
					{
						sb ~= " slotid ";
						dumpUInt(sb, trait.vClass.slotId);
					}
					sb.indent++; sb.newLine();

					newInclude(sb, refs.objects.getFilename(trait.vClass.vclass, "class"), (StringBuilder sb) {
						dumpClass(sb, trait.vClass.vclass);
					});
					break;
				case TraitKind.Function:
					if (trait.vFunction.slotId)
					{
						sb ~= " slotid ";
						dumpUInt(sb, trait.vFunction.slotId);
					}
					sb.indent++; sb.newLine();
					newInclude(sb, refs.objects.getFilename(trait.vFunction.vfunction, "method"), (StringBuilder sb) {
						dumpMethod(sb, trait.vFunction.vfunction, "method");
					}, inScript);
					break;
				case TraitKind.Method:
				case TraitKind.Getter:
				case TraitKind.Setter:
					if (trait.vMethod.dispId)
					{
						sb ~= " dispid ";
						dumpUInt(sb, trait.vMethod.dispId);
					}
					sb.indent++; sb.newLine();
					newInclude(sb, refs.objects.getFilename(trait.vMethod.vmethod, "method"), (StringBuilder sb) {
						dumpMethod(sb, trait.vMethod.vmethod, "method");
					}, inScript);
					break;
				default:
					throw new Exception("Unknown trait kind");
			}

			foreach (metadata; trait.metadata)
			{
				if (inLine)
				{
					sb.indent++; sb.newLine();
					inLine = false;
				}
				dumpMetadata(sb, metadata);
			}

			if (inLine)
				{ sb ~= " end"; sb.newLine(); }
			else
				{ sb.indent--; sb ~= "end ; trait"; sb.newLine(); }
		}
	}

	void dumpMetadata(StringBuilder sb, ASProgram.Metadata metadata)
	{
		sb ~= "metadata ";
		dumpString(sb, metadata.name);
		sb.indent++; sb.newLine();
		assert(metadata.keys.length == metadata.values.length);
		foreach (i; 0..metadata.keys.length)
		{
			sb ~= "item ";
			dumpString(sb, metadata.keys[i]);
			sb ~= " ";
			dumpString(sb, metadata.values[i]);
			sb.newLine();
		}
		sb.indent--; sb ~= "end ; metadata"; sb.newLine();
	}

	void dumpFlags(bool oneLine = false)(StringBuilder sb, ubyte flags, const string[] names)
	{
		for (int i=0; flags; i++, flags>>=1)
			if (flags & 1)
			{
				static if (oneLine)
					sb ~= " flag ";
				else
					sb ~= "flag ";
				sb ~= names[i];
				static if (!oneLine)
					sb.newLine();
			}
	}

	void dumpValue(StringBuilder sb, ref ASProgram.Value value)
	{
		with (value)
		{
			sb ~= ASTypeNames[vkind];
			sb ~= '(';
			switch (vkind)
			{
				case ASType.Integer:
					dumpInt(sb, vint);
					break;
				case ASType.UInteger:
					dumpUInt(sb, vuint);
					break;
				case ASType.Double:
					dumpDouble(sb, vdouble);
					break;
				case ASType.Utf8:
					dumpString(sb, vstring);
					break;
				case ASType.Namespace:
				case ASType.PackageNamespace:
				case ASType.PackageInternalNs:
				case ASType.ProtectedNamespace:
				case ASType.ExplicitNamespace:
				case ASType.StaticProtectedNs:
				case ASType.PrivateNamespace:
					dumpNamespace(sb, vnamespace);
					break;
				case ASType.True:
				case ASType.False:
				case ASType.Null:
				case ASType.Undefined:
					break;
				default:
					throw new Exception("Unknown type");
			}

			sb ~= ')';
		}
	}

	void dumpMethod(StringBuilder sb, ASProgram.Method method, string label)
	{
		sb ~= label;
	//	sb ~= format(" ; %d", method.id);
		sb.indent++; sb.newLine();
		if (method.name !is null)
		{
			sb ~= "name ";
			dumpString(sb, method.name);
			sb.newLine();
		}
		auto refName = refs.objects.getName(method);
		if (refName)
		{
			sb ~= "refid ";
			dumpString(sb, refName);
			sb.newLine();
		}
		foreach (m; method.paramTypes)
		{
			sb ~= "param ";
			dumpMultiname(sb, m);
			sb.newLine();
		}
		if (method.returnType)
		{
			sb ~= "returns ";
			dumpMultiname(sb, method.returnType);
			sb.newLine();
		}
		dumpFlags(sb, method.flags, MethodFlagNames);
		foreach (ref v; method.options)
		{
			sb ~= "optional ";
			dumpValue(sb, v);
			sb.newLine();
		}
		foreach (s; method.paramNames)
		{
			sb ~= "paramname ";
			dumpString(sb, s);
			sb.newLine();
		}
		if (method.vbody)
			dumpMethodBody(sb, method.vbody);
		sb.indent--; sb ~= "end ; method"; sb.newLine();
	}

	void dumpClass(StringBuilder sb, ASProgram.Class vclass)
	{
		sb ~= "class"; sb.indent++; sb.newLine();

		auto refName = refs.objects.getName(vclass);
		if (refName)
		{
			sb ~= "refid ";
			dumpString(sb, refName);
			sb.newLine();
		}
		sb ~= "instance ";
		dumpInstance(sb, vclass.instance);
		dumpMethod(sb, vclass.cinit, "cinit");
		dumpTraits(sb, vclass.traits);

		sb.indent--; sb ~= "end ; class"; sb.newLine();
	}

	void dumpInstance(StringBuilder sb, ASProgram.Instance instance)
	{
		dumpMultiname(sb, instance.name);
		sb.indent++; sb.newLine();
		if (instance.superName)
		{
			sb ~= "extends ";
			dumpMultiname(sb, instance.superName);
			sb.newLine();
		}
		foreach (i; instance.interfaces)
		{
			sb ~= "implements ";
			dumpMultiname(sb, i);
			sb.newLine();
		}
		dumpFlags(sb, instance.flags, InstanceFlagNames);
		if (instance.protectedNs)
		{
			sb ~= "protectedns ";
			dumpNamespace(sb, instance.protectedNs);
			sb.newLine();
		}
		dumpMethod(sb, instance.iinit, "iinit");
		dumpTraits(sb, instance.traits);
		sb.indent--; sb ~= "end ; instance"; sb.newLine();
	}

	void dumpScript(StringBuilder sb, ASProgram.Script script, uint index)
	{
		sb ~= "script";
		sb.indent++; sb.newLine();
		dumpMethod(sb, script.sinit, "sinit");
		dumpTraits(sb, script.traits, true);
		sb.indent--; sb ~= "end ; script"; sb.newLine();
	}

	void dumpUIntField(StringBuilder sb, string name, uint value)
	{
		sb ~= name;
		sb ~= ' ';
		dumpUInt(sb, value);
		sb.newLine();
	}

	void dumpLabel(StringBuilder sb, ref ABCFile.Label label)
	{
		sb ~= 'L';
		sb.write(label.index);
		if (label.offset != 0)
		{
			if (label.offset > 0)
				sb ~= '+';
			sb.write(label.offset);
		}
	}

	void dumpMethodBody(StringBuilder sb, ASProgram.MethodBody mbody)
	{
		sb ~= "body";
		sb.indent++; sb.newLine();
		dumpUIntField(sb, "maxstack", mbody.maxStack);
		dumpUIntField(sb, "localcount", mbody.localCount);
		dumpUIntField(sb, "initscopedepth", mbody.initScopeDepth);
		dumpUIntField(sb, "maxscopedepth", mbody.maxScopeDepth);
		sb ~= "code";
		sb.newLine();

		bool[] labels = new bool[mbody.instructions.length+1];
		// reserve exception labels
		foreach (ref e; mbody.exceptions)
			labels[e.from.index] = labels[e.to.index] = labels[e.target.index] = true;

		sb.indent++;
		dumpInstructions(sb, mbody.instructions, labels, mbody.errors);
		sb.indent--;

		sb ~= "end ; code";
		sb.newLine();
		foreach (ref e; mbody.exceptions)
		{
			sb ~= "try from ";
			dumpLabel(sb, e.from);
			sb ~= " to ";
			dumpLabel(sb, e.to);
			sb ~= " target ";
			dumpLabel(sb, e.target);
			sb ~= " type ";
			dumpMultiname(sb, e.excType);
			sb ~= " name ";
			dumpMultiname(sb, e.varName);
			sb ~= " end";
			sb.newLine();
		}
		dumpTraits(sb, mbody.traits);
		sb.indent--; sb ~= "end ; body"; sb.newLine();
		sb.linePrefix = null;
	}

	void dumpInstructions(StringBuilder sb, ASProgram.Instruction[] instructions, bool[] labels, ABCFile.Error[] errors)
	{
		foreach (ref instruction; instructions)
			foreach (i, type; opcodeInfo[instruction.opcode].argumentTypes)
				switch (type)
				{
					case OpcodeArgumentType.JumpTarget:
					case OpcodeArgumentType.SwitchDefaultTarget:
						labels[instruction.arguments[i].jumpTarget.index] = true;
						break;
					case OpcodeArgumentType.SwitchTargets:
						foreach (ref label; instruction.arguments[i].switchTargets)
							labels[label.index] = true;
						break;
					default:
						break;
				}

		void checkLabel(uint ii)
		{
			if (labels[ii])
			{
				sb.noIndent();
				sb ~= 'L';
				sb.write(ii);
				sb ~= ':';
				sb.newLine();
			}
		}

		string[] iErrors = new string[instructions.length + 1];
		foreach (ref error; errors)
			iErrors[error.loc.index] = error.msg;

		bool extraNewLine = false;
		foreach (uint ii, ref instruction; instructions)
		{
			if (extraNewLine)
				sb.newLine();
			extraNewLine = newLineAfter[instruction.opcode];
			checkLabel(ii);

			if (iErrors[ii])
			{
				sb ~= "; Error: ";
				sb ~= iErrors[ii];
				sb.newLine();
			}

			if (instruction.opcode == Opcode.OP_raw)
			{
				if (dumpRaw)
					sb ~= "; 0x%02X".format(instruction.arguments[0].ubytev);
				sb.newLine();
				continue;
			}

			sb ~= opcodeInfo[instruction.opcode].name;
			auto argTypes = opcodeInfo[instruction.opcode].argumentTypes;
			if (argTypes.length)
			{
				foreach (i; opcodeInfo[instruction.opcode].name.length..20)
					sb ~= ' ';
				foreach (i, type; argTypes)
				{
					final switch (type)
					{
						case OpcodeArgumentType.Unknown:
							throw new Exception("Don't know how to disassemble OP_" ~ opcodeInfo[instruction.opcode].name);

						case OpcodeArgumentType.ByteLiteral:
							sb.write(instruction.arguments[i].bytev);
							break;
						case OpcodeArgumentType.UByteLiteral:
							sb.write(instruction.arguments[i].ubytev);
							break;
						case OpcodeArgumentType.IntLiteral:
							sb.write(instruction.arguments[i].intv);
							break;
						case OpcodeArgumentType.UIntLiteral:
							sb.write(instruction.arguments[i].uintv);
							break;

						case OpcodeArgumentType.Int:
							dumpInt(sb, instruction.arguments[i].intv);
							break;
						case OpcodeArgumentType.UInt:
							dumpUInt(sb, instruction.arguments[i].uintv);
							break;
						case OpcodeArgumentType.Double:
							dumpDouble(sb, instruction.arguments[i].doublev);
							break;
						case OpcodeArgumentType.String:
							dumpString(sb, instruction.arguments[i].stringv);
							break;
						case OpcodeArgumentType.Namespace:
							dumpNamespace(sb, instruction.arguments[i].namespacev);
							break;
						case OpcodeArgumentType.Multiname:
							dumpMultiname(sb, instruction.arguments[i].multinamev);
							break;
						case OpcodeArgumentType.Class:
							if (instruction.arguments[i].classv is null)
								sb ~= "null";
							else
								dumpString(sb, refs.objects.getName(instruction.arguments[i].classv));
							break;
						case OpcodeArgumentType.Method:
							if (instruction.arguments[i].methodv is null)
								sb ~= "null";
							else
								dumpString(sb, refs.objects.getName(instruction.arguments[i].methodv));
							break;

						case OpcodeArgumentType.JumpTarget:
						case OpcodeArgumentType.SwitchDefaultTarget:
							dumpLabel(sb, instruction.arguments[i].jumpTarget);
							break;

						case OpcodeArgumentType.SwitchTargets:
							sb ~= '[';
							auto targets = instruction.arguments[i].switchTargets;
							foreach (ti, t; targets)
							{
								dumpLabel(sb, t);
								if (ti < targets.length-1)
									sb ~= ", ";
							}
							sb ~= ']';
							break;
					}
					if (i < argTypes.length-1)
						sb ~= ", ";
				}
			}
			sb.newLine();
		}
		checkLabel(to!uint(instructions.length));
	}
}

private:

bool nsSimilar(ASProgram.Namespace ns1, ASProgram.Namespace ns2)
{
	if (ns1.kind==ASType.PrivateNamespace || ns2.kind==ASType.PrivateNamespace)
		return ns1.kind==ns2.kind && ns1.id==ns2.id;
	// ignore ns kind in other cases
	return ns1.name == ns2.name;
}

bool[256] newLineAfter;

static this()
{
	foreach (o; [
		Opcode.OP_callpropvoid,
		Opcode.OP_constructsuper,
		Opcode.OP_initproperty,
		Opcode.OP_ifeq,
		Opcode.OP_iffalse,
		Opcode.OP_ifge,
		Opcode.OP_ifgt,
		Opcode.OP_ifle,
		Opcode.OP_iflt,
		Opcode.OP_ifne,
		Opcode.OP_ifnge,
		Opcode.OP_ifngt,
		Opcode.OP_ifnle,
		Opcode.OP_ifnlt,
		Opcode.OP_ifstricteq,
		Opcode.OP_ifstrictne,
		Opcode.OP_iftrue,
		Opcode.OP_jump,
		Opcode.OP_lookupswitch,
		Opcode.OP_pushscope,
		Opcode.OP_returnvalue,
		Opcode.OP_returnvoid,
		Opcode.OP_setglobalslot,
		Opcode.OP_setlocal,
		Opcode.OP_setlocal0,
		Opcode.OP_setlocal1,
		Opcode.OP_setlocal2,
		Opcode.OP_setlocal3,
		Opcode.OP_setproperty,
		Opcode.OP_setpropertylate,
		Opcode.OP_setslot,
		Opcode.OP_setsuper,
		Opcode.OP_si8,
		Opcode.OP_si16,
		Opcode.OP_si32,
		Opcode.OP_sf32,
		Opcode.OP_sf64,
		Opcode.OP_throw,
	])
		newLineAfter[o] = true;
}

/// Force a raw data join (workaround for issue 6064)
T[] arrayJoin(T)(T[][] arrays, T[] sep)
{
	return cast(T[])join(cast(ubyte[][])arrays, cast(ubyte[])sep);
}

/// ditto
T[] arrayJoin(T)(T[][] arrays)
{
	return cast(T[])join(cast(ubyte[][])arrays);
}
