// GenBase.fu - base class for code generators
//
// Copyright (C) 2011-2026  Piotr Fusik
//
// This file is part of Fusion Transpiler,
// see https://github.com/fusionlanguage/fut
//
// Fusion Transpiler 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.
//
// Fusion Transpiler 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 Fusion Transpiler.  If not, see http://www.gnu.org/licenses/

public abstract class GenHost : FuSemaHost
{
	public abstract TextWriter! CreateFile!(string? directory, string filename);
	public abstract void CloseFile!();
}

public abstract class GenBase : FuVisitor
{
	GenHost! Host;
	TextWriter! Writer;
	StringWriter() StringWriter;
	protected int Indent = 0;
	protected bool AtLineStart = true;
	bool AtChildStart = false;
	bool InChildBlock = false;
	protected bool InHeaderFile = false;
	SortedDictionary<string(), bool>() Includes;
	protected FuMethodBase? CurrentMethod = null;
	protected HashSet<FuContainerType>() WrittenTypes;
	protected List<FuSwitch>() SwitchesWithGoto;
	protected List<FuExpr>() CurrentTemporaries; // FuExpr or FuType

	public void SetHost!(GenHost! host)
	{
		this.Host = host;
	}

	protected abstract string GetTargetName();

	void ReportError(FuStatement statement, string message)
	{
		this.Host.ReportStatementError(statement, message);
	}

	protected void NotSupported(FuStatement statement, string feature)
	{
		ReportError(statement, $"{feature} not supported when targeting {GetTargetName()}");
	}

	protected void NotYet(FuStatement statement, string feature)
	{
		ReportError(statement, $"{feature} not supported yet when targeting {GetTargetName()}");
	}

	protected virtual void StartLine!()
	{
		if (this.AtLineStart) {
			if (this.AtChildStart) {
				this.AtChildStart = false;
				this.Writer.WriteChar('\n');
				this.Indent++;
			}
			for (int i = 0; i < this.Indent; i++)
				this.Writer.WriteChar('\t');
			this.AtLineStart = false;
		}
	}

	protected void WriteChar!(int c)
	{
		StartLine();
		this.Writer.WriteCodePoint(c);
	}

	protected void Write!(string s)
	{
		StartLine();
		this.Writer.Write(s);
	}

	internal override void VisitLiteralNull!()
	{
		Write("null");
	}

	internal override void VisitLiteralFalse!()
	{
		Write("false");
	}

	internal override void VisitLiteralTrue!()
	{
		Write("true");
	}

	internal override void VisitLiteralLong!(long i, FuPriority parent)
	{
		this.Writer.Write(i);
	}

	protected virtual int GetLiteralChars() => 0;

	internal override void VisitLiteralChar!(int c)
	{
		if (c < GetLiteralChars()) {
			WriteChar('\'');
			switch (c) {
			case '\n': Write("\\n"); break;
			case '\r': Write("\\r"); break;
			case '\t': Write("\\t"); break;
			case '\'': Write("\\'"); break;
			case '\\': Write("\\\\"); break;
			default: WriteChar(c); break;
			}
			WriteChar('\'');
		}
		else
			this.Writer.Write(c);
	}

	internal override void VisitLiteralDouble!(double value)
	{
		string() s = $"{value}";
		Write(s);
		foreach (int c in s) {
			switch (c) {
			case '-':
			case '0':
			case '1':
			case '2':
			case '3':
			case '4':
			case '5':
			case '6':
			case '7':
			case '8':
			case '9':
				break;
			default:
				return;
			}
		}
		Write(".0"); // it looked like an integer
	}

	internal override void VisitLiteralString!(string value)
	{
		WriteChar('"');
		Write(value);
		WriteChar('"');
	}

	void WriteLowercaseChar!(int c)
	{
		if (c >= 'A' && c <= 'Z')
			c += 0x20;
		this.Writer.WriteChar(c);
	}

	void WriteUppercaseChar!(int c)
	{
		if (c >= 'a' && c <= 'z')
			c -= 0x20;
		this.Writer.WriteChar(c);
	}

	protected void WriteLowercase!(string s)
	{
		StartLine();
		foreach (int c in s)
			WriteLowercaseChar(c);
	}

	protected void WriteCamelCase!(string s)
	{
		StartLine();
		WriteLowercaseChar(s[0]);
		this.Writer.Write(s.Substring(1));
	}

	protected void WritePascalCase!(string s)
	{
		StartLine();
		WriteUppercaseChar(s[0]);
		this.Writer.Write(s.Substring(1));
	}

	protected void WriteUppercaseWithUnderscores!(string s)
	{
		StartLine();
		bool first = true;
		foreach (int c in s) {
			if (!first && c >= 'A' && c <= 'Z') {
				this.Writer.WriteChar('_');
				this.Writer.WriteChar(c);
			}
			else
				WriteUppercaseChar(c);
			first = false;
		}
	}

	protected void WriteLowercaseWithUnderscores!(string s)
	{
		StartLine();
		bool first = true;
		foreach (int c in s) {
			if (c >= 'A' && c <= 'Z') {
				if (!first)
					this.Writer.WriteChar('_');
				WriteLowercaseChar(c);
			}
			else
				this.Writer.WriteChar(c);
			first = false;
		}
	}

	protected void WriteNewLine!()
	{
		this.Writer.WriteChar('\n');
		this.AtLineStart = true;
	}

	protected void WriteCharLine!(int c)
	{
		WriteChar(c);
		WriteNewLine();
	}

	protected void WriteLine!(string s)
	{
		Write(s);
		WriteNewLine();
	}

	protected void WriteUppercaseConstName!(FuConst konst)
	{
		if (konst.InMethod != null) {
			WriteUppercaseWithUnderscores(konst.InMethod.Name);
			WriteChar('_');
		}
		WriteUppercaseWithUnderscores(konst.Name);
		if (konst.InMethodIndex > 0) {
			WriteChar('_');
			VisitLiteralLong(konst.InMethodIndex, FuPriority.Primary);
		}
	}

	protected abstract void WriteName!(FuSymbol symbol);

	protected virtual void WriteBanner!()
	{
		WriteLine("// Generated automatically with \"fut\". Do not edit.");
	}

	protected void CreateFile!(string? directory, string filename)
	{
		this.Writer = this.Host.CreateFile(directory, filename);
		WriteBanner();
	}

	protected void CloseFile!()
	{
		this.Host.CloseFile();
	}

	protected void OpenStringWriter!()
	{
		this.Writer = this.StringWriter;
	}

	protected void CloseStringWriter!()
	{
		this.Writer.Write(this.StringWriter.ToString());
		this.StringWriter.Clear();
	}

	protected void Include!(string name)
	{
		if (!this.Includes.ContainsKey(name))
			this.Includes[name] = this.InHeaderFile;
	}

	protected void WriteIncludes!(string prefix, string suffix)
	{
		foreach ((string name, bool inHeaderFile) in this.Includes) {
			if (inHeaderFile == this.InHeaderFile) {
				Write(prefix);
				Write(name);
				WriteLine(suffix);
			}
		}
		if (!this.InHeaderFile)
			this.Includes.Clear();
	}

	protected virtual void StartDocLine!()
	{
		Write(" * ");
	}

	protected void WriteXmlDoc!(string text)
	{
		foreach (int c in text) {
			switch (c) {
			case '&':
				Write("&amp;");
				break;
			case '<':
				Write("&lt;");
				break;
			case '>':
				Write("&gt;");
				break;
			default:
				WriteChar(c);
				break;
			}
		}
	}

	protected virtual void WriteDocCode!(string s)
	{
		WriteXmlDoc(s);
	}

	protected virtual void WriteDocPara!(FuDocPara para, bool many)
	{
		if (many) {
			WriteNewLine();
			Write(" * <p>");
		}
		foreach (FuDocInline inline in para.Children) {
			switch (inline) {
			case FuDocText text:
				WriteXmlDoc(text.Text);
				break;
			case FuDocCode code:
				Write("<code>");
				WriteDocCode(code.Text);
				Write("</code>");
				break;
			case FuDocLine:
				WriteNewLine();
				StartDocLine();
				break;
			default:
				assert false;
			}
		}
	}

	protected virtual void WriteDocList!(FuDocList list)
	{
		WriteNewLine();
		WriteLine(" * <ul>");
		foreach (FuDocPara item in list.Items) {
			Write(" * <li>");
			WriteDocPara(item, false);
			WriteLine("</li>");
		}
		Write(" * </ul>");
	}

