// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

#nullable disable

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Xml;

namespace RazorSyntaxGenerator;

internal class SourceWriter : AbstractFileWriter
{
    private SourceWriter(TextWriter writer, Tree tree)
        : base(writer, tree)
    {
    }

    public static void WriteMain(TextWriter writer, Tree tree) => new SourceWriter(writer, tree).WriteMain();

    public static void WriteInternal(TextWriter writer, Tree tree) => new SourceWriter(writer, tree).WriteInternal();

    public static void WriteSyntax(TextWriter writer, Tree tree) => new SourceWriter(writer, tree).WriteSyntax();

    private void WriteFileHeader()
    {
        WriteLine("// <auto-generated />");
        WriteLine();
        WriteLine("using System;");
        WriteLine("using System.Collections.Generic;");
        WriteLine("using Microsoft.AspNetCore.Razor.Language.Legacy;");
        WriteLine();
    }

    private void WriteInternal()
    {
        WriteFileHeader();

        WriteLine("namespace Microsoft.AspNetCore.Razor.Language.Syntax.InternalSyntax;");

        WriteGreenTypes();
        WriteGreenVisitors();
        WriteGreenRewriter();
        WriteStaticGreenFactories();
    }

    private void WriteSyntax()
    {
        WriteFileHeader();

        WriteLine("namespace Microsoft.AspNetCore.Razor.Language.Syntax;");

        WriteRedTypes();
    }

    private void WriteMain()
    {
        WriteFileHeader();

        WriteLine("namespace Microsoft.AspNetCore.Razor.Language.Syntax;");

        WriteRedVisitors();
        WriteRedRewriter();
        WriteRedFactories();
    }

    private void WriteGreenTypes()
    {
        var nodes = Tree.Types.Where(n => n is not PredefinedNode).ToList();

        foreach (var node in nodes)
        {
            WriteLine();
            WriteGreenType(node);
        }
    }

    private void WriteGreenType(TreeType greenType)
    {
        WriteComment(greenType.TypeComment);

        switch (greenType)
        {
            case AbstractNode abstractNode:
                {
                    WriteLine($"internal abstract partial class {abstractNode.Name} : {(abstractNode.Base == "SyntaxNode" ? "GreenNode" : abstractNode.Base)}");
                    using (Block())
                    {
                        // ctor with diagnostics and annotations
                        WriteLine($"internal {abstractNode.Name}(SyntaxKind kind, RazorDiagnostic[] diagnostics, SyntaxAnnotation[] annotations)");
                        WriteIndentedLine(": base(kind, diagnostics, annotations)");
                        using (Block())
                        {
                            if (abstractNode.Name == "DirectiveTriviaSyntax")
                            {
                                WriteLine("_flags |= NodeFlags.ContainsDirectives;");
                            }
                        }

                        WriteLine();

                        // ctor without diagnostics and annotations
                        WriteLine($"internal {abstractNode.Name}(SyntaxKind kind)");
                        WriteIndentedLine(": base(kind)");
                        using (Block())
                        {
                            if (abstractNode.Name == "DirectiveTriviaSyntax")
                            {
                                WriteLine("_flags |= NodeFlags.ContainsDirectives;");
                            }
                        }

                        /* Remove
                        // object reader constructor
                        WriteLine();
                        WriteLine("    protected {0}(ObjectReader reader)", node.Name);
                        WriteLine("       : base(reader)");
                        WriteLine("    {");
                        if (node.Name == "DirectiveTriviaSyntax")
                        {
                            WriteLine("      _flags |= NodeFlags.ContainsDirectives;");
                        }
                        WriteLine("    }"); */

                        var valueFields = abstractNode.Fields.Where(n => !IsNodeOrNodeList(n.Type)).ToList();
                        var nodeFields = abstractNode.Fields.Where(n => IsNodeOrNodeList(n.Type)).ToList();

                        for (int i = 0, n = nodeFields.Count; i < n; i++)
                        {
                            var field = nodeFields[i];
                            if (IsNodeOrNodeList(field.Type))
                            {
                                WriteLine();
                                WriteComment(field.PropertyComment);

                                if (IsSeparatedNodeList(field.Type) ||
                                    IsNodeList(field.Type))
                                {
                                    WriteLine($"public abstract {(IsNew(field) ? "new " : "")}{field.Type} {field.Name} {{ get; }}");
                                }
                                else
                                {
                                    WriteLine($"public abstract {(IsNew(field) ? "new " : "")}{field.Type} {field.Name} {{ get; }}");
                                }
                            }
                        }

                        for (int i = 0, n = valueFields.Count; i < n; i++)
                        {
                            var field = valueFields[i];
                            WriteLine();
                            WriteComment(field.PropertyComment);

                            WriteLine($"public abstract {(IsNew(field) ? "new " : "")}{field.Type} {field.Name} {{ get; }}");
                        }
                    }

                    break;
                }

            case Node node:
                {
                    WriteLine($"internal sealed partial class {node.Name} : {node.Base}");
                    using (Block())
                    {
                        var valueFields = node.Fields.Where(n => !IsNodeOrNodeList(n.Type)).ToList();
                        var nodeFields = node.Fields.Where(n => IsNodeOrNodeList(n.Type)).ToList();

                        for (int i = 0, n = nodeFields.Count; i < n; i++)
                        {
                            var field = nodeFields[i];
                            var type = GetFieldType(field, green: true);
                            WriteLine($"internal readonly {type} {GetFieldName(field)};");
                        }

                        for (int i = 0, n = valueFields.Count; i < n; i++)
                        {
                            var field = valueFields[i];
                            WriteLine($"internal readonly {field.Type} {GetFieldName(field)};");
                        }

                        // write constructor with diagnostics and annotations
                        WriteLine();
                        Write($"internal {node.Name}(SyntaxKind kind");

                        WriteGreenNodeConstructorArgs(nodeFields, valueFields);

                        WriteLine(", RazorDiagnostic[] diagnostics, SyntaxAnnotation[] annotations)");
                        WriteIndentedLine(": base(kind, diagnostics, annotations)");
                        using (Block())
                        {
                            WriteCtorBody(valueFields, nodeFields);
                        }

                        /* Remove
                        // write constructor with async
                        WriteLine();
                        Write("    internal {0}(SyntaxKind kind", node.Name);

                        WriteGreenNodeConstructorArgs(nodeFields, valueFields);

                        WriteLine(", SyntaxFactoryContext context)");
                        WriteLine("        : base(kind)");
                        WriteLine("    {");
                        WriteLine("        this.SetFactoryContext(context);");
                        WriteCtorBody(valueFields, nodeFields);
                        WriteLine("    }");
                        WriteLine(); */

                        // write constructor without diagnostics and annotations
                        WriteLine();
                        Write($"internal {node.Name}(SyntaxKind kind");

                        WriteGreenNodeConstructorArgs(nodeFields, valueFields);

                        WriteLine(")");
                        WriteIndentedLine(": base(kind)");
                        using (Block())
                        {
                            WriteCtorBody(valueFields, nodeFields);
                        }

                        WriteLine();

                        // property accessors
                        foreach (var field in nodeFields)
                        {
                            var type = field.Type;

                            WriteComment(field.PropertyComment);

                            if (IsNodeList(type))
                            {
                                WriteLine($"public {OverrideOrNewModifier(field)}{type} {field.Name} => new {type}({GetFieldName(field)});");
                            }
                            else if (IsSeparatedNodeList(type))
                            {
                                WriteLine($"public {OverrideOrNewModifier(field)}{type} {field.Name} => new {type}(new SyntaxList<GreenNode>({GetFieldName(field)}));");
                            }
                            else if (type == "SyntaxNodeOrTokenList")
                            {
                                WriteLine($"public {OverrideOrNewModifier(field)}SyntaxList<GreenNode> {field.Name} => new SyntaxList<GreenNode>({GetFieldName(field)});");
                            }
                            else
                            {
                                WriteLine($"public {OverrideOrNewModifier(field)}{type} {field.Name} => {GetFieldName(field)};");
                            }
                        }

                        foreach (var field in valueFields)
                        {
                            WriteComment(field.PropertyComment);
                            WriteLine($"public {OverrideOrNewModifier(field)}{field.Type} {field.Name} => {GetFieldName(field)};");
                        }

                        // GetSlot
                        WriteLine();
                        Write("internal override GreenNode GetSlot(int index)");

                        if (nodeFields.Count == 0)
                        {
                            WriteLine(" => null;");
                        }
                        else if (nodeFields.Count == 1)
                        {
                            WriteLine();
                            WriteIndentedLine($"=> index == 0 ? this.{GetFieldName(nodeFields[0])} : null;");
                        }
                        else
                        {
                            WriteLine();
                            using (Indent())
                            {
                                WriteLine("=> index switch");
                                using (Block(addSemicolon: true))
                                {
                                    for (int i = 0, n = nodeFields.Count; i < n; i++)
                                    {
                                        var field = nodeFields[i];
                                        WriteLine($"{i} => {GetFieldName(field)},");
                                    }

                                    WriteLine("_ => null");
                                }
                            }
                        }

                        WriteLine();
                        WriteLine($"internal override SyntaxNode CreateRed(SyntaxNode parent, int position) => new Syntax.{node.Name}(this, parent, position);");

                        WriteGreenAcceptMethods(node);
                        WriteGreenUpdateMethod(node);
                        WriteSetDiagnostics(node);
                        WriteSetAnnotations(node);
                    }

                    break;
                }
        }
    }

