// GenCs.fu - C# code generator
//
// 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 class GenCs : GenTyped
{
	bool NewArray;

	protected override string GetTargetName() => "C#";

	protected override void StartDocLine!()
	{
		Write("/// ");
	}

	protected override void WriteDocPara!(FuDocPara para, bool many)
	{
		if (many) {
			WriteNewLine();
			Write("/// <para>");
		}
		foreach (FuDocInline inline in para.Children) {
			switch (inline) {
			case FuDocText text:
				WriteXmlDoc(text.Text);
				break;
			case FuDocCode code:
				switch (code.Text) {
				case "true":
				case "false":
				case "null":
					Write("<see langword=\"");
					Write(code.Text);
					Write("\" />");
					break;
				default:
					Write("<c>");
					WriteXmlDoc(code.Text);
					Write("</c>");
					break;
				}
				break;
			case FuDocLine:
				WriteNewLine();
				StartDocLine();
				break;
			default:
				assert false;
			}
		}
		if (many)
			Write("</para>");
	}

	protected override void WriteDocList!(FuDocList list)
	{
		WriteNewLine();
		WriteLine("/// <list type=\"bullet\">");
		foreach (FuDocPara item in list.Items) {
			Write("/// <item>");
			WriteDocPara(item, false);
			WriteLine("</item>");
		}
		Write("/// </list>");
	}

	protected override void WriteDoc!(FuCodeDoc? doc)
	{
		if (doc == null)
			return;
		Write("/// <summary>");
		WriteDocPara(doc.Summary, false);
		WriteLine("</summary>");
		if (doc.Details.Count > 0) {
			Write("/// <remarks>");
			if (doc.Details.Count == 1)
				WriteDocBlock(doc.Details[0], false);
			else {
				foreach (FuDocBlock block in doc.Details)
					WriteDocBlock(block, true);
			}
			WriteLine("</remarks>");
		}
	}

	protected override void WriteName!(FuSymbol symbol)
	{
		if (symbol is FuConst konst && konst.InMethod != null) {
			Write(konst.InMethod.Name);
			WriteChar('_');
			Write(symbol.Name);
			if (konst.InMethodIndex > 0)
				VisitLiteralLong(konst.InMethodIndex, FuPriority.Primary);
			return;
		}
		Write(symbol.Name);
		switch (symbol.Name) {
		case "as":
		case "await":
		case "catch":
		case "char":
		case "checked":
		case "decimal":
		case "delegate":
		case "event":
		case "explicit":
		case "extern":
		case "finally":
		case "fixed":
		case "goto":
		case "implicit":
		case "interface":
		case "is":
		case "lock":
		case "namespace":
		case "object":
		case "operator":
		case "out":
		case "params":
		case "private":
		case "readonly":
		case "ref":
		case "sbyte":
		case "sizeof":
		case "stackalloc":
		case "struct":
		case "try":
		case "typeof":
		case "ulong":
		case "unchecked":
		case "unsafe":
		case "using":
		case "volatile":
			WriteChar('_');
			break;
		default:
			break;
		}
	}

	protected override int GetLiteralChars() => 0x10000;

	void WriteVisibility!(FuVisibility visibility)
	{
		switch (visibility) {
		case FuVisibility.Private:
			break;
		case FuVisibility.Internal:
			Write("internal ");
			break;
		case FuVisibility.Protected:
			Write("protected ");
			break;
		case FuVisibility.Public:
			Write("public ");
			break;
		default:
			assert false;
		}
	}

	void WriteCallType!(FuCallType callType, string sealedString)
	{
		switch (callType) {
		case FuCallType.Static:
			Write("static ");
			break;
		case FuCallType.Normal:
			break;
		case FuCallType.Abstract:
			Write("abstract ");
			break;
		case FuCallType.Virtual:
			Write("virtual ");
			break;
		case FuCallType.Override:
			Write("override ");
			break;
		case FuCallType.Sealed:
			Write(sealedString);
			break;
		}
	}

	void WriteElementType!(FuType elementType)
	{
		Include("System.Collections.Generic");
		WriteChar('<');
		WriteType(elementType, false);
		WriteChar('>');
	}

	protected override void WriteType!(FuType type, bool promote)
	{
		switch (type) {
		case FuIntegerType:
			switch (GetTypeId(type, promote)) {
			case FuId.SByteRange:
				Write("sbyte");
				break;
			case FuId.ByteRange:
				Write("byte");
				break;
			case FuId.ShortRange:
				Write("short");
				break;
			case FuId.UShortRange:
				Write("ushort");
				break;
			case FuId.IntType:
			case FuId.NIntType:
				Write("int");
				break;
			case FuId.LongType:
				Write("long");
				break;
			default:
				assert false;
			}
			break;
		case FuClassType klass:
			switch (klass.Class.Id) {
			case FuId.StringClass:
				Write("string");
				break;
			case FuId.ArrayPtrClass:
			case FuId.ArrayStorageClass:
				WriteType(klass.GetElementType(), false);
				Write("[]");
				break;
			case FuId.ListClass:
			case FuId.QueueClass:
			case FuId.StackClass:
			case FuId.HashSetClass:
			case FuId.SortedSetClass:
				Write(klass.Class.Name);
				WriteElementType(klass.GetElementType());
				break;
			case FuId.PriorityQueueClass:
			case FuId.DictionaryClass:
			case FuId.SortedDictionaryClass:
			case FuId.OrderedDictionaryClass:
				Include("System.Collections.Generic");
				Write(klass.Class.Name);
				WriteChar('<');
				WriteType(klass.GetKeyType(), false);
				Write(", ");
				WriteType(klass.GetValueType(), false);
				WriteChar('>');
				break;
			case FuId.TextWriterClass:
			case FuId.StringWriterClass:
				Include("System.IO");
				Write(klass.Class.Name);
				break;
			case FuId.RegexClass:
			case FuId.MatchClass:
				Include("System.Text.RegularExpressions");
				Write(klass.Class.Name);
				break;
			case FuId.JsonElementClass:
				Include("System.Text.Json");
				Write("JsonElement");
				break;
			case FuId.LockClass:
				Write("object");
				break;
			default:
				Write(klass.Class.Name);
				break;
			}
			if (type.Nullable)
				WriteChar('?');
			break;
		default:
			Write(type.Name);
			break;
		}
	}

	protected override void WriteMemberOp!(FuExpr left, FuSymbolReference? symbol)
	{
		if (left.Type != null && left.Type.Nullable)
			WriteChar('!');
		WriteChar('.');
	}

	protected override void WriteIndexingInfix!(FuExpr collection)
	{
		if (collection.Type.Nullable)
			WriteChar('!');
	}

	protected override void WriteNewWithFields!(FuReadWriteClassType type, FuAggregateInitializer init)
	{
		Write("new ");
		WriteType(type, false);
		WriteObjectLiteral(init, " = ");
	}

	protected override void WriteCoercedLiteral!(FuType? type, FuExpr expr)
	{
		if (expr is FuLiteralChar && type is FuRangeType range && range.Max <= 0xff)
			WriteStaticCast(type, expr);
		else {
			base.WriteCoercedLiteral(type, expr);
			if (expr.Type != null && expr.Type.Nullable && type != null && !type.Nullable)
				WriteChar('!');
		}
	}

	protected override void WriteStaticCast!(FuType type, FuExpr expr)
	{
		if (expr is FuPrefixExpr unary && unary.Op == FuToken.Tilde && unary.Inner is FuLiteralLong literal &&
			(type.Id == FuId.ByteRange || type.Id == FuId.UShortRange))
			VisitLiteralLong(literal.Value ^ (type.Id == FuId.ByteRange ? 0xff : 0xffff), FuPriority.Primary);
		else {
			base.WriteStaticCast(type, expr);
			if (expr.Type.Nullable && !type.Nullable)
				WriteChar('!');
		}
	}

	protected override void WriteCoercedInternal!(FuType type, FuExpr expr, FuPriority parent)
	{
		base.WriteCoercedInternal(type, expr, parent);
		if (expr.Type.Nullable && !type.Nullable)
			WriteChar('!');
	}

	protected override bool IsPromoted(FuExpr expr) => base.IsPromoted(expr) || expr is FuLiteralChar;

	internal override void VisitInterpolatedString!(FuInterpolatedString expr, FuPriority parent)
	{
		if (expr.IsToString('u')) {
			WriteCall("char.ConvertFromUtf32", expr.Parts[0].Argument);
			return;
		}
		Write("$\"");
		foreach (FuInterpolatedPart part in expr.Parts) {
			WriteDoubling(part.Prefix, '{');
			WriteChar('{');
			if (part.Format == 'U' || part.Format == 'u')
				WriteCall("char.ConvertFromUtf32", part.Argument);
			else
				part.Argument.Accept(this, FuPriority.SelectCond);
			if (part.WidthExpr != null) {
				WriteChar(',');
				VisitLiteralLong(part.Width, FuPriority.Argument);
			}
			if (part.Format != ' ' && part.Format != 'U' && part.Format != 'u') {
				WriteChar(':');
				WriteChar(part.Format);
				if (part.Precision >= 0)
					VisitLiteralLong(part.Precision, FuPriority.Argument);
			}
			WriteChar('}');
		}
		WriteDoubling(expr.Suffix, '{');
		WriteChar('"');
	}

	protected override void WriteNewArray!(FuType elementType, FuExpr lengthExpr, FuPriority parent)
	{
		if (elementType is FuStorageType) {
			this.NewArray = true;
			Write("Fu.NewArray(");
			lengthExpr.Accept(this, FuPriority.Argument);
			Write(", () => ");
			WriteNewStorage(elementType);
			WriteChar(')');
		}
		else {
			Write("new ");
			WriteType(elementType.GetBaseType(), false);
			WriteChar('[');
			lengthExpr.Accept(this, FuPriority.Argument);
			WriteChar(']');
			while (elementType is FuClassType array && array.IsArray()) {
				Write("[]");
				elementType = array.GetElementType();
			}
		}
	}

	protected override void WriteNew!(FuReadWriteClassType klass, FuPriority parent)
	{
		Write("new ");
		WriteType(klass, false);
		Write("()");
	}

	protected override bool HasInitCode(FuNamedValue def) => false;

	protected override void WriteInitCode!(FuNamedValue def)
	{
	}

	protected override void WriteResource!(string name, int length)
	{
		Write("FuResource.");
		WriteResourceName(name);
	}

	protected override void WriteStringLength!(FuExpr expr)
	{
		WritePostfix(expr, ".Length");
	}

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

	internal override void VisitSymbolReference!(FuSymbolReference expr, FuPriority parent)
	{
		switch (expr.Symbol.Id) {
		case FuId.ConsoleError:
			Include("System");
			Write("Console.Error");
			break;
		case FuId.ConsoleOut:
			Include("System");
			Write("Console.Out");
			break;
		case FuId.MatchStart:
			WritePostfix(expr.Left, ".Index");
			break;
		case FuId.MatchEnd:
			if (parent > FuPriority.Add)
				WriteChar('(');
			WritePostfix(expr.Left, ".Index + ");
			WriteStringLength(expr.Left); // FIXME: side effect
			if (parent > FuPriority.Add)
				WriteChar(')');
			break;
		case FuId.MathNaN:
		case FuId.MathNegativeInfinity:
		case FuId.MathPositiveInfinity:
			Write("float.");
			Write(expr.Symbol.Name);
			break;
		default:
			base.VisitSymbolReference(expr, parent);
			break;
		}
	}

	void WriteJsonElementIs!(FuExpr obj, string name, FuPriority parent)
	{
		if (parent > FuPriority.Equality)
			WriteChar('(');
		WritePostfix(obj, ".ValueKind == JsonValueKind.");
		Write(name);
		if (parent > FuPriority.Equality)
			WriteChar(')');
	}

	protected override void WriteCallExpr!(FuType type, FuExpr? obj, FuMethod method, List<FuExpr#> args, FuPriority parent)
	{
		switch (method.Id) {
		case FuId.None:
		case FuId.ClassToString:
		case FuId.EnumHasFlag:
		case FuId.StringContains:
		case FuId.StringEndsWith:
		case FuId.StringReplace:
		case FuId.StringStartsWith:
		case FuId.StringSubstring:
		case FuId.StringToLower:
		case FuId.StringToUpper:
		case FuId.ListAddRange:
		case FuId.ListClear:
		case FuId.ListContains:
		case FuId.ListCopyTo:
		case FuId.ListIndexOf:
		case FuId.ListRemoveAt:
		case FuId.ListRemoveRange:
		case FuId.ListSortAll:
		case FuId.QueueClear:
		case FuId.QueueDequeue:
		case FuId.QueuePeek:
		case FuId.StackClear:
		case FuId.StackPeek:
		case FuId.StackPop:
		case FuId.PriorityQueueClear:
		case FuId.PriorityQueueDequeue:
		case FuId.PriorityQueuePeek:
		case FuId.HashSetClear:
		case FuId.HashSetContains:
		case FuId.HashSetRemove:
		case FuId.SortedSetClear:
		case FuId.SortedSetContains:
		case FuId.SortedSetRemove:
		case FuId.DictionaryClear:
		case FuId.DictionaryContainsKey:
		case FuId.DictionaryRemove:
		case FuId.SortedDictionaryClear:
		case FuId.SortedDictionaryContainsKey:
		case FuId.SortedDictionaryRemove:
		case FuId.OrderedDictionaryClear:
		case FuId.OrderedDictionaryContainsKey:
		case FuId.OrderedDictionaryRemove:
		case FuId.TextWriterFlush:
		case FuId.ConsoleReadLine:
		case FuId.StringWriterToString:
		case FuId.BitConverterInt32BitsToSingle:
		case FuId.BitConverterInt64BitsToDouble:
		case FuId.BitConverterSingleToInt32Bits:
		case FuId.BitConverterDoubleToInt64Bits:
		case FuId.ConvertToBase64String:
		case FuId.JsonElementGetString:
		case FuId.JsonElementGetDouble:
		case FuId.JsonElementGetBoolean:
			if (obj != null) {
				obj.Accept(this, FuPriority.Primary);
				WriteMemberOp(obj, null);
			}
			WriteName(method);
			WriteCoercedArgsInParentheses(method, args);
			break;
		case FuId.EnumFromInt:
			WriteStaticCast(type, args[0]);
			break;
		case FuId.EnumToInt:
			Write("(int) ");
			obj.Accept(this, FuPriority.Primary);
			break;
		case FuId.IntTryParse:
		case FuId.NIntTryParse:
		case FuId.LongTryParse:
		case FuId.FloatTryParse:
		case FuId.DoubleTryParse:
			WriteType(obj.Type, false);
			Write(".TryParse(");
			args[0].Accept(this, FuPriority.Argument);
			if (args.Count == 2) {
				if (!(args[1] is FuLiteralLong radix) || radix.Value != 16)
					NotSupported(args[1], "Radix");
				Include("System.Globalization");
				Write(", NumberStyles.HexNumber, null");
			}
			Write(", out ");
			obj.Accept(this, FuPriority.Argument);
			WriteChar(')');
			break;
		case FuId.StringIndexOf:
		case FuId.StringLastIndexOf:
			obj.Accept(this, FuPriority.Primary);
			WriteMemberOp(obj, null);
			Write(method.Name);
			WriteChar('(');
			int c = GetOneAscii(args[0]);
			if (c >= 0)
				VisitLiteralChar(c);
			else
				args[0].Accept(this, FuPriority.Argument);
			WriteChar(')');
			break;
		case FuId.ArrayBinarySearchAll:
		case FuId.ArrayBinarySearchPart:
			Include("System");
			Write("Array.BinarySearch(");
			obj.Accept(this, FuPriority.Argument);
			Write(", ");
			if (args.Count == 3) {
				args[1].Accept(this, FuPriority.Argument);
				Write(", ");
				args[2].Accept(this, FuPriority.Argument);
				Write(", ");
			}
			WriteNotPromoted(obj.Type.AsClassType().GetElementType(), args[0]);
			WriteChar(')');
			break;
		case FuId.ArrayContains:
			Include("System.Linq");
			WriteMethodCall(obj, "Contains", args[0]);
			break;
		case FuId.ArrayCopyTo:
			Include("System");
			Write("Array.Copy(");
			obj.Accept(this, FuPriority.Argument);
			Write(", ");
			WriteCoercedArgs(method, args);
			WriteChar(')');
			break;
		case FuId.ArrayFillAll:
		case FuId.ArrayFillPart:
			Include("System");
			if (args[0] is FuLiteral literal && literal.IsDefaultValue()) {
				Write("Array.Clear(");
				obj.Accept(this, FuPriority.Argument);
				if (args.Count == 1) {
					// .NET Framework compatibility
					Write(", 0, ");
					WriteArrayStorageLength(obj, FuPriority.Argument);
				}
			}
			else {
				Write("Array.Fill(");
				obj.Accept(this, FuPriority.Argument);
				Write(", ");
				WriteNotPromoted(obj.Type.AsClassType().GetElementType(), args[0]);
			}
			if (args.Count == 3) {
				Write(", ");
				args[1].Accept(this, FuPriority.Argument);
				Write(", ");
				args[2].Accept(this, FuPriority.Argument);
			}
			WriteChar(')');
			break;
		case FuId.ArraySortAll:
			Include("System");
			WriteCall("Array.Sort", obj);
			break;
		case FuId.ArraySortPart:
			Include("System");
			WriteCall("Array.Sort", obj, args[0], args[1]);
			break;
		case FuId.ListAdd:
		case FuId.HashSetAdd:
		case FuId.SortedSetAdd:
			WriteListAdd(obj, "Add", args);
			break;
		case FuId.ListAll:
			WriteMethodCall(obj, "TrueForAll", args[0]);
			break;
		case FuId.ListAny:
			WriteMethodCall(obj, "Exists", args[0]);
			break;
		case FuId.ListInsert:
			WriteListInsert(obj, "Insert", args);
			break;
		case FuId.ListLast:
			WritePostfix(obj, "[^1]");
			break;
		case FuId.QueueEnqueue:
			WriteListAdd(obj, "Enqueue", args);
			break;
		case FuId.StackPush:
			WriteListAdd(obj, "Push", args);
			break;
		case FuId.ListSortPart:
			WritePostfix(obj, ".Sort(");
			WriteCoercedArgs(method, args);
			Write(", null)");
			break;
		case FuId.PriorityQueueEnqueue:
			WritePostfix(obj, ".Enqueue(");
			WriteNotPromoted(obj.Type.AsClassType().GetElementType(), args[0]);
			Write(", ");
			WriteNotPromoted(obj.Type.AsClassType().GetValueType(), args[1]);
			WriteChar(')');
			break;
		case FuId.DictionaryAdd:
			WritePostfix(obj, ".Add(");
			args[0].Accept(this, FuPriority.Argument);
			Write(", ");
			WriteNewStorage(obj.Type.AsClassType().GetValueType());
			WriteChar(')');
			break;
		case FuId.TextWriterWrite:
		case FuId.TextWriterWriteLine:
		case FuId.ConsoleWrite:
		case FuId.ConsoleWriteLine:
			if (method.Id == FuId.ConsoleWrite || method.Id == FuId.ConsoleWriteLine)
				Include("System");
			obj.Accept(this, FuPriority.Primary);
			WriteMemberOp(obj, null);
			Write(method.Name);
			WriteChar('(');
			if (args.Count != 0) {
				if (args[0] is FuLiteralChar) {
					Write("(int) ");
					args[0].Accept(this, FuPriority.Primary);
				}
				else
					args[0].Accept(this, FuPriority.Argument);
			}
			WriteChar(')');
			break;
		case FuId.TextWriterWriteChar:
			WriteCharMethodCall(obj, "Write", args[0]);
			break;
		case FuId.TextWriterWriteCodePoint:
			WritePostfix(obj, ".Write(");
			if (args[0] is FuLiteralChar literalChar && literalChar.Value < 0x10000)
				args[0].Accept(this, FuPriority.Argument);
			else {
				Include("System.Text");
				WriteCall("new Rune", args[0]);
			}
			WriteChar(')');
			break;
		case FuId.StringWriterClear:
			WritePostfix(obj, ".GetStringBuilder().Clear()");
			break;
		case FuId.UTF8GetByteCount:
			Include("System.Text");
			WriteCall("Encoding.UTF8.GetByteCount", args[0]);
			break;
		case FuId.UTF8GetBytes:
			Include("System.Text");
			Write("Encoding.UTF8.GetBytes(");
			args[0].Accept(this, FuPriority.Argument);
			Write(", 0, ");
			WritePostfix(args[0], ".Length, "); // FIXME: side effect
			args[1].Accept(this, FuPriority.Argument);
			Write(", ");
			args[2].Accept(this, FuPriority.Argument);
			WriteChar(')');
			break;
		case FuId.UTF8GetString:
			Include("System.Text");
			Write("Encoding.UTF8.GetString");
			WriteInParentheses(args);
			break;
		case FuId.EnvironmentGetEnvironmentVariable:
			Include("System");
			Write("Environment.GetEnvironmentVariable");
			WriteInParentheses(args);
			break;
		case FuId.DateTimeOffsetUtcNowToUnixTimeMilliseconds:
			Include("System");
			Write("DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()");
			break;
		case FuId.RegexCompile:
			Include("System.Text.RegularExpressions");
			Write("new Regex");
			WriteInParentheses(args);
			break;
		case FuId.RegexEscape:
		case FuId.RegexIsMatchStr:
		case FuId.RegexIsMatchRegex:
			Include("System.Text.RegularExpressions");
			obj.Accept(this, FuPriority.Primary);
			WriteChar('.');
			Write(method.Name);
			WriteInParentheses(args);
			break;
		case FuId.MatchFindStr:
			Include("System.Text.RegularExpressions");
			WriteChar('(');
			obj.Accept(this, FuPriority.Assign);
			Write(" = Regex.Match");
			WriteInParentheses(args);
			Write(").Success");
			break;
		case FuId.MatchFindRegex:
			Include("System.Text.RegularExpressions");
			WriteChar('(');
			obj.Accept(this, FuPriority.Assign);
			Write(" = ");
			WriteMethodCall(args[1], "Match", args[0]);
			Write(").Success");
			break;
		case FuId.MatchGetCapture:
			WritePostfix(obj, ".Groups[");
			args[0].Accept(this, FuPriority.Argument);
			Write("].Value");
			break;
		case FuId.JsonElementParse:
			Write("JsonDocument.Parse(");
			args[0].Accept(this, FuPriority.Argument);
			Write(").RootElement");
			break;
		case FuId.JsonElementIsObject:
			WriteJsonElementIs(obj, "Object", parent);
			break;
		case FuId.JsonElementIsArray:
			WriteJsonElementIs(obj, "Array", parent);
			break;
		case FuId.JsonElementIsString:
			WriteJsonElementIs(obj, "String", parent);
			break;
		case FuId.JsonElementIsNumber:
			WriteJsonElementIs(obj, "Number", parent);
			break;
		case FuId.JsonElementIsBoolean:
			if (parent > FuPriority.CondOr)
				WriteChar('(');
			WritePostfix(obj, ".ValueKind == JsonValueKind.True || ");
			WritePostfix(obj, ".ValueKind == JsonValueKind.False"); // FIXME: side effect
			if (parent > FuPriority.CondOr)
				WriteChar(')');
			break;
		case FuId.JsonElementIsNull:
			WriteJsonElementIs(obj, "Null", parent);
			break;
		case FuId.JsonElementGetObject:
			Include("System.Linq");
			WritePostfix(obj, ".EnumerateObject().ToDictionary(p => p.Name, p => p.Value)");
			break;
		case FuId.JsonElementGetArray:
			Include("System.Linq");
			WritePostfix(obj, ".EnumerateArray().ToList()");
			break;
		case FuId.MathMethod:
		case FuId.MathCeiling:
		case FuId.MathFusedMultiplyAdd:
		case FuId.MathLog2:
		case FuId.MathRound:
		case FuId.MathSqrt:
		case FuId.MathTruncate:
			Include("System");
			Write("Math");
			if (!args.Any(arg => arg.Type.Id == FuId.DoubleType))
				WriteChar('F');
			WriteChar('.');
			Write(method.Name);
			WriteInParentheses(args);
			break;
		case FuId.MathAbs:
		case FuId.MathClamp:
		case FuId.MathMax:
		case FuId.MathMin:
			Include("System");
			Write("Math.");
			Write(method.Name);
			WriteInParentheses(args);
			break;
		case FuId.MathIsFinite:
		case FuId.MathIsInfinity:
		case FuId.MathIsNaN:
			Write("double.");
			WriteCall(method.Name, args[0]);
			break;
		default:
			NotSupported(obj, method.Name);
			break;
		}
	}

	protected override void WriteOpAssignRight!(FuBinaryExpr expr)
	{
		WriteAssignRight(expr);
	}

	internal override void VisitBinaryExpr!(FuBinaryExpr expr, FuPriority parent)
	{
		switch (expr.Op) {
		case FuToken.AndAssign:
		case FuToken.OrAssign:
		case FuToken.XorAssign:
			if (parent > FuPriority.Assign)
				WriteChar('(');
			expr.Left.Accept(this, FuPriority.Assign);
			WriteChar(' ');
			Write(expr.GetOpString());
			WriteChar(' ');
			WriteAssignRight(expr);
			if (parent > FuPriority.Assign)
				WriteChar(')');
			break;
		default:
			base.VisitBinaryExpr(expr, parent);
			break;
		}
	}

	internal override void VisitLambdaExpr!(FuLambdaExpr expr)
	{
		WriteName(expr.First);
		Write(" => ");
		expr.Body.Accept(this, FuPriority.Statement);
	}

	protected override void DefineObjectLiteralTemporary!(FuUnaryExpr expr)
	{
	}

	protected override void DefineIsVar!(FuBinaryExpr binary)
	{
	}

	protected override void WriteAssert!(FuAssert statement)
	{
		if (statement.CompletesNormally()) {
			Include("System.Diagnostics");
			Write("Debug.Assert(");
			statement.Cond.Accept(this, FuPriority.Argument);
			if (statement.Message != null) {
				Write(", ");
				statement.Message.Accept(this, FuPriority.Argument);
			}
		}
		else {
			// assert false;
			Include("System");
			Write("throw new NotImplementedException(");
			if (statement.Message != null)
				statement.Message.Accept(this, FuPriority.Argument);
		}
		WriteLine(");");
	}

	internal override void VisitForeach!(FuForeach statement)
	{
		Write("foreach (");
		if (statement.Collection.Type is FuClassType dict && dict.Class.TypeParameterCount == 2) {
			WriteChar('(');
			WriteTypeAndName(statement.GetVar());
			Write(", ");
			WriteTypeAndName(statement.GetValueVar());
			WriteChar(')');
		}
		else
			WriteTypeAndName(statement.GetVar());
		Write(" in ");
		statement.Collection.Accept(this, FuPriority.Argument);
		WriteChar(')');
		WriteChild(statement.Body);
	}

	internal override void VisitLock!(FuLock statement)
	{
		WriteCall("lock ", statement.Lock);
		WriteChild(statement.Body);
	}

	protected override void WriteException!()
	{
		Include("System");
		Write("Exception");
	}

	protected override void WriteEnum!(FuEnum enu)
	{
		WriteNewLine();
		WriteDoc(enu.Documentation);
		if (enu is FuEnumFlags) {
			Include("System");
			WriteLine("[Flags]");
		}
		WritePublic(enu);
		Write("enum ");
		WriteLine(enu.Name);
		OpenBlock();
		enu.AcceptValues(this);
		WriteNewLine();
		CloseBlock();
	}

	protected override void WriteRegexOptionsEnum!(FuProgram program)
	{
		if (program.RegexOptionsEnum)
			Include("System.Text.RegularExpressions");
	}

	protected override void WriteConst!(FuConst konst)
	{
		WriteNewLine();
		WriteDoc(konst.Documentation);
		WriteVisibility(konst.Visibility);
		Write(konst.Type is FuArrayStorageType ? "static readonly " : "const ");
		WriteTypeAndName(konst);
		Write(" = ");
		WriteCoercedExpr(konst.Type, konst.Value);
		WriteCharLine(';');
	}

	protected override void WriteField!(FuField field)
	{
		WriteNewLine();
		WriteDoc(field.Documentation);
		WriteVisibility(field.Visibility);
		if (field.Type.IsFinal() && !field.IsAssignableStorage())
			Write("readonly ");
		WriteVar(field);
		if (!field.Type.Nullable && field.Type is FuClassType &&
			(field.Type.IsFinal() ? field.Value is FuLiteralNull  && !(field.Type is FuArrayStorageType): field.Value == null))
			Write(" = null!");
		WriteCharLine(';');
	}

	protected override void WriteParameterDoc!(FuVar param, bool first)
	{
		Write("/// <param name=\"");
		WriteName(param);
		Write("\">");
		WriteDocPara(param.Documentation.Summary, false);
		WriteLine("</param>");
	}

	protected override void WriteThrowsDoc!(FuThrowsDeclaration decl)
	{
		Write("/// <exception cref=\"");
		WriteExceptionClass(decl.Symbol);
		Write("\">");
		WriteDocPara(decl.Documentation.Summary, false);
		WriteLine("</exception>");
	}

	protected override bool IsShortMethod(FuMethod method) => method.Body is FuReturn;

	protected override void WriteMethod!(FuMethod method)
	{
		if (method.Id == FuId.ClassToString && method.CallType == FuCallType.Abstract)
			return;
		WriteNewLine();
		WriteDoc(method.Documentation);
		WriteParametersAndThrowsDoc(method);
		WriteVisibility(method.Visibility);
		if (method.Id == FuId.ClassToString)
			Write("override ");
		else
			WriteCallType(method.CallType, "sealed override ");
		WriteTypeAndName(method);
		WriteParameters(method, method.Id != FuId.Main);
		WriteBody(method);
	}

	protected override void WriteClass!(FuClass klass, FuProgram program)
	{
		WriteNewLine();
		WriteDoc(klass.Documentation);
		WritePublic(klass);
		WriteCallType(klass.CallType, "sealed ");
		OpenClass(klass, "", " : ");

		if (NeedsConstructor(klass)) {
			if (klass.Constructor != null) {
				WriteDoc(klass.Constructor.Documentation);
				WriteVisibility(klass.Constructor.Visibility);
			}
			else
				Write("internal ");
			Write(klass.Name);
			WriteLine("()");
			OpenBlock();
			WriteConstructorBody(klass);
			CloseBlock();
		}
		else if (klass.Id == FuId.ExceptionClass) {
			WriteExceptionConstructor(klass, "() { }");
			WriteExceptionConstructor(klass, "(String message) : base(message) { }");
			WriteExceptionConstructor(klass, "(String message, Exception innerException) : base(message, innerException) { }");
		}

		WriteMembers(klass, true);

		CloseBlock();
	}

	void WriteLib!()
	{
		if (this.NewArray) {
			Include("System");
			WriteNewLine();
			WriteLine("static class Fu");
			OpenBlock();
			WriteLine("public static T[] NewArray<T>(int size, Func<T> f)");
			OpenBlock();
			WriteLine("T[] a = new T[size];");
			WriteLine("for (int i = 0; i < size; i++)");
			WriteLine("\ta[i] = f();");
			WriteLine("return a;");
			CloseBlock();
			CloseBlock();
		}
	}

	void WriteResources!(SortedDictionary<string(), List<byte>()> resources)
	{
		WriteNewLine();
		WriteLine("static class FuResource");
		OpenBlock();
		foreach ((string name, List<byte> content) in resources) {
			Write("internal static readonly byte[] ");
			WriteResourceName(name);
			WriteLine(" = {");
			WriteChar('\t');
			WriteBytes(content);
			WriteLine(" };");
		}
		CloseBlock();
	}

	public override void WriteProgram!(FuProgram program, string outputFile, string namespace)
	{
		this.NewArray = false;
		OpenStringWriter();
		if (namespace.Length != 0) {
			Write("namespace ");
			WriteLine(namespace);
			OpenBlock();
		}
		WriteTopLevelNatives(program);
		WriteTypes(program);
		WriteLib();
		if (program.Resources.Count > 0)
			WriteResources(program.Resources);
		if (namespace.Length != 0)
			CloseBlock();

		CreateFile(null, outputFile);
		WriteLine("#nullable enable");
		WriteIncludes("using ", ";");
		CloseStringWriter();
		CloseFile();
	}
}