	protected void WriteDocBlock!(FuDocBlock block, bool many)
	{
		switch (block) {
		case FuDocPara para:
			WriteDocPara(para, many);
			break;
		case FuDocList list:
			WriteDocList(list);
			break;
		default:
			assert false;
		}
	}

	protected void WriteContent!(FuCodeDoc doc)
	{
		StartDocLine();
		WriteDocPara(doc.Summary, false);
		WriteNewLine();
		if (doc.Details.Count > 0) {
			StartDocLine();
			if (doc.Details.Count == 1)
				WriteDocBlock(doc.Details[0], false);
			else {
				foreach (FuDocBlock block in doc.Details)
					WriteDocBlock(block, true);
			}
			WriteNewLine();
		}
	}

	protected virtual void WriteDoc!(FuCodeDoc? doc)
	{
		if (doc != null) {
			WriteLine("/**");
			WriteContent(doc);
			WriteLine(" */");
		}
	}

	protected virtual void WriteSelfDoc!(FuMethod method)
	{
	}

	protected virtual void WriteParameterDoc!(FuVar param, bool first)
	{
		Write(" * @param ");
		WriteName(param);
		WriteChar(' ');
		WriteDocPara(param.Documentation.Summary, false);
		WriteNewLine();
	}

	protected virtual void WriteReturnDoc!(FuMethod method)
	{
	}

	protected virtual void WriteThrowsDoc!(FuThrowsDeclaration decl)
	{
		Write(" * @throws ");
		WriteExceptionClass(decl.Symbol);
		WriteChar(' ');
		WriteDocPara(decl.Documentation.Summary, false);
		WriteNewLine();
	}

	protected void WriteParametersAndThrowsDoc!(FuMethod method)
	{
		bool first = true;
		for (FuVar? param = method.FirstParameter(); param != null; param = param.NextVar()) {
			if (param.Documentation != null) {
				WriteParameterDoc(param, first);
				first = false;
			}
		}
		WriteReturnDoc(method);
		foreach (FuThrowsDeclaration# decl in method.Throws) {
			if (decl.Documentation != null)
				WriteThrowsDoc(decl);
		}
	}

	protected void WriteMethodDoc!(FuMethod method)
	{
		if (method.Documentation == null)
			return;
		WriteLine("/**");
		WriteContent(method.Documentation);
		WriteSelfDoc(method);
		WriteParametersAndThrowsDoc(method);
		WriteLine(" */");
	}

	protected void WriteTopLevelNatives!(FuProgram program)
	{
		foreach (string content in program.TopLevelNatives)
			Write(content);
	}

	protected void OpenBlock!()
	{
		WriteCharLine('{');
		this.Indent++;
	}

	protected void CloseBlock!()
	{
		this.Indent--;
		WriteCharLine('}');
	}

	protected virtual void EndStatement!()
	{
		WriteCharLine(';');
	}

	protected void WriteComma!(int i)
	{
		if (i > 0) {
			if ((i & 15) == 0) {
				WriteCharLine(',');
				WriteChar('\t');
			}
			else
				Write(", ");
		}
	}

	protected void WriteBytes!(List<byte> content)
	{
		int i = 0;
		foreach (byte b in content) {
			WriteComma(i++);
			VisitLiteralLong(b, FuPriority.Argument);
		}
	}

	protected virtual FuId GetTypeId(FuType type, bool promote) => promote && type is FuRangeType ? FuId.IntType : type.Id;

	protected abstract void WriteTypeAndName!(FuNamedValue value);

	protected virtual void WriteLocalName!(FuSymbol symbol, FuPriority parent)
	{
		if (symbol is FuField)
			Write("this.");
		WriteName(symbol);
	}

	protected void WriteDoubling!(string s, int doubled)
	{
		foreach (int c in s) {
			if (c == doubled)
				WriteChar(c);
			WriteChar(c);
		}
	}

	protected void WriteDoublingBraces!(string s)
	{
		foreach (int c in s) {
			if (c == '{' || c == '}')
				WriteChar(c);
			WriteChar(c);
		}
	}

	protected virtual void WritePrintfWidth!(FuInterpolatedPart part)
	{
		if (part.WidthExpr != null)
			VisitLiteralLong(part.Width, FuPriority.Primary);
		if (part.Precision >= 0) {
			WriteChar('.');
			VisitLiteralLong(part.Precision, FuPriority.Primary);
		}
	}

	static int GetPrintfFormat(FuType type, int format)
	{
		switch (type) {
		case FuIntegerType:
			switch (format) {
			case 'X':
			case 'x':
				return format;
			case 'U':
			case 'u':
				return 'c';
			default:
				return 'd';
			}
		case FuNumericType:
			switch (format) {
			case 'E':
			case 'e':
			case 'f':
			case 'G':
				return format;
			case 'F':
				return 'f';
			default:
				return 'g';
			}
		// case FuStringType: - matched by FuClassType
		case FuClassType:
			return 's';
		default:
			assert false;
		}
	}

	protected virtual void WritePrintfPartFormat!(FuInterpolatedPart part)
	{
		WritePrintfWidth(part);
		WriteChar(GetPrintfFormat(part.Argument.Type, part.Format));
	}

	protected void WritePrintfFormat!(FuInterpolatedString expr)
	{
		foreach (FuInterpolatedPart part in expr.Parts) {
			WriteDoubling(part.Prefix, '%');
			WriteChar('%');
			WritePrintfPartFormat(part);
		}
		WriteDoubling(expr.Suffix, '%');
	}

	protected void WritePyFormat!(FuInterpolatedPart part)
	{
		if (part.WidthExpr != null || part.Precision >= 0 || (part.Format != ' ' && part.Format != 'D'))
			WriteChar(':');
		if (part.WidthExpr != null) {
			if (part.Width >= 0) {
				if (!(part.Argument.Type is FuNumericType))
					WriteChar('>');
				VisitLiteralLong(part.Width, FuPriority.Primary);
			}
			else {
				WriteChar('<');
				VisitLiteralLong(-part.Width, FuPriority.Primary);
			}
		}
		if (part.Precision >= 0) {
			WriteChar(part.Argument.Type is FuIntegerType ? '0' : '.');
			VisitLiteralLong(part.Precision, FuPriority.Primary);
		}
		switch (part.Format) {
		case ' ':
		case 'D':
			break;
		case 'U':
		case 'u':
			WriteChar('c');
			break;
		default:
			WriteChar(part.Format);
			break;
		}
		WriteChar('}');
	}

	protected virtual void WriteInterpolatedStringArg!(FuInterpolatedPart part)
	{
		part.Argument.Accept(this, FuPriority.Argument);
	}

	protected void WriteInterpolatedStringArgs!(FuInterpolatedString expr)
	{
		foreach (FuInterpolatedPart part in expr.Parts) {
			Write(", ");
			WriteInterpolatedStringArg(part);
		}
	}

	protected void WritePrintf!(FuInterpolatedString expr, bool newLine)
	{
		WriteChar('"');
		WritePrintfFormat(expr);
		if (newLine)
			Write("\\n");
		WriteChar('"');
		WriteInterpolatedStringArgs(expr);
		WriteChar(')');
	}

	protected void WritePostfix!(FuExpr obj, string s)
	{
		obj.Accept(this, FuPriority.Primary);
		Write(s);
	}

	protected void WriteCall!(string function, FuExpr arg0, FuExpr? arg1 = null, FuExpr? arg2 = null)
	{
		Write(function);
		WriteChar('(');
		arg0.Accept(this, FuPriority.Argument);
		if (arg1 != null) {
			Write(", ");
			arg1.Accept(this, FuPriority.Argument);
			if (arg2 != null) {
				Write(", ");
				arg2.Accept(this, FuPriority.Argument);
			}
		}
		WriteChar(')');
	}

	protected virtual void WriteMemberOp!(FuExpr left, FuSymbolReference? symbol)
	{
		WriteChar('.');
	}

	protected void WriteMethodCall!(FuExpr obj, string method, FuExpr arg0, FuExpr? arg1 = null)
	{
		obj.Accept(this, FuPriority.Primary);
		WriteMemberOp(obj, null);
		WriteCall(method, arg0, arg1);
	}