    private void WriteGreenNodeConstructorArgs(List<Field> nodeFields, List<Field> valueFields)
    {
        foreach (var field in nodeFields)
        {
            var type = GetFieldType(field, green: true);

            Write($", {type} {GetParameterName(field)}");
        }

        foreach (var field in valueFields)
        {
            Write($", {field.Type} {GetParameterName(field)}");
        }
    }

    private void WriteCtorBody(List<Field> valueFields, List<Field> nodeFields)
    {
        // constructor body
        WriteLine("SlotCount = {0};", nodeFields.Count);

        foreach (var field in nodeFields)
        {
            if (IsAnyList(field.Type) || IsOptional(field))
            {
                WriteLine($"if ({GetParameterName(field)} != null)");

                using (Block())
                {
                    WriteLine($"AdjustFlagsAndWidth({GetParameterName(field)});");
                    WriteLine($"{GetFieldName(field)} = {GetParameterName(field)};");
                }
            }
            else
            {
                WriteLine($"AdjustFlagsAndWidth({GetParameterName(field)});");
                WriteLine($"{GetFieldName(field)} = {GetParameterName(field)};");
            }
        }

        foreach (var field in valueFields)
        {
            WriteLine($"{GetFieldName(field)} = {GetParameterName(field)};");
        }
    }

    private void WriteSetAnnotations(Node node)
    {
        WriteLine();
        WriteLine("internal override GreenNode SetAnnotations(SyntaxAnnotation[] annotations)");
        using (Indent())
        {
            Write($"=> new {node.Name}(");
            WriteCommaSeparatedList(
                "Kind",
                node.Fields.Select(GetFieldName),
                "GetDiagnostics()",
                "annotations");
            WriteLine(");");
        }
    }

    private void WriteSetDiagnostics(Node node)
    {
        WriteLine();
        WriteLine("internal override GreenNode SetDiagnostics(RazorDiagnostic[] diagnostics)");
        using (Indent())
        {
            Write($"=> new {node.Name}(");
            WriteCommaSeparatedList(
                "Kind",
                node.Fields.Select(GetFieldName),
                "diagnostics",
                "GetAnnotations()");
            WriteLine(");");
        }
    }

    private void WriteGreenAcceptMethods(Node node)
    {
        WriteLine();
        WriteLine($"public override TResult Accept<TResult>(SyntaxVisitor<TResult> visitor) => visitor.Visit{StripPost(node.Name, "Syntax")}(this);");
        WriteLine($"public override void Accept(SyntaxVisitor visitor) => visitor.Visit{StripPost(node.Name, "Syntax")}(this);");
    }

    private void WriteGreenVisitors()
    {
        WriteGreenVisitor(withResult: true);
        WriteGreenVisitor(withResult: false);
    }

    private void WriteGreenVisitor(bool withResult)
    {
        var nodes = Tree.Types.Where(n => n is not PredefinedNode).ToList();

        WriteLine();
        WriteLine("internal partial class SyntaxVisitor" + (withResult ? "<TResult>" : ""));
        using (Block())
        {
            foreach (var node in nodes.OfType<Node>())
            {
                WriteLine($"public virtual {(withResult ? "TResult" : "void")} Visit{StripPost(node.Name, "Syntax")}({node.Name} node) => DefaultVisit(node);");
            }
        }
    }

    private void WriteGreenUpdateMethod(Node node)
    {
        WriteLine();
        Write($"public {node.Name} Update(");

        // parameters
        WriteCommaSeparatedList(node.Fields.Select(static f =>
        {
            var type =
                f.Type == "SyntaxNodeOrTokenList" ? "InternalSyntax.SyntaxList<GreenNode>" :
                f.Type == "SyntaxTokenList" ? "InternalSyntax.SyntaxList<SyntaxToken>" :
                IsNodeList(f.Type) ? "InternalSyntax." + f.Type :
                IsSeparatedNodeList(f.Type) ? "InternalSyntax." + f.Type :
                f.Type;

            return $"{type} {GetParameterName(f)}";
        }));
        WriteLine(")");

        using (Block())
        {
            Write("if (");
            int nCompared = 0;
            foreach (var field in node.Fields)
            {
                if (IsDerivedOrListOfDerived("SyntaxNode", field.Type) || IsDerivedOrListOfDerived("SyntaxToken", field.Type) || field.Type == "SyntaxNodeOrTokenList")
                {
                    if (nCompared > 0)
                    {
                        Write(" || ");
                    }

                    Write($"{GetParameterName(field)} != {field.Name}");
                    nCompared++;
                }
            }
            if (nCompared > 0)
            {
                WriteLine(")");
                using (Block())
                {
                    Write($"var newNode = SyntaxFactory.{StripPost(node.Name, "Syntax")}(");
                    WriteCommaSeparatedList(
                        node.Kinds.Count > 1 ? "Kind" : "",
                        node.Fields.Select(GetParameterName));

                    WriteLine(");");
                    WriteLine("var diags = GetDiagnostics();");
                    WriteLine("if (diags != null && diags.Length > 0)");
                    WriteIndentedLine("newNode = newNode.WithDiagnosticsGreen(diags);");
                    WriteLine("var annotations = GetAnnotations();");
                    WriteLine("if (annotations != null && annotations.Length > 0)");
                    WriteIndentedLine("newNode = newNode.WithAnnotationsGreen(annotations);");
                    WriteLine("return newNode;");
                }
            }

            WriteLine();
            WriteLine("return this;");
        }
    }

    private void WriteGreenRewriter()
    {
        var nodes = Tree.Types.Where(n => n is not PredefinedNode).ToList();

        WriteLine();
        WriteLine("internal partial class SyntaxRewriter : SyntaxVisitor<GreenNode>");
        using (Block())
        {
            var nWritten = 0;
            foreach (var node in nodes.OfType<Node>())
            {
                var nodeFields = node.Fields.Where(nd => IsNodeOrNodeList(nd.Type)).ToList();

                if (nWritten > 0)
                {
                    WriteLine();
                }

                nWritten++;
                WriteLine($"public override GreenNode Visit{StripPost(node.Name, "Syntax")}({node.Name} node)");

                using (Indent())
                {
                    if (nodeFields.Count == 0)
                    {
                        WriteLine("=> node;");
                    }
                    else
                    {
                        Write("=> node.Update(");
                        WriteCommaSeparatedList(node.Fields.Select(f =>
                        {
                            if (IsAnyList(f.Type))
                            {
                                return $"VisitList(node.{f.Name})";
                            }
                            else if (IsNode(f.Type))
                            {
                                return $"({f.Type})Visit(node.{f.Name})";
                            }
                            else
                            {
                                return $"node.{f.Name}";
                            }
                        }));
                        WriteLine(");");
                    }
                }
            }
        }
    }

    private void WriteStaticGreenFactories()
    {
        var nodes = Tree.Types.Where(n => n is not (PredefinedNode or AbstractNode)).ToList();
        WriteLine();
        WriteLine("internal static partial class SyntaxFactory");
        using (Block())
        {
            WriteGreenFactories(nodes);
            WriteGreenTypeList();
        }
    }

    private void WriteGreenFactories(List<TreeType> nodes, bool withSyntaxFactoryContext = false)
    {
        for (int i = 0, n = nodes.Count; i < n; i++)
        {
            var node = nodes[i];

            WriteGreenFactory((Node)node, withSyntaxFactoryContext);

            if (i < n - 1)
            {
                WriteLine();
            }
        }
    }

    private void WriteGreenTypeList()
    {
        WriteLine();
        WriteLine("internal static IEnumerable<Type> GetNodeTypes()");
        using (Block())
        {
            WriteLine("return new Type[]");

            using (Block(addSemicolon: true))
            {
                var nodes = Tree.Types.Where(n => n is not PredefinedNode and not AbstractNode).ToList();
                for (int i = 0, n = nodes.Count; i < n; i++)
                {
                    var node = nodes[i];
                    Write($"typeof({node.Name})");
                    if (i < n - 1)
                    {
                        Write(",");
                    }

                    WriteLine();
                }
            }
        }
    }

    private void WriteGreenFactory(Node node, bool withSyntaxFactoryContext = false)
    {
        var valueFields = node.Fields.Where(n => !IsNodeOrNodeList(n.Type)).ToList();
        var nodeFields = node.Fields.Where(n => IsNodeOrNodeList(n.Type)).ToList();

        Write($"public {(withSyntaxFactoryContext ? "" : "static ")}{node.Name} {StripPost(node.Name, "Syntax")}(");
        WriteGreenFactoryParameters(node);
        WriteLine(")");
        using (Block())
        {
            // validate kind
            if (node.Kinds.Count > 1)
            {
                WriteLine("switch (kind)");
                using (Block())
                {
                    var kinds = node.Kinds.Distinct().ToList();
                    foreach (var kind in node.Kinds)
                    {
                        WriteLine($"case SyntaxKind.{kind.Name}:{(kind == kinds[^1] ? " break;" : string.Empty)}");
                    }

                    WriteLine("default: throw new ArgumentException(\"kind\");");
                }
            }

            // validate parameters
            //WriteLine("#if DEBUG");
            foreach (var field in nodeFields)
            {
                var pname = GetParameterName(field);

                if (!IsAnyList(field.Type) && !IsOptional(field))
                {
                    WriteLine($"ArgHelper.ThrowIfNull({pname});");
                }

                if (field.Type == "SyntaxToken" && field.Kinds?.Count > 0)
                {
                    if (field.Kinds.Count == 1)
                    {
                        if (IsOptional(field))
                        {
                            WriteLine($"if ({pname} is not null && {pname}.Kind is not (SyntaxKind.{field.Kinds[0].Name} or SyntaxKind.None))");
                        }
                        else
                        {
                            WriteLine($"if ({pname}.Kind != SyntaxKind.{field.Kinds[0].Name})");
                        }

                        WriteIndentedLine($"ThrowHelper.ThrowArgumentException(nameof({pname}), " +
                            $"$\"Invalid SyntaxKind. Expected 'SyntaxKind.{field.Kinds[0].Name}'{(IsOptional(field) ? " or 'SyntaxKind.None'" : "")}, but it was {{{pname}.Kind}}\");");
                    }
                    else
                    {
                        if (IsOptional(field))
                        {
                            WriteLine($"if ({pname} != null)");
                            OpenBlock();
                        }

                        WriteLine($"switch ({pname}.Kind)");
                        using (Block())
                        {
                            foreach (var kind in field.Kinds)
                            {
                                WriteLine($"case SyntaxKind.{kind.Name}:");
                            }

                            //we need to check for Kind=None as well as node == null because that's what the red factory will pass
                            if (IsOptional(field))
                            {
                                WriteLine("case SyntaxKind.None:");
                            }

                            WriteIndentedLine("break;");
                            WriteLine("default:");
                            WriteIndentedLine($"throw new ArgumentException(\"{pname}\");");
                        }

                        if (IsOptional(field))
                        {
                            CloseBlock();
                        }
                    }
                }
            }

            //WriteLine("#endif");

            if (node.Name != "SkippedTokensTriviaSyntax" &&
                node.Name != "DocumentationCommentTriviaSyntax" &&
                node.Name != "IncompleteMemberSyntax" &&
                valueFields.Count + nodeFields.Count <= 3)
            {
                //int hash;
                //var cached = SyntaxNodeCache.TryGetNode((int)SyntaxKind.IdentifierName, identifier, this.context, out hash);
                //if (cached != null) return (IdentifierNameSyntax)cached;

                //var result = new IdentifierNameSyntax(SyntaxKind.IdentifierName, identifier, this.context);
                //if (hash >= 0)
                //{
                //    SyntaxNodeCache.AddNode(result, hash);
                //}

                //return result;

                WriteLine();

                /* Remove
                //int hash;
                WriteLine("      int hash;");
                //SyntaxNode cached = SyntaxNodeCache.TryGetNode(SyntaxKind.IdentifierName, identifier, this.context, out hash);
                if (withSyntaxFactoryContext)
                {
                    Write("      var cached = CSharpSyntaxNodeCache.TryGetNode((int)");
                }
                else
                {
                    Write("      var cached = SyntaxNodeCache.TryGetNode((int)");
                }

                WriteCtorArgList(nd, withSyntaxFactoryContext, valueFields, nodeFields);
                WriteLine(", out hash);");
                //    if (cached != null) return (IdentifierNameSyntax)cached;
                WriteLine("      if (cached != null) return ({0})cached;", nd.Name);
                WriteLine(); */

                //var result = new IdentifierNameSyntax(SyntaxKind.IdentifierName, identifier);
                Write($"var result = new {node.Name}(");
                WriteCtorArgList(node, withSyntaxFactoryContext, valueFields, nodeFields);
                WriteLine(");");

                /* Remove
                //if (hash >= 0)
                WriteLine("      if (hash >= 0)");
                //{
                WriteLine("      {");
                //    SyntaxNodeCache.AddNode(result, hash);
                WriteLine("          SyntaxNodeCache.AddNode(result, hash);");
                //}
                WriteLine("      }"); */
                WriteLine();

                //return result;
                WriteLine("return result;");
            }
            else
            {
                WriteLine();
                Write("return new {0}(", node.Name);
                WriteCtorArgList(node, withSyntaxFactoryContext, valueFields, nodeFields);
                WriteLine(");");
            }
        }
    }

    private void WriteGreenFactoryParameters(Node nd)
    {
        if (nd.Kinds.Count > 1)
        {
            Write("SyntaxKind kind, ");
        }
        for (int i = 0, n = nd.Fields.Count; i < n; i++)
        {
            var field = nd.Fields[i];
            if (i > 0)
            {
                Write(", ");
            }

            var type = field.Type;
            if (type == "SyntaxNodeOrTokenList")
            {
                type = "SyntaxList<GreenNode>";
            }
            else if (IsSeparatedNodeList(field.Type) ||
                     IsNodeList(field.Type))
            {
                type = "Microsoft.AspNetCore.Razor.Language.Syntax.InternalSyntax." + type;
            }
            Write($"{type} {GetParameterName(field)}");
        }
    }