	protected void WriteInParentheses!(List<FuExpr#> args)
	{
		WriteChar('(');
		bool first = true;
		foreach (FuExpr arg in args) {
			if (!first)
				Write(", ");
			arg.Accept(this, FuPriority.Argument);
			first = false;
		}
		WriteChar(')');
	}

	protected virtual void WriteSelectValues!(FuType type, FuSelectExpr expr)
	{
		WriteCoerced(type, expr.OnTrue, FuPriority.Select);
		Write(" : ");
		WriteCoerced(type, expr.OnFalse, FuPriority.Select);
	}

	protected virtual void WriteCoercedSelect!(FuType type, FuSelectExpr expr, FuPriority parent)
	{
		if (parent > FuPriority.Select)
			WriteChar('(');
		expr.Cond.Accept(this, FuPriority.SelectCond);
		Write(" ? ");
		WriteSelectValues(type, expr);
		if (parent > FuPriority.Select)
			WriteChar(')');
	}

	protected virtual void WriteCoercedInternal!(FuType type, FuExpr expr, FuPriority parent)
	{
		expr.Accept(this, parent);
	}

	protected void WriteCoerced!(FuType type, FuExpr expr, FuPriority parent)
	{
		if (expr is FuSelectExpr select)
			WriteCoercedSelect(type, select, parent);
		else
			WriteCoercedInternal(type, expr, parent);
	}

	protected virtual void WriteCoercedExpr!(FuType type, FuExpr expr)
	{
		WriteCoerced(type, expr, FuPriority.Argument);
	}

	protected virtual void WriteStronglyCoerced!(FuType type, FuExpr expr)
	{
		WriteCoerced(type, expr, FuPriority.Argument);
	}

	protected virtual void WriteCoercedLiteral!(FuType? type, FuExpr expr)
	{
		expr.Accept(this, FuPriority.Argument);
	}

	protected void WriteCoercedLiterals!(FuType? type, List<FuExpr#> exprs)
	{
		for (int i = 0; i < exprs.Count; i++) {
			WriteComma(i);
			WriteCoercedLiteral(type, exprs[i]);
		}
	}

	protected void WriteCoercedArgs!(FuMethod method, List<FuExpr#> args)
	{
		FuVar param = method.FirstParameter();
		bool first = true;
		foreach (FuExpr arg in args) {
			if (!first)
				Write(", ");
			first = false;
			WriteStronglyCoerced(param.Type, arg);
			param = param.NextVar();
		}
	}

	protected void WriteCoercedArgsInParentheses!(FuMethod method, List<FuExpr#> args)
	{
		WriteChar('(');
		WriteCoercedArgs(method, args);
		WriteChar(')');
	}

	protected abstract void WriteNewArray!(FuType elementType, FuExpr lengthExpr, FuPriority parent);

	protected virtual void WriteNewArrayStorage!(FuArrayStorageType array)
	{
		WriteNewArray(array.GetElementType(), array.LengthExpr, FuPriority.Argument);
	}

	protected abstract void WriteNew!(FuReadWriteClassType klass, FuPriority parent);

	protected void WriteNewStorage!(FuType type)
	{
		switch (type) {
		case FuArrayStorageType array:
			WriteNewArrayStorage(array);
			break;
		case FuStorageType storage:
			WriteNew(storage, FuPriority.Argument);
			break;
		default:
			assert false;
		}
	}

	protected virtual void WriteArrayStorageInit!(FuArrayStorageType array, FuExpr? value)
	{
		Write(" = ");
		WriteNewArrayStorage(array);
	}

	protected virtual void WriteNewWithFields!(FuReadWriteClassType type, FuAggregateInitializer init)
	{
		WriteNew(type, FuPriority.Argument);
	}

	protected virtual void WriteStorageInit!(FuNamedValue def)
	{
		Write(" = ");
		if (def.Value is FuAggregateInitializer init) {
			assert def.Type is FuReadWriteClassType klass;
			WriteNewWithFields(klass, init);
		}
		else
			WriteNewStorage(def.Type);
	}

	protected virtual void WriteVarInit!(FuNamedValue def)
	{
		if (def.Type is FuArrayStorageType array)
			WriteArrayStorageInit(array, def.Value);
		else if (def.Type.IsFinal() && def.Value is FuLiteralNull) {
		}
		else if (def.Value != null && !(def.Value is FuAggregateInitializer)) {
			Write(" = ");
			WriteCoercedExpr(def.Type, def.Value);
		}
		else if (def.Type.IsFinal() && !(def.Parent is FuParameters))
			WriteStorageInit(def);
	}

	protected virtual void WriteVar!(FuNamedValue def)
	{
		WriteTypeAndName(def);
		WriteVarInit(def);
	}

	internal override void VisitVar!(FuVar expr)
	{
		WriteVar(expr);
	}

	protected void WriteObjectLiteral!(FuAggregateInitializer init, string separator)
	{
		string prefix = " { ";
		foreach (FuExpr item in init.Items) {
			Write(prefix);
			assert item is FuBinaryExpr assign;
			assert assign.Left is FuSymbolReference field;
			WriteName(field.Symbol);
			Write(separator);
			WriteCoerced(assign.Left.Type, assign.Right, FuPriority.Argument);
			prefix = ", ";
		}
		Write(" }");
	}

	static FuAggregateInitializer? GetAggregateInitializer(FuNamedValue def)
	{
		FuExpr expr = def.Value;
		if (expr is FuPrefixExpr unary)
			expr = unary.Inner;
		return expr is FuAggregateInitializer init ? init : null;
	}

	void WriteAggregateInitField!(FuExpr obj, FuExpr item)
	{
		assert item is FuBinaryExpr assign;
		assert assign.Left is FuSymbolReference field;
		WriteMemberOp(obj, field);
		WriteName(field.Symbol);
		Write(" = ");
		WriteCoerced(field.Type, assign.Right, FuPriority.Argument);
		EndStatement();
	}

	protected virtual void WriteInitCode!(FuNamedValue def)
	{
		FuAggregateInitializer? init = GetAggregateInitializer(def);
		if (init != null) {
			foreach (FuExpr item in init.Items) {
				WriteLocalName(def, FuPriority.Primary);
				WriteAggregateInitField(def, item);
			}
		}
	}

	protected virtual void DefineIsVar!(FuBinaryExpr binary)
	{
		if (binary.Right is FuVar def) {
			EnsureChildBlock();
			WriteVar(def);
			EndStatement();
		}
	}

	protected void WriteArrayElement!(FuNamedValue def, int nesting)
	{
		WriteLocalName(def, FuPriority.Primary);
		for (int i = 0; i < nesting; i++) {
			Write("[_i");
			VisitLiteralLong(i, FuPriority.Primary);
			WriteChar(']');
		}
	}

	protected void OpenLoop!(string intString, int nesting, int count)
	{
		Write("for (");
		Write(intString);
		Write(" _i");
		VisitLiteralLong(nesting, FuPriority.Primary);
		Write(" = 0; _i");
		VisitLiteralLong(nesting, FuPriority.Primary);
		Write(" < ");
		VisitLiteralLong(count, FuPriority.Rel);
		Write("; _i");
		VisitLiteralLong(nesting, FuPriority.Primary);
		Write("++) ");
		OpenBlock();
	}

	protected void WriteTemporaryName!(int id)
	{
		Write("futemp");
		VisitLiteralLong(id, FuPriority.Primary);
	}

	protected bool TryWriteTemporary!(FuExpr expr)
	{
		int id = this.CurrentTemporaries.IndexOf(expr);
		if (id < 0)
			return false;
		WriteTemporaryName(id);
		return true;
	}

	protected void WriteResourceName!(string name)
	{
		foreach (int c in name)
			WriteChar(FuLexer.IsLetterOrDigit(c) ? c : '_');
	}

	protected abstract void WriteResource!(string name, int length);

	internal override void VisitPrefixExpr!(FuPrefixExpr expr, FuPriority parent)
	{
		switch (expr.Op) {
		case FuToken.Increment:
			Write("++");
			break;
		case FuToken.Decrement:
			Write("--");
			break;
		case FuToken.Minus:
			WriteChar('-');
			// FIXME: - --foo[bar]
			if (expr.Inner is FuPrefixExpr inner && (inner.Op == FuToken.Minus || inner.Op == FuToken.Decrement))
				WriteChar(' ');
			break;
		case FuToken.Tilde:
			WriteChar('~');
			break;
		case FuToken.ExclamationMark:
			WriteChar('!');
			break;
		case FuToken.New:
			assert expr.Type is FuDynamicPtrType dynamic;
			if (TryWriteTemporary(expr))
				return;
			if (dynamic.Class.Id == FuId.ArrayPtrClass)
				WriteNewArray(dynamic.GetElementType(), expr.Inner, parent);
			else if (expr.Inner is FuAggregateInitializer init)
				WriteNewWithFields(dynamic, init);
			else
				WriteNew(dynamic, parent);
			return;
		case FuToken.Resource:
			assert expr.Inner is FuLiteralString name;
			assert expr.Type is FuArrayStorageType array;
			WriteResource(name.Value, array.Length);
			return;
		default:
			assert false;
		}
		expr.Inner.Accept(this, FuPriority.Primary);
	}

	internal override void VisitPostfixExpr!(FuPostfixExpr expr, FuPriority parent)
	{
		expr.Inner.Accept(this, FuPriority.Primary);
		switch (expr.Op) {
		case FuToken.Increment:
			Write("++");
			break;
		case FuToken.Decrement:
			Write("--");
			break;
		default:
			assert false;
		}
	}

	protected bool IsWholeArray(FuExpr array, FuExpr offset, FuExpr length) => array.Type is FuArrayStorageType arrayStorage
		&& offset.IsLiteralZero() && length is FuLiteralLong literalLength && arrayStorage.Length == literalLength.Value;

	protected void StartAdd!(FuExpr expr)
	{
		if (!expr.IsLiteralZero()) {
			expr.Accept(this, FuPriority.Add);
			Write(" + ");
		}
	}

	protected void WriteAdd!(FuExpr left, FuExpr right)
	{
		if (left is FuLiteralLong leftLiteral) {
			long leftValue = leftLiteral.Value;
			if (leftValue == 0) {
				right.Accept(this, FuPriority.Argument);
				return;
			}
			if (right is FuLiteralLong rightLiteral) {
				VisitLiteralLong(leftValue + rightLiteral.Value, FuPriority.Argument);
				return;
			}
		}
		else if (right.IsLiteralZero()) {
			left.Accept(this, FuPriority.Argument);
			return;
		}
		left.Accept(this, FuPriority.Add);
		Write(" + ");
		right.Accept(this, FuPriority.Add);
	}

	protected void WriteStartEnd!(FuExpr startIndex, FuExpr length)
	{
		startIndex.Accept(this, FuPriority.Argument);
		Write(", ");
		WriteAdd(startIndex, length); // FIXME: side effect
	}

	protected static bool NeedAddParentheses(FuPriority parent)
	{
		switch (parent) {
		case FuPriority.Or:
		case FuPriority.Xor:
		case FuPriority.And:
		case FuPriority.Shift:
			return true;
		default:
			return parent > FuPriority.Add;
		}
	}

	protected virtual void WriteBinaryOperand!(FuExpr expr, FuPriority parent, FuBinaryExpr binary)
	{
		expr.Accept(this, parent);
	}

	protected void WriteBinaryExpr!(FuBinaryExpr expr, bool parentheses, FuPriority left, string op, FuPriority right)
	{
		if (parentheses)
			WriteChar('(');
		WriteBinaryOperand(expr.Left, left, expr);
		Write(op);
		WriteBinaryOperand(expr.Right, right, expr);
		if (parentheses)
			WriteChar(')');
	}

	protected void WriteBinaryExpr2!(FuBinaryExpr expr, FuPriority parent, FuPriority child, string op)
	{
		WriteBinaryExpr(expr, parent > child, child, op, child);
	}

	protected static string GetEqOp(bool not) => not ? " != " : " == ";

	protected virtual void WriteEqualOperand!(FuExpr expr, FuExpr other)
	{
		expr.Accept(this, FuPriority.Equality);
	}

	protected void WriteEqualExpr!(FuExpr? left, FuExpr right, FuPriority parent, string op)
	{
		if (parent > FuPriority.CondAnd)
			WriteChar('(');
		if (left == null)
			Write("fuSwitchValue");
		else
			WriteEqualOperand(left, right);
		Write(op);
		WriteEqualOperand(right, left);
		if (parent > FuPriority.CondAnd)
			WriteChar(')');
	}

	protected virtual void WriteEqual!(FuExpr? left, FuExpr right, FuPriority parent, bool not)
	{
		WriteEqualExpr(left, right, parent, GetEqOp(not));
	}

	protected virtual void WriteRel!(FuBinaryExpr expr, FuPriority parent, string op)
	{
		WriteBinaryExpr(expr, parent > FuPriority.CondAnd, FuPriority.Rel, op, FuPriority.Rel);
	}

	protected virtual void WriteAnd!(FuBinaryExpr expr, FuPriority parent)
	{
		WriteBinaryExpr(expr, parent > FuPriority.CondAnd && parent != FuPriority.And, FuPriority.And, " & ", FuPriority.And);
	}

	protected virtual void WriteAssignRight!(FuBinaryExpr expr)
	{
		WriteCoerced(expr.Left.Type, expr.Right, FuPriority.Argument);
	}

	protected virtual void WriteAssign!(FuBinaryExpr expr, FuPriority parent)
	{
		if (parent > FuPriority.Assign)
			WriteChar('(');
		expr.Left.Accept(this, FuPriority.Assign);
		Write(" = ");
		WriteAssignRight(expr);
		if (parent > FuPriority.Assign)
			WriteChar(')');
	}

	protected virtual void WriteOpAssignRight!(FuBinaryExpr expr)
	{
		expr.Right.Accept(this, FuPriority.Argument);
	}

	protected virtual void WriteIndexingInfix!(FuExpr collection)
	{
	}

	protected void WriteIndexing!(FuExpr collection, FuExpr index)
	{
		collection.Accept(this, FuPriority.Primary);
		WriteIndexingInfix(collection);
		WriteChar('[');
		index.Accept(this, FuPriority.Argument);
		WriteChar(']');
	}

	protected virtual void WriteIndexingExpr!(FuBinaryExpr expr, FuPriority parent)
	{
		WriteIndexing(expr.Left, expr.Right);
	}

	protected virtual string GetIsOperator() => " is ";

	internal override void VisitBinaryExpr!(FuBinaryExpr expr, FuPriority parent)
	{
		switch (expr.Op) {
		case FuToken.Plus:
			WriteBinaryExpr(expr, NeedAddParentheses(parent), FuPriority.Add, " + ", FuPriority.Add);
			break;
		case FuToken.Minus:
			WriteBinaryExpr(expr, NeedAddParentheses(parent), FuPriority.Add, " - ", FuPriority.Mul);
			break;
		case FuToken.Asterisk:
			WriteBinaryExpr(expr, parent > FuPriority.Mul, FuPriority.Mul, " * ", FuPriority.Primary);
			break;
		case FuToken.Slash:
			WriteBinaryExpr(expr, parent > FuPriority.Mul, FuPriority.Mul, " / ", FuPriority.Primary);
			break;
		case FuToken.Mod:
			WriteBinaryExpr(expr, parent > FuPriority.Mul, FuPriority.Mul, " % ", FuPriority.Primary);
			break;
		case FuToken.ShiftLeft:
			WriteBinaryExpr(expr, parent > FuPriority.Shift, FuPriority.Shift, " << ", FuPriority.Mul);
			break;
		case FuToken.ShiftRight:
			WriteBinaryExpr(expr, parent > FuPriority.Shift, FuPriority.Shift, " >> ", FuPriority.Mul);
			break;
		case FuToken.Equal:
			WriteEqual(expr.Left, expr.Right, parent, false);
			break;
		case FuToken.NotEqual:
			WriteEqual(expr.Left, expr.Right, parent, true);
			break;
		case FuToken.Less:
			WriteRel(expr, parent, " < ");
			break;
		case FuToken.LessOrEqual:
			WriteRel(expr, parent, " <= ");
			break;
		case FuToken.Greater:
			WriteRel(expr, parent, " > ");
			break;
		case FuToken.GreaterOrEqual:
			WriteRel(expr, parent, " >= ");
			break;
		case FuToken.And:
			WriteAnd(expr, parent);
			break;
		case FuToken.Or:
			WriteBinaryExpr2(expr, parent, FuPriority.Or, " | ");
			break;
		case FuToken.Xor:
			WriteBinaryExpr(expr, parent > FuPriority.Xor || parent == FuPriority.Or, FuPriority.Xor, " ^ ", FuPriority.Xor);
			break;
		case FuToken.CondAnd:
			WriteBinaryExpr(expr, parent > FuPriority.CondAnd || parent == FuPriority.CondOr, FuPriority.CondAnd, " && ", FuPriority.CondAnd);
			break;
		case FuToken.CondOr:
			WriteBinaryExpr2(expr, parent, FuPriority.CondOr, " || ");
			break;
		case FuToken.Assign:
			WriteAssign(expr, parent);
			break;
		case FuToken.AddAssign:
		case FuToken.SubAssign:
		case FuToken.MulAssign:
		case FuToken.DivAssign:
		case FuToken.ModAssign:
		case FuToken.AndAssign:
		case FuToken.OrAssign:
		case FuToken.XorAssign:
		case FuToken.ShiftLeftAssign:
		case FuToken.ShiftRightAssign:
			if (parent > FuPriority.Assign)
				WriteChar('(');
			expr.Left.Accept(this, FuPriority.Assign);
			WriteChar(' ');
			Write(expr.GetOpString());
			WriteChar(' ');
			WriteOpAssignRight(expr);
			if (parent > FuPriority.Assign)
				WriteChar(')');
			break;

		case FuToken.LeftBracket:
			if (expr.Left.Type is FuStringType)
				WriteCharAt(expr);
			else
				WriteIndexingExpr(expr, parent);
			break;

		case FuToken.Is:
			if (parent > FuPriority.Rel)
				WriteChar('(');
			expr.Left.Accept(this, FuPriority.Rel);
			Write(GetIsOperator());
			switch (expr.Right) {
			case FuSymbolReference symbol:
				WriteName(symbol.Symbol);
				break;
			case FuVar def:
				WriteTypeAndName(def);
				break;
			default:
				assert false;
			}
			if (parent > FuPriority.Rel)
				WriteChar(')');
			break;

		default:
			assert false;
		}
	}

	protected abstract void WriteStringLength!(FuExpr expr);

	protected virtual void WriteArrayLength!(FuExpr expr, FuPriority parent)
	{
		WritePostfix(expr, ".length");
	}

	protected static bool IsReferenceTo(FuExpr expr, FuId id) => expr is FuSymbolReference symbol && symbol.Symbol.Id == id;

	protected static bool IsConsoleStream(FuExpr expr) => expr is FuSymbolReference symbol && (symbol.Symbol.Id == FuId.ConsoleError || symbol.Symbol.Id == FuId.ConsoleOut);

	protected bool WriteJavaMatchProperty!(FuSymbolReference expr, FuPriority parent)
	{
		switch (expr.Symbol.Id) {
		case FuId.MatchStart:
			WritePostfix(expr.Left, ".start()");
			return true;
		case FuId.MatchEnd:
			WritePostfix(expr.Left, ".end()");
			return true;
		case FuId.MatchLength:
			if (parent > FuPriority.Add)
				WriteChar('(');
			WritePostfix(expr.Left, ".end() - ");
			WritePostfix(expr.Left, ".start()"); // FIXME: side effect
			if (parent > FuPriority.Add)
				WriteChar(')');
			return true;
		case FuId.MatchValue:
			WritePostfix(expr.Left, ".group()");
			return true;
		default:
			return false;
		}
	}

	internal override void VisitSymbolReference!(FuSymbolReference expr, FuPriority parent)
	{
		if (expr.Left == null)
			WriteLocalName(expr.Symbol, parent);
		else if (expr.Symbol.Id == FuId.StringLength)
			WriteStringLength(expr.Left);
		else if (expr.Symbol.Id == FuId.ArrayLength)
			WriteArrayLength(expr.Left, parent);
		else {
			expr.Left.Accept(this, FuPriority.Primary);
			WriteMemberOp(expr.Left, expr);
			WriteName(expr.Symbol);
		}
	}

	protected abstract void WriteCharAt!(FuBinaryExpr expr);

	protected virtual void WriteNotPromoted!(FuType type, FuExpr expr)
	{
		expr.Accept(this, FuPriority.Argument);
	}

	protected virtual void WriteEnumAsInt!(FuExpr expr, FuPriority parent)
	{
		expr.Accept(this, parent);
	}

	protected void WriteEnumHasFlag!(FuExpr obj, List<FuExpr#> args, FuPriority parent)
	{
		if (parent > FuPriority.Equality)
			WriteChar('(');
		int i = args[0].IntValue();
		if ((i & i - 1) == 0 && i != 0) { // TODO: BitOperations.IsPow2
			WriteChar('(');
			WriteEnumAsInt(obj, FuPriority.And);
			Write(" & ");
			WriteEnumAsInt(args[0], FuPriority.And);
			Write(") != 0");
		}
		else {
			Write("(~");
			WriteEnumAsInt(obj, FuPriority.Primary);
			Write(" & ");
			WriteEnumAsInt(args[0], FuPriority.And);
			Write(") == 0");
		}
		if (parent > FuPriority.Equality)
			WriteChar(')');
	}

	protected void WriteTryParseRadix!(List<FuExpr#> args)
	{
		Write(", ");
		if (args.Count == 2)
			args[1].Accept(this, FuPriority.Argument);
		else
			Write("10");
	}

	protected void WriteListAdd!(FuExpr obj, string method, List<FuExpr#> args)
	{
		obj.Accept(this, FuPriority.Primary);
		WriteChar('.');
		Write(method);
		WriteChar('(');
		FuType elementType = obj.Type.AsClassType().GetElementType();
		if (args.Count == 0)
			WriteNewStorage(elementType);
		else
			WriteNotPromoted(elementType, args[0]);
		WriteChar(')');
	}

	protected void WriteListInsert!(FuExpr obj, string method, List<FuExpr#> args, string separator = ", ")
	{
		obj.Accept(this, FuPriority.Primary);
		WriteChar('.');
		Write(method);
		WriteChar('(');
		args[0].Accept(this, FuPriority.Argument);
		Write(separator);
		FuType elementType = obj.Type.AsClassType().GetElementType();
		if (args.Count == 1)
			WriteNewStorage(elementType);
		else
			WriteNotPromoted(elementType, args[1]);
		WriteChar(')');
	}

	protected void WriteDictionaryAdd!(FuExpr obj, List<FuExpr#> args)
	{
		WriteIndexing(obj, args[0]);
		Write(" = ");
		WriteNewStorage(obj.Type.AsClassType().GetValueType());
	}

	protected void WriteContains!(FuExpr haystack, FuExpr needle, FuPriority parent)
	{
		if (parent > FuPriority.And)
			WriteChar('(');
		needle.Accept(this, FuPriority.Rel);
		Write(" in ");
		haystack.Accept(this, FuPriority.Primary);
		if (parent > FuPriority.And)
			WriteChar(')');
	}

	protected void WriteClampAsMinMax!(List<FuExpr#> args)
	{
		args[0].Accept(this, FuPriority.Argument);
		Write(", ");
		args[1].Accept(this, FuPriority.Argument);
		Write("), ");
		args[2].Accept(this, FuPriority.Argument);
		WriteChar(')');
	}

	protected void WriteRegexLiteral!(string s)
	{
		WriteChar('/');
		bool escaped = false;
		foreach (int c in s) {
			switch (c) {
			case '\\':
				if (!escaped) {
					escaped = true;
					continue;
				}
				escaped = false;
				break;
			case '"':
			case '\'':
				escaped = false;
				break;
			case '/':
				escaped = true;
				break;
			default:
				break;
			}
			if (escaped) {
				WriteChar('\\');
				escaped = false;
			}
			WriteChar(c);
		}
		WriteChar('/');
	}

	protected RegexOptions GetRegexOptions(List<FuExpr#> args)
	{
		FuExpr expr = args.Last();
		if (expr.Type is FuEnum)
			return RegexOptions.FromInt(expr.IntValue());
		return RegexOptions.None;
	}

	protected bool WriteRegexOptions!(List<FuExpr#> args, string prefix, string separator, string suffix, string i, string m, string s)
	{
		RegexOptions options = GetRegexOptions(args);
		if (options == RegexOptions.None)
			return false;
		Write(prefix);
		if (options.HasFlag(RegexOptions.IgnoreCase))
			Write(i);
		if (options.HasFlag(RegexOptions.Multiline)) {
			if (options.HasFlag(RegexOptions.IgnoreCase))
				Write(separator);
			Write(m);
		}
		if (options.HasFlag(RegexOptions.Singleline)) {
			if (options != RegexOptions.Singleline)
				Write(separator);
			Write(s);
		}
		Write(suffix);
		return true;
	}

	protected abstract void WriteCallExpr!(FuType type, FuExpr? obj, FuMethod method, List<FuExpr#> args, FuPriority parent);

	internal override void VisitCallExpr!(FuCallExpr expr, FuPriority parent)
	{
		assert expr.Method.Symbol is FuMethod method;
		WriteCallExpr(expr.Type, expr.Method.Left, method, expr.Arguments, parent);
	}

	internal override void VisitSelectExpr!(FuSelectExpr expr, FuPriority parent)
	{
		WriteCoercedSelect(expr.Type, expr, parent);
	}

	protected void EnsureChildBlock!()
	{
		if (this.AtChildStart) {
			this.AtLineStart = false;
			this.AtChildStart = false;
			WriteChar(' ');
			OpenBlock();
			this.InChildBlock = true;
		}
	}

	protected static bool HasTemporaries(FuExpr expr)
	{
		switch (expr) {
		case FuAggregateInitializer init:
			return init.Items.Any(item => HasTemporaries(item));
		case FuLiteral:
		case FuLambdaExpr:
			return false;
		case FuInterpolatedString interp:
			return interp.Parts.Any(part => HasTemporaries(part.Argument));
		case FuSymbolReference symbol:
			return symbol.Left != null && HasTemporaries(symbol.Left);
		case FuUnaryExpr unary:
			return unary.Inner != null && (HasTemporaries(unary.Inner) || unary.Inner is FuAggregateInitializer);
		case FuBinaryExpr binary:
			return HasTemporaries(binary.Left) || (binary.Op == FuToken.Is ? binary.Right is FuVar : HasTemporaries(binary.Right));
		case FuSelectExpr select:
			return HasTemporaries(select.Cond) || HasTemporaries(select.OnTrue) || HasTemporaries(select.OnFalse);
		case FuCallExpr call:
			return HasTemporaries(call.Method) || call.Arguments.Any(arg => HasTemporaries(arg));
		default:
			assert false;
		}
	}

	protected abstract void StartTemporaryVar!(FuType type);

	protected virtual void DefineObjectLiteralTemporary!(FuUnaryExpr expr)
	{
		if (expr.Inner is FuAggregateInitializer init) {
			EnsureChildBlock();
			int id = this.CurrentTemporaries.IndexOf(expr.Type);
			if (id < 0) {
				id = this.CurrentTemporaries.Count;
				StartTemporaryVar(expr.Type);
				this.CurrentTemporaries.Add(expr);
			}
			else
				this.CurrentTemporaries[id] = expr;
			WriteTemporaryName(id);
			Write(" = ");
			assert expr.Type is FuDynamicPtrType dynamic;
			WriteNew(dynamic, FuPriority.Argument);
			EndStatement();
			foreach (FuExpr item in init.Items) {
				WriteTemporaryName(id);
				WriteAggregateInitField(expr, item);
			}
		}
	}

	protected virtual void WriteTemporariesNotSubstring!(FuExpr expr)
	{
		WriteTemporaries(expr);
	}

	protected virtual void WriteOwningTemporary!(FuExpr expr)
	{
	}

	protected virtual void WriteArgTemporary!(FuMethod method, FuVar param, FuExpr arg)
	{
	}

	protected void WriteTemporaries!(FuExpr expr)
	{
		switch (expr) {
		case FuVar def:
			if (def.Value != null) {
				if (def.Value is FuUnaryExpr unary && unary.Inner is FuAggregateInitializer)
					WriteTemporaries(unary.Inner);
				else
					WriteTemporaries(def.Value);
			}
			break;
		case FuAggregateInitializer init:
			foreach (FuExpr item in init.Items) {
				assert item is FuBinaryExpr assign;
				WriteTemporaries(assign.Right);
			}
			break;
		case FuLiteral:
		case FuLambdaExpr:
			break;
		case FuInterpolatedString interp:
			foreach (FuInterpolatedPart part in interp.Parts)
				WriteTemporariesNotSubstring(part.Argument);
			break;
		case FuSymbolReference symbol:
			if (symbol.Left != null) {
				WriteTemporaries(symbol.Left);
				WriteOwningTemporary(symbol.Left);
			}
			break;
		case FuUnaryExpr unary:
			if (unary.Inner != null) {
				WriteTemporaries(unary.Inner);
				DefineObjectLiteralTemporary(unary);
			}
			break;
		case FuBinaryExpr binary:
			WriteTemporariesNotSubstring(binary.Left);
			if (binary.Op == FuToken.Is)
				DefineIsVar(binary);
			else {
				WriteTemporaries(binary.Right);
				if (binary.Op != FuToken.Assign)
					WriteOwningTemporary(binary.Right);
			}
			break;
		case FuSelectExpr select:
			WriteTemporaries(select.Cond);
			WriteTemporaries(select.OnTrue);
			WriteTemporaries(select.OnFalse);
			break;
		case FuCallExpr call:
			WriteTemporaries(call.Method);
			assert call.Method.Symbol is FuMethod method;
			FuVar? param = method.FirstParameter();
			foreach (FuExpr arg in call.Arguments) {
				WriteTemporaries(arg);
				WriteArgTemporary(method, param, arg);
				param = param.NextVar();
			}
			break;
		default:
			assert false;
		}
	}

	protected virtual void CleanupTemporary!(int i, FuExpr temp)
	{
	}

	protected void CleanupTemporaries!()
	{
		for (int i = this.CurrentTemporaries.Count; --i >= 0; ) {
			FuExpr temp = this.CurrentTemporaries[i];
			if (!(temp is FuType)) {
				CleanupTemporary(i, temp);
				this.CurrentTemporaries[i] = temp.Type;
			}
		}
	}

	internal override void VisitExpr!(FuExpr statement)
	{
		WriteTemporaries(statement);
		statement.Accept(this, FuPriority.Statement);
		WriteCharLine(';');
		if (statement is FuVar def)
			WriteInitCode(def);
		CleanupTemporaries();
	}

	internal override void VisitConst!(FuConst statement)
	{
	}

	protected abstract void WriteAssertCast!(FuBinaryExpr expr);

	protected abstract void WriteAssert!(FuAssert statement);

	internal override void VisitAssert!(FuAssert statement)
	{
		if (statement.Cond is FuBinaryExpr binary && binary.Op == FuToken.Is && binary.Right is FuVar)
			WriteAssertCast(binary);
		else
			WriteAssert(statement);
	}

	protected void WriteFirstStatements!(List<FuStatement#> statements, int count)
	{
		for (int i = 0; i < count; i++)
			statements[i].AcceptStatement(this);
	}

	protected virtual void WriteStatements!(List<FuStatement#> statements)
	{
		WriteFirstStatements(statements, statements.Count);
	}

	protected virtual void CleanupBlock!(FuBlock statement)
	{
	}

	protected void TrimTemporariesAndCloseBlock!(int temporariesCount)
	{
		this.CurrentTemporaries.RemoveRange(temporariesCount, this.CurrentTemporaries.Count - temporariesCount);
		CloseBlock();
	}

	internal override void VisitBlock!(FuBlock statement)
	{
		if (this.AtChildStart) {
			this.AtLineStart = false;
			this.AtChildStart = false;
			WriteChar(' ');
		}
		OpenBlock();
		int temporariesCount = this.CurrentTemporaries.Count;
		WriteStatements(statement.Statements);
		CleanupBlock(statement);
		TrimTemporariesAndCloseBlock(temporariesCount);
	}

	protected virtual void WriteChild!(FuStatement! statement)
	{
		bool wasInChildBlock = this.InChildBlock;
		this.AtLineStart = true;
		this.AtChildStart = true;
		this.InChildBlock = false;
		int temporariesCount = this.CurrentTemporaries.Count;
		statement.AcceptStatement(this);
		if (this.InChildBlock)
			TrimTemporariesAndCloseBlock(temporariesCount);
		else if (!(statement is FuBlock))
			this.Indent--;
		this.InChildBlock = wasInChildBlock;
	}

	protected virtual void StartBreakGoto!()
	{
		Write("goto fuafterswitch");
	}

	internal override void VisitBreak!(FuBreak statement)
	{
		if (statement.LoopOrSwitch is FuSwitch switchStatement) {
			int gotoId = this.SwitchesWithGoto.IndexOf(switchStatement);
			if (gotoId >= 0) {
				StartBreakGoto();
				VisitLiteralLong(gotoId, FuPriority.Primary);
				WriteCharLine(';');
				return;
			}
		}
		WriteLine("break;");
	}

	internal override void VisitContinue!(FuContinue statement)
	{
		WriteLine("continue;");
	}

	internal override void VisitDoWhile!(FuDoWhile statement)
	{
		Write("do");
		WriteChild(statement.Body);
		Write("while (");
		statement.Cond.Accept(this, FuPriority.Argument);
		WriteLine(");");
	}

	internal override void VisitFor!(FuFor statement)
	{
		if (statement.Cond != null)
			WriteTemporaries(statement.Cond);
		Write("for (");
		if (statement.Init != null)
			statement.Init.Accept(this, FuPriority.Statement);
		WriteChar(';');
		if (statement.Cond != null) {
			WriteChar(' ');
			statement.Cond.Accept(this, FuPriority.Argument);
		}
		WriteChar(';');
		if (statement.Advance != null) {
			WriteChar(' ');
			statement.Advance.Accept(this, FuPriority.Statement);
		}
		WriteChar(')');
		WriteChild(statement.Body);
	}

	protected void WriteTryParseFailure!(FuCallExpr call, FuStatement!? onFailure)
	{
		if (!(onFailure is FuReturn || onFailure is FuThrow)) {
			call.Method.Left.Accept(this, FuPriority.Assign);
			Write(" = 0");
			EndStatement();
		}
		if (onFailure != null)
			FlattenBlock(onFailure);
	}

	protected void EndTryParse!(FuCallExpr call, FuStatement!? onParsed, string statement, FuStatement!? onFailure)
	{
		if (onParsed != null)
			FlattenBlock(onParsed);
		CloseBlock();
		Write(statement);
		OpenBlock();
		WriteTryParseFailure(call, onFailure);
		CloseBlock();
	}

	protected virtual void WriteTryParseStatement!(FuCallExpr call, FuStatement!? onParsed, FuStatement!? onFailure)
	{
		assert false;
	}

	protected bool TryWriteTryParse!(FuExpr expr, FuStatement!? onParsed, FuStatement!? onFailure)
	{
		if (expr is FuCallExpr call) {
			switch (call.Method.Symbol.Id) {
			case FuId.IntTryParse:
			case FuId.NIntTryParse:
			case FuId.LongTryParse:
			case FuId.FloatTryParse:
			case FuId.DoubleTryParse:
				WriteTryParseStatement(call, onParsed, onFailure);
				return true;
			default:
				break;
			}
		}
		return false;
	}

	internal bool TryWriteIfTryParse!(FuIf statement)
	{
		if (statement.Cond is FuPrefixExpr not && not.Op == FuToken.ExclamationMark)
			return TryWriteTryParse(not.Inner, statement.OnFalse, statement.OnTrue);
		return TryWriteTryParse(statement.Cond, statement.OnTrue, statement.OnFalse);
	}

	protected virtual bool EmbedIfWhileIsVar!(FuExpr expr, bool write) => false;

	void StartIfWhile!(FuExpr expr)
	{
		EmbedIfWhileIsVar(expr, true);
		expr.Accept(this, FuPriority.Argument);
		WriteChar(')');
	}

	protected virtual void StartIf!(FuExpr expr)
	{
		Write("if (");
		StartIfWhile(expr);
	}

	void WriteIf!(FuIf statement)
	{
		StartIf(statement.Cond);
		WriteChild(statement.OnTrue);
		if (statement.OnFalse != null) {
			Write("else");
			if (statement.OnFalse is FuIf! elseIf) {
				bool wasInChildBlock = this.InChildBlock;
				this.AtLineStart = true;
				this.AtChildStart = true;
				this.InChildBlock = false;
				if (!EmbedIfWhileIsVar(elseIf.Cond, false)) // FIXME: IsVar but not object literal
					WriteTemporaries(elseIf.Cond);
				if (this.InChildBlock) {
					WriteIf(elseIf);
					CloseBlock();
				}
				else {
					this.AtLineStart = false;
					this.AtChildStart = false;
					WriteChar(' ');
					WriteIf(elseIf);
				}
				this.InChildBlock = wasInChildBlock;
			}
			else
				WriteChild(statement.OnFalse);
		}
	}

	internal override void VisitIf!(FuIf statement)
	{
		if (!EmbedIfWhileIsVar(statement.Cond, false)) // FIXME: IsVar but not object literal
			WriteTemporaries(statement.Cond);
		WriteIf(statement);
	}

	internal override void VisitNative!(FuNative statement)
	{
		Write(statement.Content);
	}

	internal override void VisitReturn!(FuReturn statement)
	{
		if (statement.Value == null)
			WriteLine("return;");
		else {
			WriteTemporaries(statement.Value);
			Write("return ");
			WriteStronglyCoerced(this.CurrentMethod.Type, statement.Value);
			WriteCharLine(';');
			CleanupTemporaries();
		}
	}

	protected void DefineVar!(FuExpr value)
	{
		if (value is FuVar def) {
			WriteVar(def);
			EndStatement();
		}
	}

	protected void WriteSwitchLabel!(FuSwitch statement)
	{
		Write("fuswitch");
		VisitLiteralLong(this.SwitchesWithGoto.Count, FuPriority.Primary);
		this.SwitchesWithGoto.Add(statement);
		Write(": ");
	}

	protected virtual void WriteSwitchCaseTypeVar!(FuExpr value)
	{
	}

	protected virtual void WriteSwitchValue!(FuExpr expr)
	{
		expr.Accept(this, FuPriority.Argument);
	}

	protected virtual void WriteSwitchCaseValue!(FuSwitch statement, FuExpr value)
	{
		if (value is FuBinaryExpr when1 && when1.Op == FuToken.When) {
			WriteSwitchCaseValue(statement, when1.Left);
			Write(" when ");
			when1.Right.Accept(this, FuPriority.Argument);
		}
		else
			WriteCoercedLiteral(statement.Value.Type, value);
	}

	protected virtual void WriteSwitchCaseBody!(List<FuStatement#> statements)
	{
		WriteStatements(statements);
	}

	protected virtual void WriteSwitchCase!(FuSwitch statement, FuCase kase)
	{
		foreach (FuExpr value in kase.Values) {
			Write("case ");
			WriteSwitchCaseValue(statement, value);
			WriteCharLine(':');
		}
		this.Indent++;
		WriteSwitchCaseBody(kase.Body);
		this.Indent--;
	}

	protected void StartSwitch!(FuSwitch statement)
	{
		Write("switch (");
		WriteSwitchValue(statement.Value);
		WriteLine(") {");
		foreach (FuCase kase in statement.Cases)
			WriteSwitchCase(statement, kase);
	}

	protected virtual bool NeedsSwitchVar(FuExpr expr) => !expr.IsSimple();

	protected virtual void WriteSwitchVar!(FuExpr expr)
	{
		StartTemporaryVar(expr.Type);
		Write("fuSwitchValue = ");
		expr.Accept(this, FuPriority.Argument);
	}

	protected void WriteExprOrSwitchValue!(bool switchVar, FuExpr expr, FuPriority parent)
	{
		if (switchVar)
			Write("fuSwitchValue");
		else
			expr.Accept(this, parent);
	}

	protected virtual void WriteSwitchCaseCond!(bool switchVar, FuExpr switchValue, FuExpr value, FuPriority parent)
	{
		if (value is FuBinaryExpr when1 && when1.Op == FuToken.When) {
			if (parent > FuPriority.SelectCond)
				WriteChar('(');
			WriteSwitchCaseCond(switchVar, switchValue, when1.Left, FuPriority.CondAnd);
			Write(" && ");
			when1.Right.Accept(this, FuPriority.CondAnd);
			if (parent > FuPriority.SelectCond)
				WriteChar(')');
		}
		else
			WriteEqual(switchVar ? null : switchValue, value, parent, false);
	}

	protected virtual void WriteIfCaseBody!(List<FuStatement#> body, bool doWhile, FuSwitch statement, FuCase? kase)
	{
		int length = FuSwitch.LengthWithoutTrailingBreak(body);
		if (doWhile && FuSwitch.HasEarlyBreak(body)) {
			this.Indent++;
			WriteNewLine();
			Write("do ");
			OpenBlock();
			WriteFirstStatements(body, length);
			CloseBlock();
			WriteLine("while (false);");
			this.Indent--;
		}
		else if (length != 1 || body[0] is FuIf || body[0] is FuSwitch /* potentially as ifs */) {
			WriteChar(' ');
			OpenBlock();
			WriteFirstStatements(body, length);
			CloseBlock();
		}
		else
			WriteChild(body[0]);
	}

	protected void WriteSwitchAsIfs!(FuSwitch statement, bool doWhile)
	{
		bool switchVar = NeedsSwitchVar(statement.Value);
		if (switchVar) {
			OpenBlock();
			WriteSwitchVar(statement.Value);
			WriteCharLine(';');
		}
		foreach (FuCase kase in statement.Cases) {
			foreach (FuExpr value in kase.Values) {
				if (value is FuBinaryExpr when1 && when1.Op == FuToken.When) {
					DefineVar(when1.Left);
					WriteTemporaries(when1);
				}
				else
					WriteSwitchCaseTypeVar(value);
			}
		}
		string op = "if (";
		foreach (FuCase kase in statement.Cases) {
			FuPriority parent = kase.Values.Count == 1 ? FuPriority.Argument : FuPriority.CondOr;
			foreach (FuExpr value in kase.Values) {
				Write(op);
				WriteSwitchCaseCond(switchVar, statement.Value, value, parent);
				op = " || ";
			}
			WriteChar(')');
			WriteIfCaseBody(kase.Body, doWhile, statement, kase);
			op = "else if (";
		}
		if (statement.HasDefault()) {
			Write("else");
			WriteIfCaseBody(statement.DefaultBody, doWhile, statement, null);
		}
		if (switchVar)
			CloseBlock();
	}

	internal override void VisitSwitch!(FuSwitch statement)
	{
		WriteTemporaries(statement.Value);
		StartSwitch(statement);
		if (statement.DefaultBody.Count > 0) {
			WriteLine("default:");
			this.Indent++;
			WriteSwitchCaseBody(statement.DefaultBody);
			this.Indent--;
		}
		WriteCharLine('}');
	}

	protected virtual void WriteException!()
	{
		Write("Exception");
	}

	protected void WriteExceptionClass!(FuSymbol klass)
	{
		if (klass.Name == "Exception")
			WriteException();
		else
			WriteName(klass);
	}

	protected virtual void WriteThrowMessage!(FuExpr? expr)
	{
		if (expr != null)
			expr.Accept(this, FuPriority.Argument);
	}

	protected void WriteThrowArgument!(FuThrow statement)
	{
		WriteExceptionClass(statement.Class.Symbol);
		WriteChar('(');
		WriteThrowMessage(statement.Message);
		WriteChar(')');
	}

	internal override void VisitThrow!(FuThrow statement)
	{
		Write("throw new ");
		WriteThrowArgument(statement);
		WriteCharLine(';');
	}

	internal override void VisitWhile!(FuWhile statement)
	{
		if (!EmbedIfWhileIsVar(statement.Cond, false)) // FIXME: IsVar but not object literal
			WriteTemporaries(statement.Cond);
		Write("while (");
		StartIfWhile(statement.Cond);
		WriteChild(statement.Body);
	}

	protected void FlattenBlock!(FuStatement! statement)
	{
		if (statement is FuBlock block)
			WriteStatements(block.Statements);
		else
			statement.AcceptStatement(this);
	}

	protected virtual bool HasInitCode(FuNamedValue def) => GetAggregateInitializer(def) != null;

	protected virtual bool NeedsConstructor(FuClass klass)
	{
		for (FuSymbol? symbol = klass.First; symbol != null; symbol = symbol.Next) {
			if (symbol is FuField field && HasInitCode(field))
				return true;
		}
		return klass.Constructor != null;
	}

	protected virtual void WriteInitField!(FuField field)
	{
		WriteInitCode(field);
	}

	protected void WriteConstructorBody!(FuClass klass)
	{
		for (FuSymbol? symbol = klass.First; symbol != null; symbol = symbol.Next) {
			if (symbol is FuField field)
				WriteInitField(field);
		}
		if (klass.Constructor != null) {
			this.CurrentMethod = klass.Constructor;
			assert klass.Constructor.Body is FuBlock block;
			WriteStatements(block.Statements);
			this.CurrentMethod = null;
		}
		this.SwitchesWithGoto.Clear();
		this.CurrentTemporaries.Clear();
	}

	protected virtual void WriteParameter!(FuVar param)
	{
		WriteTypeAndName(param);
	}

	protected void WriteRemainingParameters!(FuMethod method, bool first, bool defaultArguments)
	{
		for (FuVar? param = method.FirstParameter(); param != null; param = param.NextVar()) {
			if (!first)
				Write(", ");
			first = false;
			WriteParameter(param);
			if (defaultArguments)
				WriteVarInit(param);
		}
		WriteChar(')');
	}

	protected void WriteParameters!(FuMethod method, bool defaultArguments)
	{
		WriteChar('(');
		WriteRemainingParameters(method, true, defaultArguments);
	}

	protected virtual bool IsShortMethod(FuMethod method) => false;

	protected void WriteBody!(FuMethod method)
	{
		if (method.CallType == FuCallType.Abstract)
			WriteCharLine(';');
		else {
			this.CurrentMethod = method;
			if (IsShortMethod(method)) {
				Write(" => ");
				assert method.Body is FuReturn ret;
				WriteCoerced(method.Type, ret.Value, FuPriority.Argument);
				WriteCharLine(';');
			}
			else {
				WriteNewLine();
				OpenBlock();
				FlattenBlock(method.Body);
				CloseBlock();
			}
			this.CurrentMethod = null;
		}
	}

	protected void WritePublic!(FuContainerType container)
	{
		if (container.IsPublic)
			Write("public ");
	}

	protected void WriteEnumValue!(FuConst konst, bool withValue)
	{
		WriteDoc(konst.Documentation);
		WriteName(konst);
		if (withValue && !(konst.Value is FuImplicitEnumValue)) {
			Write(" = ");
			konst.Value.Accept(this, FuPriority.Argument);
		}
	}

	internal override void VisitEnumValue!(FuConst konst, FuConst? previous)
	{
		if (previous != null)
			WriteCharLine(',');
		WriteEnumValue(konst, true);
	}

	protected abstract void WriteEnum!(FuEnum enu);

	protected virtual void WriteRegexOptionsEnum!(FuProgram program)
	{
		if (program.RegexOptionsEnum)
			WriteEnum(program.System.RegexOptionsEnum);
	}

	protected void StartClass!(FuClass klass, string suffix, string extendsClause)
	{
		Write("class ");
		Write(klass.Name);
		Write(suffix);
		if (klass.HasBaseClass()) {
			Write(extendsClause);
			WriteExceptionClass(klass.Parent);
		}
	}

	protected void OpenClass!(FuClass klass, string suffix, string extendsClause)
	{
		StartClass(klass, suffix, extendsClause);
		WriteNewLine();
		OpenBlock();
	}

	protected abstract void WriteConst!(FuConst konst);

	protected abstract void WriteField!(FuField field);

	protected abstract void WriteMethod!(FuMethod method);

	protected void WriteMembers!(FuClass klass, bool constArrays)
	{
		for (FuSymbol? symbol = klass.First; symbol != null; symbol = symbol.Next) {
			switch (symbol) {
			case FuConst konst:
				WriteConst(konst);
				break;
			case FuField field:
				WriteField(field);
				break;
			case FuMethod method:
				WriteMethod(method);
				this.SwitchesWithGoto.Clear();
				this.CurrentTemporaries.Clear();
				break;
			case FuNative nat:
				VisitNative(nat);
				break;
			default:
				assert false;
			}
		}
		if (constArrays) {
			foreach (FuConst konst in klass.ConstArrays)
				WriteConst(konst);
		}
	}

	protected bool WriteBaseClass!(FuClass klass, FuProgram program)
	{
		if (klass.Name == "Exception")
			return false;
		// topological sorting of class hierarchy
		if (this.WrittenTypes.Contains(klass))
			return false;
		this.WrittenTypes.Add(klass);
		if (klass.Parent is FuClass baseClass)
			WriteClass(baseClass, program);
		return true;
	}

	protected abstract void WriteClass!(FuClass klass, FuProgram program);

	protected void WriteTypes!(FuProgram program)
	{
		WriteRegexOptionsEnum(program);
		for (FuSymbol? type = program.First; type != null; type = type.Next) {
			switch (type) {
			case FuClass klass:
				WriteClass(klass, program);
				break;
			case FuEnum enu:
				WriteEnum(enu);
				break;
			default:
				assert false;
			}
		}
	}

	public abstract void WriteProgram!(FuProgram program, string outputFile, string namespace);
}