    private void WriteCtorArgList(Node nd, bool withSyntaxFactoryContext, List<Field> valueFields, List<Field> nodeFields)
    {
        if (nd.Kinds.Count == 1)
        {
            Write("SyntaxKind.");
            Write(nd.Kinds[0].Name);
        }
        else
        {
            Write("kind");
        }
        for (int i = 0, n = nodeFields.Count; i < n; i++)
        {
            var field = nodeFields[i];
            Write(", ");
            if (field.Type == "SyntaxList<SyntaxToken>" || IsAnyList(field.Type))
            {
                Write($"{GetParameterName(field)}.Node");
            }
            else
            {
                Write(GetParameterName(field));
            }
        }
        // values are at end
        for (int i = 0, n = valueFields.Count; i < n; i++)
        {
            var field = valueFields[i];
            Write(", ");
            Write(GetParameterName(field));
        }
        if (withSyntaxFactoryContext)
        {
            Write(", this.context");
        }
    }

    private void WriteRedTypes()
    {
        var nodes = Tree.Types.Where(n => !(n is PredefinedNode)).ToList();
        for (int i = 0, n = nodes.Count; i < n; i++)
        {
            var node = nodes[i];
            WriteLine();
            WriteRedType(node);
        }
    }

    private List<Field> GetNodeOrNodeListFields(TreeType node)
        => node is AbstractNode an
            ? an.Fields.Where(n => IsNodeOrNodeList(n.Type)).ToList()
            : node is Node nd
                ? nd.Fields.Where(n => IsNodeOrNodeList(n.Type)).ToList()
                : new List<Field>();

    private void WriteRedType(TreeType redType)
    {
        WriteComment(redType.TypeComment);

        switch (redType)
        {
            case AbstractNode abstractNode:
                {
                    WriteLine($"internal abstract partial class {abstractNode.Name} : {abstractNode.Base}");
                    using (Block())
                    {
                        WriteLine($"internal {abstractNode.Name}(GreenNode green, SyntaxNode parent, int position)");
                        WriteIndentedLine(": base(green, parent, position)");
                        using (Block())
                        {
                        }

                        var valueFields = abstractNode.Fields.Where(n => !IsNodeOrNodeList(n.Type)).ToList();
                        var nodeFields = GetNodeOrNodeListFields(abstractNode);

                        foreach (var field in nodeFields)
                        {
                            if (IsNodeOrNodeList(field.Type))
                            {
                                //red SyntaxLists can't contain tokens, so we switch to SyntaxTokenList
                                var fieldType = GetRedFieldType(field);
                                WriteLine();
                                WriteComment(field.PropertyComment);
                                WriteLine($"public abstract {(IsNew(field) ? "new " : "")}{fieldType} {field.Name} {{ get; }}");
                                WriteLine($"public {abstractNode.Name} With{field.Name}({fieldType} {GetParameterName(field)}) => With{field.Name}Core({GetParameterName(field)});");
                                WriteLine($"internal abstract {abstractNode.Name} With{field.Name}Core({fieldType} {GetParameterName(field)});");

                                if (IsAnyList(field.Type))
                                {
                                    var argType = GetElementType(field.Type);
                                    WriteLine();
                                    WriteLine($"public {abstractNode.Name} Add{field.Name}(params {argType}[] items) => Add{field.Name}Core(items);");
                                    WriteLine($"internal abstract {abstractNode.Name} Add{field.Name}Core(params {argType}[] items);");
                                }
                                else
                                {
                                    var referencedNode = TryGetNodeForNestedList(field);
                                    if (referencedNode != null)
                                    {
                                        foreach (var referencedNodeField in referencedNode.Fields)
                                        {
                                            if (IsAnyList(referencedNodeField.Type))
                                            {
                                                var argType = GetElementType(referencedNodeField.Type);

                                                WriteLine();
                                                WriteLine($"public {abstractNode.Name} Add{StripPost(field.Name, "Opt")}{referencedNodeField.Name}(params {argType}[] items) => Add{StripPost(field.Name, "Opt")}{referencedNodeField.Name}Core(items);");
                                                WriteLine($"internal abstract {abstractNode.Name} Add{StripPost(field.Name, "Opt")}{referencedNodeField.Name}Core(params {argType}[] items);");
                                            }
                                        }
                                    }
                                }
                            }
                        }

                        foreach (var field in valueFields)
                        {
                            WriteLine();
                            WriteComment(field.PropertyComment);
                            WriteLine($"public abstract {(IsNew(field) ? "new " : "")}{field.Type} {field.Name} {{ get; }}");
                        }

                        var baseType = GetTreeType(abstractNode.Base);
                        if (baseType != null)
                        {
                            var baseNodeFields = GetNodeOrNodeListFields(baseType);
                            if (baseNodeFields.Count > 0)
                            {
                                WriteLine();
                            }

                            foreach (var baseField in baseNodeFields)
                            {
                                WriteLine($"public new {abstractNode.Name} With{baseField.Name}({GetRedFieldType(baseField)} {GetParameterName(baseField)}) " +
                                    $"=> ({abstractNode.Name})With{baseField.Name}Core({GetParameterName(baseField)});");
                            }

                            foreach (var baseField in baseNodeFields)
                            {
                                if (IsAnyList(baseField.Type))
                                {
                                    var argType = GetElementType(baseField.Type);
                                    WriteLine();
                                    WriteLine($"public new {abstractNode.Name} Add{baseField.Name}(params {argType}[] items) => ({abstractNode.Name})Add{baseField.Name}Core(items);");
                                }
                                else
                                {
                                    var referencedNode = TryGetNodeForNestedList(baseField);
                                    if (referencedNode != null)
                                    {
                                        // look for list members...
                                        foreach (var referencedNodeField in referencedNode.Fields)
                                        {
                                            if (IsAnyList(referencedNodeField.Type))
                                            {
                                                var argType = GetElementType(referencedNodeField.Type);

                                                WriteLine();
                                                WriteLine($"public new {baseType.Name} Add{StripPost(baseField.Name, "Opt")}{referencedNodeField.Name}(params {argType}[] items) => Add{StripPost(baseField.Name, "Opt")}{referencedNodeField.Name}Core(items);");
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }

                    break;
                }

            case Node node:
                {
                    WriteLine($"internal sealed partial class {node.Name} : {node.Base}");
                    using (Block())
                    {
                        var valueFields = node.Fields.Where(n => !IsNodeOrNodeList(n.Type)).ToList();
                        var nodeFields = node.Fields.Where(n => IsNodeOrNodeList(n.Type)).ToList();

                        foreach (var field in nodeFields)
                        {
                            if (field.Type != "SyntaxToken" && field.Type != "SyntaxList<SyntaxToken>")
                            {
                                if (IsSeparatedNodeList(field.Type) || field.Type == "SyntaxNodeOrTokenList")
                                {
                                    WriteLine($"private SyntaxNode {GetFieldName(field)};");
                                }
                                else
                                {
                                    var type = GetFieldType(field, green: false);
                                    WriteLine($"private {type} {GetFieldName(field)};");
                                }
                            }
                        }

                        // write constructor
                        WriteLine();
                        WriteLine($"internal {node.Name}(GreenNode green, SyntaxNode parent, int position)");
                        WriteIndentedLine(": base(green, parent, position)");
                        using (Block())
                        {
                        }
                        WriteLine();

                        // property accessors
                        for (var i = 0; i < nodeFields.Count; i++)
                        {
                            var field = nodeFields[i];

                            if (field.Type == "SyntaxToken")
                            {
                                WriteComment(field.PropertyComment);
                                Write($"public {OverrideOrNewModifier(field)}{field.Type} {field.Name}");
                                if (IsOptional(field))
                                {
                                    WriteLine();
                                    using (Block())
                                    {

                                        WriteLine("get");
                                        using (Block())
                                        {
                                            WriteLine($"var slot = ((InternalSyntax.{node.Name})Green).{field.Name};");
                                            WriteLine($"return slot != null ? new SyntaxToken(this, slot, {GetChildPosition(i)}, {GetChildIndex(i)}) : default;");
                                        }
                                    }
                                }
                                else
                                {
                                    WriteLine($" => new SyntaxToken(this, ((InternalSyntax.{node.Name})Green).{GetFieldName(field)}, {GetChildPosition(i)}, {GetChildIndex(i)});");
                                }
                            }
                            else if (field.Type == "SyntaxList<SyntaxToken>")
                            {
                                WriteComment(field.PropertyComment);
                                WriteLine($"public {OverrideOrNewModifier(field)}SyntaxTokenList {field.Name}");
                                using (Block())
                                {
                                    WriteLine(" get");
                                    using (Block())
                                    {
                                        WriteLine($"var slot = Green.GetSlot({i});");
                                        WriteLine($"return slot != null ? new SyntaxTokenList(this, slot, {GetChildPosition(i)}, {GetChildIndex(i)}) : default;");
                                    }
                                }
                            }
                            else
                            {
                                WriteComment(field.PropertyComment);
                                Write($"public {OverrideOrNewModifier(field)}{field.Type} {field.Name} ");

                                if (IsNodeList(field.Type))
                                {
                                    WriteLine($" => new {field.Type}(GetRed(ref {GetFieldName(field)}, {i}));");
                                }
                                else if (IsSeparatedNodeList(field.Type))
                                {
                                    WriteLine();
                                    using (Block())
                                    {
                                        WriteLine("get");
                                        using (Block())
                                        {
                                            WriteLine($"var red = GetRed(ref {GetFieldName(field)}, {i});");
                                            WriteLine($"return red != null ? new {field.Type}(red, {GetChildIndex(i)}) : default;");
                                        }
                                    }
                                }
                                else if (field.Type == "SyntaxNodeOrTokenList")
                                {
                                    throw new InvalidOperationException("field cannot be a random SyntaxNodeOrTokenList");
                                }
                                else
                                {
                                    if (i == 0)
                                    {
                                        WriteLine($" => GetRedAtZero(ref {GetFieldName(field)});");
                                    }
                                    else
                                    {
                                        WriteLine($" => GetRed(ref {GetFieldName(field)}, {i});");
                                    }
                                }
                            }
                        }

                        foreach (var field in valueFields)
                        {
                            WriteComment(field.PropertyComment);
                            WriteLine($"public {OverrideOrNewModifier(field)}{field.Type} {field.Name} => ((InternalSyntax.{node.Name})Green).{field.Name};");
                        }

                        {
                            WriteLine();

                            //GetNodeSlot forces creation of a red node.
                            Write("internal override SyntaxNode GetNodeSlot(int index)");

                            var relevantNodes = nodeFields
                                .Select((field, index) => (field, index))
                                .Where(t => t.field.Type is not "SyntaxToken" and not "SyntaxList<SyntaxToken>")
                                .ToList();

                            if (relevantNodes.Count == 0)
                            {
                                WriteLine(" => null;");
                            }
                            else if (relevantNodes.Count == 1)
                            {
                                var (field, index) = relevantNodes[0];
                                var whenTrue = index == 0
                                    ? $"GetRedAtZero(ref this.{GetFieldName(field)})"
                                    : $"GetRed(ref this.{GetFieldName(field)}, {index})";

                                WriteLine($" => index == {index} ? {whenTrue} : null;");
                            }
                            else
                            {
                                WriteLine();
                                using (Indent())
                                {
                                    WriteLine("=> index switch");
                                    using (Block(addSemicolon: true))
                                    {
                                        foreach (var (field, index) in relevantNodes)
                                        {
                                            if (index == 0)
                                            {
                                                WriteLine($"{index} => GetRedAtZero(ref {GetFieldName(field)}),");
                                            }
                                            else
                                            {
                                                WriteLine($"{index} => GetRed(ref {GetFieldName(field)}, {index}),");
                                            }
                                        }

                                        WriteLine("_ => null");
                                    }
                                }
                            }
                        }

                        WriteLine();

                        {
                            //GetCachedSlot returns a red node if we have it.
                            Write("internal override SyntaxNode GetCachedSlot(int index)");

                            var relevantNodes = nodeFields
                                .Select((field, index) => (field, index))
                                .Where(t => t.field.Type is not "SyntaxToken" and not "SyntaxList<SyntaxToken>")
                                .ToList();

                            if (relevantNodes.Count == 0)
                            {
                                WriteLine(" => null;");
                            }
                            else if (relevantNodes.Count == 1)
                            {
                                var (field, index) = relevantNodes[0];
                                WriteLine($" => index == {index} ? this.{GetFieldName(field)} : null;");
                            }
                            else
                            {
                                WriteLine();
                                using (Indent())
                                {
                                    WriteLine("=> index switch");
                                    using (Block(addSemicolon: true))
                                    {
                                        foreach (var (field, index) in relevantNodes)
                                        {
                                            WriteLine($"{index} => this.{GetFieldName(field)},");
                                        }

                                        WriteLine("_ => null");
                                    }
                                }
                            }
                        }


                        WriteRedAcceptMethods(node);
                        WriteRedUpdateMethod(node);
                        WriteRedWithMethods(node);
                        WriteRedListHelperMethods(node);
                    }

                    break;
                }
        }
    }

    private static string GetRedFieldType(Field field)
    {
        return field.Type == "SyntaxList<SyntaxToken>"
            ? "SyntaxTokenList"
            : field.Type;
    }

    private static string GetChildPosition(int i)
    {
        return i == 0 ? "Position" : $"GetChildPosition({i})";
    }

    private static string GetChildIndex(int i)
    {
        return i == 0 ? "0" : $"GetChildIndex({i})";
    }

    private void WriteRedAcceptMethods(Node node)
    {
        WriteLine();
        WriteLine($"public override TResult Accept<TResult>(SyntaxVisitor<TResult> visitor) => visitor.Visit{StripPost(node.Name, "Syntax")}(this);");
        WriteLine($"public override void Accept(SyntaxVisitor visitor) => visitor.Visit{StripPost(node.Name, "Syntax")}(this);");
    }

    private void WriteRedVisitors()
    {
        WriteRedVisitor(genericResult: true);
        WriteRedVisitor(genericResult: false);
    }

    private void WriteRedVisitor(bool genericResult)
    {
        var (result, typeParams) = genericResult
            ? ("TResult", "<TResult>")
            : ("void", string.Empty);

        var nodes = Tree.Types.Where(n => n is not PredefinedNode).ToList();

        WriteLine();
        WriteLine($"internal partial class SyntaxVisitor{typeParams}");
        using (Block())
        {
            var nWritten = 0;
            foreach (var node in nodes.OfType<Node>())
            {
                if (nWritten > 0)
                {
                    WriteLine();
                }

                nWritten++;
                WriteComment($"<summary>Called when the visitor visits a {node.Name} node.</summary>");
                WriteLine($"public virtual {result} Visit{StripPost(node.Name, "Syntax")}({node.Name} node) => DefaultVisit(node);");
            }
        }
    }

    private void WriteRedUpdateMethod(Node node)
    {
        WriteLine();
        Write($"public {node.Name} Update(");
        WriteCommaSeparatedList(
            node.Fields.Select(f => $"{GetRedPropertyType(f)} {GetParameterName(f)}"));
        WriteLine(")");

        using (Block())
        {
            Write("if (");
            var nCompared = 0;
            foreach (var field in node.Fields)
            {
                if (IsDerivedOrListOfDerived("SyntaxNode", field.Type) || IsDerivedOrListOfDerived("SyntaxToken", field.Type) || field.Type is "SyntaxNodeOrTokenList" or "ISpanChunkGenerator")
                {
                    if (nCompared > 0)
                    {
                        Write(" || ");
                    }

                    Write($"{GetParameterName(field)} != {field.Name}");
                    nCompared++;
                }
            }

            if (nCompared > 0)
            {
                WriteLine(")");
                using (Block())
                {
                    Write($"var newNode = SyntaxFactory.{StripPost(node.Name, "Syntax")}(");
                    WriteCommaSeparatedList(
                        node.Kinds.Count > 1 ? "Kind" : string.Empty,
                        node.Fields.Select(f => GetParameterName(f)));
                    WriteLine(");");
                    WriteLine("var diagnostics = GetDiagnostics();");
                    WriteLine("if (diagnostics != null && diagnostics.Length > 0)");
                    WriteIndentedLine("newNode = newNode.WithDiagnostics(diagnostics);");
                    WriteLine("var annotations = GetAnnotations();");
                    WriteLine("return annotations?.Length > 0 ? newNode.WithAnnotations(annotations) : newNode;");
                }
            }

            WriteLine();
            WriteLine("return this;");
        }
    }

    private void WriteRedWithMethods(Node node)
    {
        foreach (var field in node.Fields)
        {
            var type = GetRedPropertyType(field);

            if (field == node.Fields.First())
            {
                WriteLine();
            }

            var isNew = false;
            if (IsOverride(field))
            {
                var baseType = GetHighestBaseTypeWithField(node, field.Name);
                if (baseType != null)
                {
                    WriteLine($"internal override {baseType.Name} With{field.Name}Core({type} {GetParameterName(field)}) => With{field.Name}({GetParameterName(field)});");
                    isNew = true;
                }
            }

            Write($"public{(isNew ? " new " : " ")}{node.Name} With{StripPost(field.Name, "Opt")}({type} {GetParameterName(field)}) => Update(");

            // call update inside each setter
            WriteCommaSeparatedList(node.Fields.Select(f =>
                f == field ? GetParameterName(f) : f.Name));
            WriteLine(");");
        }
    }

    private TreeType GetHighestBaseTypeWithField(TreeType node, string name)
    {
        TreeType bestType = null;
        for (var current = node; current != null; current = TryGetBaseType(current))
        {
            var fields = GetNodeOrNodeListFields(current);
            var field = fields.FirstOrDefault(f => f.Name == name);
            if (field != null)
            {
                bestType = current;
            }
        }

        return bestType;
    }

    private TreeType TryGetBaseType(TreeType node)
        => node is AbstractNode an
            ? GetTreeType(an.Base)
            : node is Node n
                ? GetTreeType(n.Base)
                : null;

    private void WriteRedListHelperMethods(Node node)
    {
        foreach (var field in node.Fields)
        {
            if (IsAnyList(field.Type))
            {
                // write list helper methods for list properties
                WriteRedListHelperMethods(node, field);
            }
            else
            {
                var referencedNode = TryGetNodeForNestedList(field);
                if (referencedNode != null)
                {
                    // look for list members...
                    foreach (var referencedNodeField in referencedNode.Fields)
                    {
                        if (IsAnyList(referencedNodeField.Type))
                        {
                            WriteRedNestedListHelperMethods(node, field, referencedNode, referencedNodeField);
                        }
                    }
                }
            }
        }
    }

    private Node TryGetNodeForNestedList(Field field)
    {
        var referencedNode = GetNode(field.Type);
        if (referencedNode != null && (!IsOptional(field) || RequiredFactoryArgumentCount(referencedNode) == 0))
        {
            return referencedNode;
        }

        return null;
    }

    private void WriteRedListHelperMethods(Node node, Field field)
    {
        var argType = GetElementType(field.Type);

        var isNew = false;
        if (IsOverride(field))
        {
            var baseType = GetHighestBaseTypeWithField(node, field.Name);
            if (baseType != null)
            {
                WriteLine($"internal override {baseType.Name} Add{field.Name}Core(params {argType}[] items) => Add{field.Name}(items);");
                isNew = true;
            }
        }

        WriteLine();
        WriteLine($"public{(isNew ? " new " : " ")}{node.Name} Add{field.Name}(params {argType}[] items) => With{StripPost(field.Name, "Opt")}(this.{field.Name}.AddRange(items));");
    }

    private void WriteRedNestedListHelperMethods(Node node, Field field, Node referencedNode, Field referencedNodeField)
    {
        var argType = GetElementType(referencedNodeField.Type);

        var isNew = false;
        if (IsOverride(field))
        {
            var baseType = GetHighestBaseTypeWithField(node, field.Name);
            if (baseType != null)
            {
                WriteLine($"internal override {baseType.Name} Add{StripPost(field.Name, "Opt")}{referencedNodeField.Name}Core(params {argType}[] items) => Add{StripPost(field.Name, "Opt")}{referencedNodeField.Name}(items);");
                isNew = true;
            }
        }

        // AddBaseListTypes
        WriteLine();
        Write($"public{(isNew ? " new " : " ")}{node.Name} Add{StripPost(field.Name, "Opt")}{referencedNodeField.Name}(params {argType}[] items)");

        if (IsOptional(field))
        {
            WriteLine();
            using (Block())
            {
                var factoryName = StripPost(referencedNode.Name, "Syntax");
                var varName = StripPost(GetFieldName(field), "Opt");
                WriteLine($"var {varName} = this.{field.Name} ?? SyntaxFactory.{factoryName}();");
                WriteLine($"return this.With{StripPost(field.Name, "Opt")}({varName}.With{StripPost(referencedNodeField.Name, "Opt")}({varName}.{referencedNodeField.Name}.AddRange(items)));");
            }
        }
        else
        {
            WriteLine($" => With{StripPost(field.Name, "Opt")}(this.{field.Name}.With{StripPost(referencedNodeField.Name, "Opt")}(this.{field.Name}.{referencedNodeField.Name}.AddRange(items)));");
        }
    }

    private void WriteRedRewriter()
    {
        var nodes = Tree.Types.Where(n => n is not PredefinedNode).ToList();

        WriteLine();
        WriteLine("internal partial class SyntaxRewriter : SyntaxVisitor<SyntaxNode>");
        using (Block())
        {
            var nWritten = 0;
            foreach (var node in nodes.OfType<Node>())
            {
                var nodeFields = node.Fields.Where(nd => IsNodeOrNodeList(nd.Type)).ToList();

                if (nWritten > 0)
                {
                    WriteLine();
                }

                nWritten++;
                WriteLine($"public override SyntaxNode Visit{StripPost(node.Name, "Syntax")}({node.Name} node)");

                if (node.Fields.Count == 0)
                {
                    WriteIndentedLine("=> node;");
                }
                else
                {
                    using (Indent())
                    {
                        Write("=> node.Update(");
                        WriteCommaSeparatedList(node.Fields.Select(f =>
                        {
                            if (IsNodeOrNodeList(f.Type))
                            {
                                if (IsAnyList(f.Type))
                                {
                                    return $"VisitList(node.{f.Name})";
                                }
                                else if (f.Type == "SyntaxToken")
                                {
                                    return $"({f.Type})VisitToken(node.{f.Name})";
                                }
                                else
                                {
                                    return $"({f.Type})Visit(node.{f.Name})";
                                }
                            }

                            return $"node.{f.Name}";
                        }));

                        WriteLine(");");
                    }
                }
            }
        }
    }

    private void WriteRedFactories()
    {
        var nodes = Tree.Types.Where(n => n is not PredefinedNode and not AbstractNode).OfType<Node>().ToList();
        WriteLine();
        WriteLine("internal static partial class SyntaxFactory");
        using (Block())
        {
            foreach (var node in nodes)
            {
                WriteRedFactory(node);
                WriteRedFactoryWithNoAutoCreatableTokens(node);
                WriteRedMinimalFactory(node);
                WriteRedMinimalFactory(node, withStringNames: true);
                WriteKindConverters(node);
            }
        }
    }

    protected bool CanBeAutoCreated(Node node, Field field)
    {
        return IsAutoCreatableToken(node, field) || IsAutoCreatableNode(node, field);
    }

    private static bool IsAutoCreatableToken(Node node, Field field)
    {
        return field.Type == "SyntaxToken"
            && field.Kinds != null
            && ((field.Kinds.Count == 1 && field.Kinds[0].Name != "IdentifierToken" && !field.Kinds[0].Name.EndsWith("LiteralToken", StringComparison.Ordinal)) || (field.Kinds.Count > 1 && field.Kinds.Count == node.Kinds.Count));
    }

    private bool IsAutoCreatableNode(Node node, Field field)
    {
        var referencedNode = GetNode(field.Type);
        return referencedNode != null && RequiredFactoryArgumentCount(referencedNode) == 0;
    }

    private bool IsRequiredFactoryField(Node node, Field field)
    {
        return (!IsOptional(field) && !IsAnyList(field.Type) && !CanBeAutoCreated(node, field)) || IsValueField(field);
    }

    private bool IsValueField(Field field)
    {
        return !IsNodeOrNodeList(field.Type);
    }

    private int RequiredFactoryArgumentCount(Node nd, bool includeKind = true)
    {
        var count = 0;

        // kind must be specified in factory
        if (nd.Kinds.Count > 1 && includeKind)
        {
            count++;
        }

        for (int i = 0, n = nd.Fields.Count; i < n; i++)
        {
            var field = nd.Fields[i];
            if (IsRequiredFactoryField(nd, field))
            {
                count++;
            }
        }

        return count;
    }

    private int OptionalFactoryArgumentCount(Node nd)
    {
        var count = 0;
        for (int i = 0, n = nd.Fields.Count; i < n; i++)
        {
            var field = nd.Fields[i];
            if (IsOptional(field) || CanBeAutoCreated(nd, field) || IsAnyList(field.Type))
            {
                count++;
            }
        }

        return count;
    }

    // full factory signature with nothing optional
    private void WriteRedFactory(Node node)
    {
        WriteLine();

        var valueFields = node.Fields.Where(IsValueField).ToList();
        var nodeFields = node.Fields.Where(n => !IsValueField(n)).ToList();

        var hasValidation = node.Kinds.Count > 1 || nodeFields.Any(f => NeedsNullCheck(f) || NeedsSyntaxKindValidation(f));

        WriteComment($"<summary>Creates a new {node.Name} instance.</summary>");
        Write($"public static {node.Name} {StripPost(node.Name, "Syntax")}(");
        WriteCommaSeparatedList(
            node.Kinds.Count > 1 ? "SyntaxKind kind" : string.Empty,
            node.Fields.Select(f => $"{GetRedPropertyType(f)} {GetParameterName(f)}"));
        WriteLine(")");

        if (hasValidation)
        {
            // We only need to write a block if we're adding validation.
            OpenBlock();

            // validate kinds
            if (node.Kinds.Count > 1)
            {
                WriteLine("switch (kind)");
                using (Block())
                {
                    var kinds = node.Kinds.Distinct().ToList();
                    foreach (var kind in node.Kinds)
                    {
                        WriteLine($"case SyntaxKind.{kind.Name}:{(kind == kinds[^1] ? " break;" : string.Empty)}");
                    }

                    WriteLine("default: throw new ArgumentException(\"kind\");");
                }
            }

            // validate parameters
            foreach (var field in nodeFields)
            {
                var pname = GetParameterName(field);

                if (field.Type == "SyntaxToken" && field.Kinds?.Count > 0)
                {
                    var fieldKinds = GetKindsOfFieldOrNearestParent(node, field);

                    if (fieldKinds.Count > 0)
                    {
                        var kinds = fieldKinds.ToList();

                        if (IsOptional(field))
                        {
                            kinds.Add(new Kind { Name = "None" });
                        }

                        var kindText = string.Join(" or ", kinds.Select(k => $"SyntaxKind.{k.Name}"));

                        WriteLine($"if ({pname}.Kind is not ({kindText})) return ThrowHelper.ThrowArgumentException<{node.Name}>(nameof({pname}), " +
                            $"$\"Invalid SyntaxKind. Expected '{kindText}', but it was {{{pname}.Kind}}\");");
                    }
                }
                else if (!IsAnyList(field.Type) && !IsOptional(field))
                {
                    WriteLine($"ArgHelper.ThrowIfNull({pname});");
                }
            }

            Write($"return ({node.Name})InternalSyntax.SyntaxFactory.{StripPost(node.Name, "Syntax")}(");
        }
        else
        {
            // If we're not generating validation, we can use an expression body.
            IncreaseIndent();
            Write($"=> ({node.Name})InternalSyntax.SyntaxFactory.{StripPost(node.Name, "Syntax")}(");
        }

        WriteCommaSeparatedList(
            node.Kinds.Count > 1 ? "kind" : string.Empty,
            nodeFields.Select(f =>
            {
                if (f.Type == "SyntaxToken")
                {
                    return $"(Syntax.InternalSyntax.SyntaxToken){GetParameterName(f)}.Node";
                }
                else if (f.Type == "SyntaxList<SyntaxToken>")
                {
                    return $"{GetParameterName(f)}.Node.ToGreenList<InternalSyntax.SyntaxToken>()";
                }
                else if (IsNodeList(f.Type))
                {
                    return $"{GetParameterName(f)}.Node.ToGreenList<InternalSyntax.{GetElementType(f.Type)}>()";
                }
                else if (IsSeparatedNodeList(f.Type))
                {
                    return $"{GetParameterName(f)}.Node.ToGreenSeparatedList<InternalSyntax.{GetElementType(f.Type)}>()";
                }
                else if (f.Type == "SyntaxNodeOrTokenList")
                {
                    return $"{GetParameterName(f)}.Node.ToGreenList<GreenNode>()";
                }
                else
                {
                    return $"{GetParameterName(f)} == null ? null : (InternalSyntax.{f.Type}){GetParameterName(f)}.Green";
                }
            }),
            // values are at end
            valueFields.Select(f => GetParameterName(f)));

        WriteLine(").CreateRed();");

        if (hasValidation)
        {
            CloseBlock();
        }
        else
        {
            DescreaseIndent();
        }

        //WriteLine();

        static bool NeedsNullCheck(Field field)
        {
            return !IsAnyList(field.Type) && !IsOptional(field);
        }

        static bool NeedsSyntaxKindValidation(Field field)
        {
            return field.Type == "SyntaxToken" && field.Kinds?.Count > 0;
        }
    }

    private static string GetRedPropertyType(Field field)
    {
        return field.Type == "SyntaxList<SyntaxToken>"
            ? "SyntaxTokenList"
            : field.Type;
    }

    private string GetDefaultValue(Node nd, Field field)
    {
        if (IsRequiredFactoryField(nd, field))
        {
            Console.WriteLine(nd.Name);
            Console.WriteLine(field.Name);
        }
        System.Diagnostics.Debug.Assert(!IsRequiredFactoryField(nd, field));

        if (IsOptional(field) || IsAnyList(field.Type))
        {
            return $"default({GetRedPropertyType(field)})";
        }
        else if (field.Type == "SyntaxToken")
        {
            // auto construct token?
            if (field.Kinds.Count == 1)
            {
                return $"SyntaxFactory.Token(SyntaxKind.{field.Kinds[0].Name})";
            }
            else
            {
                return $"SyntaxFactory.Token(Get{StripPost(nd.Name, "Syntax")}{StripPost(field.Name, "Opt")}Kind(kind))";
            }
        }
        else
        {
            var referencedNode = GetNode(field.Type);
            return $"SyntaxFactory.{StripPost(referencedNode.Name, "Syntax")}()";
        }
    }

    // Writes Get<Property>Kind() methods for converting between node kind and member token kinds...
    private void WriteKindConverters(Node node)
    {
        foreach (var field in node.Fields)
        {
            if (field.Type == "SyntaxToken" && CanBeAutoCreated(node, field) && field.Kinds.Count > 1)
            {
                WriteLine();
                WriteLine($"private static SyntaxKind Get{StripPost(node.Name, "Syntax")}{StripPost(field.Name, "Opt")}Kind(SyntaxKind kind)");
                using (Indent())
                {
                    WriteLine("=> kind switch");
                    using (Block())
                    {
                        for (var i = 0; i < field.Kinds.Count; i++)
                        {
                            var nKind = node.Kinds[i];
                            var pKind = field.Kinds[i];
                            WriteLine($"SyntaxKind.{nKind.Name} => SyntaxKind.{pKind.Name}");
                        }

                        WriteLine($"_ => ThrowHelper.ThrowArgumentOutOfRangeException<SyntaxKind>(nameof(kind));");
                    }
                }
            }
        }
    }

    private static IEnumerable<Field> DetermineRedFactoryWithNoAutoCreatableTokenFields(Node node)
    {
        return node.Fields.Where(f => !IsAutoCreatableToken(node, f));
    }

    // creates a factory without auto-creatable token arguments
    private void WriteRedFactoryWithNoAutoCreatableTokens(Node node)
    {
        var nAutoCreatableTokens = node.Fields.Count(f => IsAutoCreatableToken(node, f));
        if (nAutoCreatableTokens == 0)
        {
            return; // already handled by general factory
        }

        var factoryWithNoAutoCreatableTokenFields = new HashSet<Field>(DetermineRedFactoryWithNoAutoCreatableTokenFields(node));
        var minimalFactoryFields = DetermineMinimalFactoryFields(node);
        if (minimalFactoryFields != null && factoryWithNoAutoCreatableTokenFields.SetEquals(minimalFactoryFields))
        {
            return; // will be handled in minimal factory case
        }

        WriteLine();

        WriteComment($"<summary>Creates a new {node.Name} instance.</summary>");
        Write($"public static {node.Name} {StripPost(node.Name, "Syntax")}(");
        WriteCommaSeparatedList(
            node.Kinds.Count > 1 ? "SyntaxKind kind" : string.Empty,
            node.Fields
                .Where(factoryWithNoAutoCreatableTokenFields.Contains)
                .Select(f => $"{GetRedPropertyType(f)} {GetParameterName(f)}"));
        WriteLine(")");

        using (Indent())
        {
            Write($"=> SyntaxFactory.{StripPost(node.Name, "Syntax")}(");
            WriteCommaSeparatedList(
                node.Kinds.Count > 1 ? "kind" : string.Empty,
                node.Fields.Select(f => factoryWithNoAutoCreatableTokenFields.Contains(f)
                    ? GetParameterName(f)
                    : GetDefaultValue(node, f)));
            WriteLine(");");
        }
    }

    private Field DetermineMinimalOptionalField(Node nd)
    {
        // first if there is a single list, then choose the list because it would not have been optional
        var listCount = nd.Fields.Count(f => IsAnyNodeList(f.Type));
        if (listCount == 1)
        {
            return nd.Fields.First(f => IsAnyNodeList(f.Type));
        }
        else
        {
            // otherwise, if there is a single optional node, use that..
            var nodeCount = nd.Fields.Count(f => IsNode(f.Type) && f.Type != "SyntaxToken");
            if (nodeCount == 1)
            {
                return nd.Fields.First(f => IsNode(f.Type) && f.Type != "SyntaxToken");
            }
            else
            {
                return null;
            }
        }
    }

    private IEnumerable<Field> DetermineMinimalFactoryFields(Node nd)
    {
        // special case to allow a single optional argument if there would have been no arguments
        // and we can determine a best single argument.
        Field allowOptionalField = null;

        var optionalCount = OptionalFactoryArgumentCount(nd);
        if (optionalCount == 0)
        {
            return null; // no fields...
        }

        var requiredCount = RequiredFactoryArgumentCount(nd, includeKind: false);
        if (requiredCount == 0 && optionalCount > 1)
        {
            allowOptionalField = DetermineMinimalOptionalField(nd);
        }

        return nd.Fields.Where(f => IsRequiredFactoryField(nd, f) || allowOptionalField == f);
    }

    // creates a factory with only the required arguments (everything else is defaulted)
    private void WriteRedMinimalFactory(Node node, bool withStringNames = false)
    {
        var optionalCount = OptionalFactoryArgumentCount(node);
        if (optionalCount == 0)
        {
            return; // already handled w/ general factory method
        }

        var minimalFactoryfields = new HashSet<Field>(DetermineMinimalFactoryFields(node));

        if (withStringNames && !minimalFactoryfields.Any(f => IsRequiredFactoryField(node, f) && CanAutoConvertFromString(f)))
        {
            return; // no string-name overload necessary
        }

        WriteLine();

        WriteComment($"<summary>Creates a new {node.Name} instance.</summary>");
        Write($"public static {node.Name} {StripPost(node.Name, "Syntax")}(");
        WriteCommaSeparatedList(
            node.Kinds.Count > 1 ? "SyntaxKind kind" : string.Empty,
            node.Fields.Where(minimalFactoryfields.Contains).Select(f =>
            {
                var type = GetRedPropertyType(f);

                if (IsRequiredFactoryField(node, f))
                {
                    if (withStringNames && CanAutoConvertFromString(f))
                    {
                        type = "string";
                    }

                    return $"{type} {GetParameterName(f)}";
                }
                else
                {
                    return $"{type} {GetParameterName(f)} = default({type})";
                }
            }));
        WriteLine(")");

        using (Indent())
        {
            Write($"=> SyntaxFactory.{StripPost(node.Name, "Syntax")}(");

            WriteCommaSeparatedList(
                node.Kinds.Count > 1 ? "kind" : string.Empty,
                node.Fields.Select(f =>
                {
                    if (minimalFactoryfields.Contains(f))
                    {
                        if (IsRequiredFactoryField(node, f))
                        {
                            if (withStringNames && CanAutoConvertFromString(f))
                            {
                                return $"{GetStringConverterMethod(f)}({GetParameterName(f)})";
                            }
                            else
                            {
                                return GetParameterName(f);
                            }
                        }
                        else
                        {
                            if (IsOptional(f) || IsAnyList(f.Type))
                            {
                                return GetParameterName(f);
                            }
                            else
                            {
                                return $"{GetParameterName(f)} ?? {GetDefaultValue(node, f)}";
                            }
                        }
                    }

                    return GetDefaultValue(node, f);
                }));
            WriteLine(");");
        }
    }

    private static bool CanAutoConvertFromString(Field field)
    {
        return IsIdentifierToken(field) || IsIdentifierNameSyntax(field);
    }

    private static bool IsIdentifierToken(Field field)
    {
        return field.Type == "SyntaxToken" && field.Kinds != null && field.Kinds.Count == 1 && field.Kinds[0].Name == "IdentifierToken";
    }

    private static bool IsIdentifierNameSyntax(Field field)
    {
        return field.Type == "IdentifierNameSyntax";
    }

    private static string GetStringConverterMethod(Field field)
    {
        if (IsIdentifierToken(field))
        {
            return "SyntaxFactory.Identifier";
        }
        else if (IsIdentifierNameSyntax(field))
        {
            return "SyntaxFactory.IdentifierName";
        }
        else
        {
            throw new NotSupportedException();
        }
    }

    /// <summary>
    /// Anything inside a &lt;Comment&gt; tag gets written out (escaping untouched) as the
    /// XML doc comment.  Line breaks will be preserved.
    /// </summary>
    private void WriteComment(string comment)
    {
        if (comment != null)
        {
            var lines = comment.Split(new string[] { "\r", "\n", "\r\n" }, StringSplitOptions.RemoveEmptyEntries);
            foreach (var line in lines.Where(l => !string.IsNullOrWhiteSpace(l)))
            {
                WriteLine("/// {0}", line.TrimStart());
            }
        }
    }

    /// <summary>
    /// Anything inside a &lt;Comment&gt; tag gets written out (escaping untouched) as the
    /// XML doc comment.  Line breaks will be preserved.
    /// </summary>
    private void WriteComment(Comment comment)
    {
        if (comment != null)
        {
            foreach (XmlElement element in comment.Body)
            {
                string[] lines = element.OuterXml.Split(new string[] { "\r", "\n", "\r\n" }, StringSplitOptions.RemoveEmptyEntries);
                foreach (string line in lines.Where(l => !string.IsNullOrWhiteSpace(l)))
                {
                    WriteLine("/// {0}", line.TrimStart());
                }
            }
        }
    }

    private static string GetFieldName(Field field)
        => UnderscoreCamelCase(field.Name);

    private static string GetParameterName(Field field)
        => CamelCase(field.Name);
}
