using CapnpC.CSharp.Generator.Model;
using System;
using System.Collections.Generic;
using System.Linq;
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
using static CapnpC.CSharp.Generator.CodeGen.SyntaxHelpers;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using System.Threading.Tasks;
using System.Threading;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis;

namespace CapnpC.CSharp.Generator.CodeGen
{
    class InterfaceSnippetGen
    {
        readonly GenNames _names;

        public InterfaceSnippetGen(GenNames names)
        {
            _names = names;
        }

        TypeSyntax TransformReturnType(Method method)
        {
            switch (method.Results.Count)
            {
                case 0:
                    return IdentifierName(nameof(Task));

                case 1 when method.Results[0].Type.Tag == TypeTag.Struct:
                    return GenericName(nameof(Task)).AddTypeArgumentListArguments(
                        _names.MakeTypeSyntax(method.Results[0].Type, method.DeclaringInterface, TypeUsage.DomainClass, Nullability.NonNullable));

                case 1:
                    return GenericName(nameof(Task)).AddTypeArgumentListArguments(
                        _names.MakeTypeSyntax(method.Results[0].Type, method.DeclaringInterface, TypeUsage.DomainClass, Nullability.NullableRef));

                default:
                    return GenericName(nameof(Task)).AddTypeArgumentListArguments(
                        TupleType(SeparatedList(
                            method.Results.Select(
                                f => TupleElement(_names.MakeTypeSyntax(f.Type, method.DeclaringInterface, TypeUsage.DomainClass, Nullability.NullableRef))))));
            }
        }

        ParameterSyntax[] TransformParameters(Method method)
        {
            var list = new List<ParameterSyntax>();

            if (method.Params.Count > 0)
            {
                var arg0 = method.Params[0];

                if (arg0.Name == null)
                {
                    list.Add(Parameter(_names.AnonymousParameter.Identifier)
                        .WithType(_names.MakeTypeSyntax(arg0.Type, method.DeclaringInterface, TypeUsage.DomainClass, Nullability.NullableRef)));
                }
                else
                {
                    foreach (var arg in method.Params)
                    {
                        list.Add(Parameter(Identifier(_names.GetCodeIdentifierLowerCamel(arg)))
                            .WithType(_names.MakeTypeSyntax(arg.Type, method.DeclaringInterface, TypeUsage.DomainClass, Nullability.NullableRef)));
                    }
                }
            }

            list.Add(Parameter(_names.CancellationTokenParameter.Identifier)
                .WithType(IdentifierName(nameof(CancellationToken)))
                .WithDefault(EqualsValueClause(LiteralExpression(
                    SyntaxKind.DefaultLiteralExpression,
                    Token(SyntaxKind.DefaultKeyword)))));

            return list.ToArray();
        }

        IEnumerable<TypeParameterSyntax> MakeTypeParameters(TypeDefinition def)
        {
            foreach (string name in def.GenericParameters)
            {
                yield return TypeParameter(_names.GetGenericTypeParameter(name).Identifier);
            }
        }

        IEnumerable<TypeParameterConstraintClauseSyntax> MakeTypeParameterConstraints(TypeDefinition def)
        {
            foreach (string name in def.GenericParameters)
            {
                yield return TypeParameterConstraintClause(
                    _names.GetGenericTypeParameter(name).IdentifierName)
                        .AddConstraints(ClassOrStructConstraint(SyntaxKind.ClassConstraint));
            }
        }

        public MemberDeclarationSyntax MakeInterface(TypeDefinition type)
        {
            var ifaceDecl = InterfaceDeclaration(_names.MakeTypeName(type, NameUsage.Interface).Identifier)
                .AddModifiers(Public)
                .AddAttributeLists(
                    _names.MakeTypeDecorationAttributes(type.Id)
                        .AddAttributes(
                            Attribute(IdentifierName("Proxy"))
                                .AddArgumentListArguments(
                                    AttributeArgument(
                                        TypeOfExpression(_names.MakeGenericTypeNameForAttribute(type, NameUsage.Proxy)))),
                            Attribute(IdentifierName("Skeleton"))
                                .AddArgumentListArguments(
                                    AttributeArgument(
                                        TypeOfExpression(_names.MakeGenericTypeNameForAttribute(type, NameUsage.Skeleton))))));

            if (type.GenericParameters.Count > 0)
            {
                ifaceDecl = ifaceDecl
                    .AddTypeParameterListParameters(MakeTypeParameters(type).ToArray())
                    .AddConstraintClauses(MakeTypeParameterConstraints(type).ToArray());
            }

            if (type.Superclasses.Count == 0)
            {
                ifaceDecl = ifaceDecl.AddBaseListTypes(SimpleBaseType(IdentifierName(nameof(IDisposable))));
            }
            else
            {
                foreach (var superClass in type.Superclasses)
                {
                    ifaceDecl = ifaceDecl.AddBaseListTypes(
                        SimpleBaseType(_names.MakeTypeSyntax(
                            superClass, type,
                            TypeUsage.DomainClass,
                            Nullability.NonNullable)));
                }
            }

            foreach (var method in type.Methods)
            {
                var methodDecl = MethodDeclaration(
                    TransformReturnType(method),
                    _names.GetCodeIdentifier(method).Identifier)
                    .AddParameterListParameters(TransformParameters(method))
                    .WithSemicolonToken(Token(SyntaxKind.SemicolonToken));

                if (method.GenericParameters.Count > 0)
                {
                    methodDecl = methodDecl
                        .AddTypeParameterListParameters(MakeTypeParameters(method).ToArray())
                        .AddConstraintClauses(MakeTypeParameterConstraints(method).ToArray());

                }

                ifaceDecl = ifaceDecl.AddMembers(methodDecl);
            }

            return ifaceDecl;
        }

        bool IsSubjectToPipelining(Model.Type type, HashSet<Model.Type> visited)
        {
            if (!visited.Add(type))
                return false;

            switch (type.Tag)
            {
                case TypeTag.AnyPointer:
                case TypeTag.CapabilityPointer:
                case TypeTag.Interface:
                case TypeTag.ListPointer:
                case TypeTag.StructPointer:
                    return true;

                case TypeTag.List:
                    return IsSubjectToPipelining(type.ElementType, visited);

                case TypeTag.Struct:
                    return type.Fields.Any(f => IsSubjectToPipelining(f.Type, visited));

                default:
                    return false;
            }
        }

        bool IsSubjectToPipelining(Method method)
        {
            return method.Results.Any(r => IsSubjectToPipelining(r.Type, new HashSet<Model.Type>()));
        }

        IEnumerable<ExpressionSyntax> MakeProxyCallInitializerAssignments(Method method)
        {
            for (int i = 0; i < method.Params.Count; i++)
            {
                var methodParam = method.Params[i];
                var field = method.ParamsStruct.Fields[i];

                yield return AssignmentExpression(
                    SyntaxKind.SimpleAssignmentExpression,
                    _names.GetCodeIdentifier(methodParam).IdentifierName,
                    IdentifierName(_names.GetCodeIdentifierLowerCamel(field)));
            }
        }

        IEnumerable<ArgumentSyntax> MakeProxyReturnResultTupleElements(Method method)
        {
            foreach (var item in method.ResultStruct.Fields)
            {
                yield return Argument(MemberAccessExpression(
                    SyntaxKind.SimpleMemberAccessExpression,
                    _names.ResultLocal.IdentifierName,
                    _names.GetCodeIdentifier(item).IdentifierName));
            }
        }

        StatementSyntax MakeProxyReturnResult(Method method)
        {
            if (method.ResultStruct.Definition.SpecialName == SpecialName.MethodResultStruct)
            {
                if (method.ResultStruct.Fields.Count == 0)
                {
                    return ReturnStatement();
                }
                else
                {
                    return ReturnStatement(TupleExpression()
                        .AddArguments(MakeProxyReturnResultTupleElements(method).ToArray()));
                }

            }
            else
            {
                return ReturnStatement(_names.ResultLocal.IdentifierName);
            }
        }

        StatementSyntax MakeProxyCreateResult(Method method)
        {
            var resultType = method.ResultStruct;
            var domainType = _names.MakeTypeSyntax(resultType, method.DeclaringInterface, TypeUsage.DomainClass, Nullability.NonNullable);

            ExpressionSyntax createDomain = InvocationExpression(
                    MemberAccessExpression(
                        SyntaxKind.SimpleMemberAccessExpression,
                        IdentifierName(nameof(Capnp.CapnpSerializable)),
                        GenericName(nameof(Capnp.CapnpSerializable.Create))
                            .AddTypeArgumentListArguments(MakeNonNullableType(domainType))))
                        .AddArgumentListArguments(
                            Argument(_names.DeserializerLocal.IdentifierName));

            if (_names.NullableEnable)
            {
                createDomain = PostfixUnaryExpression(
                    SyntaxKind.SuppressNullableWarningExpression,
                    createDomain);
            }

            return LocalDeclarationStatement(
                VariableDeclaration(
                    IdentifierName("var"))
                    .WithVariables(
                        SingletonSeparatedList(
                            VariableDeclarator(
                                _names.ResultLocal.Identifier)
                                .WithInitializer(
                                        EqualsValueClause(createDomain)))));
        }

        IEnumerable<TypeParameterSyntax> MakeTypeParameters(Method method)
        {
            foreach (string name in method.GenericParameters)
            {
                yield return TypeParameter(_names.GetGenericTypeParameter(name).Identifier);
            }
        }

        IEnumerable<TypeParameterConstraintClauseSyntax> MakeTypeParameterConstraints(Method method)
        {
            foreach (string name in method.GenericParameters)
            {
                yield return TypeParameterConstraintClause(
                    _names.GetGenericTypeParameter(name).IdentifierName)
                        .AddConstraints(ClassOrStructConstraint(SyntaxKind.ClassConstraint));
            }
        }

        public MemberDeclarationSyntax MakeProxy(TypeDefinition type)
        {
            var classDecl = ClassDeclaration(_names.MakeTypeName(type, NameUsage.Proxy).Identifier)
                .AddAttributeLists(_names.MakeTypeDecorationAttributes(type.Id))
                .AddModifiers(Public)
                .AddBaseListTypes(
                    SimpleBaseType(_names.Type<Capnp.Rpc.Proxy>(Nullability.NonNullable)),
                    SimpleBaseType(_names.MakeGenericTypeName(type, NameUsage.Interface)));

            if (type.GenericParameters.Count > 0)
            {
                classDecl = classDecl
                    .AddTypeParameterListParameters(MakeTypeParameters(type).ToArray())
                    .AddConstraintClauses(MakeTypeParameterConstraints(type).ToArray());
            }

            var allMethods =
                from c in Types.FromDefinition(type).AllImplementedClasses
                from m in c.Methods
                select m;

            foreach (var method in allMethods)
            {
                var bodyStmts = new List<StatementSyntax>();

                bodyStmts.Add(LocalDeclarationStatement(
                            VariableDeclaration(
                                IdentifierName("var"))
                            .WithVariables(
                                SingletonSeparatedList(
                                    VariableDeclarator(
                                        _names.ParamsLocal.Identifier)
                                    .WithInitializer(
                                        EqualsValueClause(
                                            InvocationExpression(
                                                MemberAccessExpression(
                                                    SyntaxKind.SimpleMemberAccessExpression,
                                                    IdentifierName(nameof(Capnp.SerializerState)),
                                                    GenericName(
                                                        Identifier(nameof(Capnp.SerializerState.CreateForRpc)))
                                                    .WithTypeArgumentList(
                                                        TypeArgumentList(
                                                            SingletonSeparatedList(
                                                                _names.MakeTypeSyntax(
                                                                    method.ParamsStruct, 
                                                                    method.ParamsStruct.Definition, 
                                                                    TypeUsage.Writer, Nullability.NonNullable))))))))))));

                if (method.ParamsStruct.Definition.SpecialName == SpecialName.MethodParamsStruct)
                {
                    bodyStmts.Add(LocalDeclarationStatement(
                                VariableDeclaration(
                                    IdentifierName("var"))
                                .WithVariables(
                                    SingletonSeparatedList<VariableDeclaratorSyntax>(
                                        VariableDeclarator(
                                            _names.AnonymousParameter.Identifier)
                                        .WithInitializer(
                                            EqualsValueClause(
                                                ObjectCreationExpression(
                                                    _names.MakeTypeSyntax(
                                                        method.ParamsStruct,
                                                        method.ParamsStruct.Definition,
                                                        TypeUsage.DomainClass,
                                                        Nullability.NonNullable))
                                                .WithArgumentList(
                                                    ArgumentList())
                                                .WithInitializer(
                                                    InitializerExpression(
                                                        SyntaxKind.ObjectInitializerExpression,
                                                        SeparatedList<ExpressionSyntax>(
                                                            CommonSnippetGen.MakeCommaSeparatedList(
                                                                MakeProxyCallInitializerAssignments(method)).ToArray())))))))));
                }

                bodyStmts.Add(ExpressionStatement(
                    ConditionalAccessExpression(
                        _names.AnonymousParameter.IdentifierName,
                        InvocationExpression(
                            MemberBindingExpression(_names.SerializeMethod.IdentifierName))
                        .AddArgumentListArguments(
                            Argument(_names.ParamsLocal.IdentifierName)))));

                var call = InvocationExpression(IdentifierName(nameof(Capnp.Rpc.BareProxy.Call)))
                    .AddArgumentListArguments(
                        Argument(
                            LiteralExpression(SyntaxKind.NumericLiteralExpression,
                                Literal(method.DeclaringInterface.Id))),
                        Argument(
                            LiteralExpression(SyntaxKind.NumericLiteralExpression, Literal(method.Id))),
                        Argument(
                            InvocationExpression(
                                MemberAccessExpression(
                                    SyntaxKind.SimpleMemberAccessExpression,
                                    _names.ParamsLocal.IdentifierName,
                                    GenericName(nameof(Capnp.SerializerState.Rewrap))
                                        .AddTypeArgumentListArguments(_names.Type<Capnp.DynamicSerializerState>(Nullability.NonNullable))))
                                        .AddArgumentListArguments()),
                        Argument(
                            LiteralExpression(SyntaxKind.FalseLiteralExpression)),
                        Argument(
                            _names.CancellationTokenParameter.IdentifierName));

                MethodDeclarationSyntax methodDecl;

                if (IsSubjectToPipelining(method))
                {
                    methodDecl = MethodDeclaration(
                        TransformReturnType(method),
                        _names.GetCodeIdentifier(method).Identifier)
                        .AddParameterListParameters(TransformParameters(method))
                        .AddModifiers(Public);

                    var pipelineAwareCall = InvocationExpression(
                        MemberAccessExpression(
                            SyntaxKind.SimpleMemberAccessExpression,
                            IdentifierName(nameof(Capnp.Rpc.Impatient)),
                            IdentifierName(nameof(Capnp.Rpc.Impatient.MakePipelineAware))))
                            .AddArgumentListArguments(
                                Argument(call),
                                Argument(SimpleLambdaExpression(
                                    Parameter(_names.DeserializerLocal.Identifier),
                                    Block(
                                        UsingStatement(
                                            Block(  
                                                MakeProxyCreateResult(method), 
                                                MakeProxyReturnResult(method)))
                                        .WithExpression(_names.DeserializerLocal.IdentifierName)))));

                    bodyStmts.Add(ReturnStatement(pipelineAwareCall));
                }
                else
                {
                    methodDecl = MethodDeclaration(
                        TransformReturnType(method),
                        _names.GetCodeIdentifier(method).Identifier)
                        .AddParameterListParameters(TransformParameters(method))
                        .AddModifiers(Public, Token(SyntaxKind.AsyncKeyword));

                    var whenReturned = MemberAccessExpression(
                        SyntaxKind.SimpleMemberAccessExpression,
                        call,
                        IdentifierName(nameof(Capnp.Rpc.IPromisedAnswer.WhenReturned)));

                    bodyStmts.Add(UsingStatement(
                        Block(
                            MakeProxyCreateResult(method),
                            MakeProxyReturnResult(method)))
                        .WithDeclaration(VariableDeclaration(
                            IdentifierName("var"))
                                .AddVariables(
                                    VariableDeclarator(
                                        _names.DeserializerLocal.Identifier)
                                    .WithInitializer(
                                        EqualsValueClause(
                                            AwaitExpression(whenReturned))))));
                }

                if (method.GenericParameters.Count > 0)
                {
                    methodDecl = methodDecl
                        .AddTypeParameterListParameters(MakeTypeParameters(method).ToArray())
                        .AddConstraintClauses(MakeTypeParameterConstraints(method).ToArray());
                }

                methodDecl = methodDecl.AddBodyStatements(bodyStmts.ToArray());

                classDecl = classDecl.AddMembers(methodDecl);
            }

            return classDecl;
        }

        IEnumerable<ArgumentSyntax> MakeSkeletonSetMethodTableArguments(TypeDefinition def)
        {
            foreach (var method in def.Methods)
            {
                if (method.GenericParameters.Count > 0)
                {
                    yield return Argument(
                        GenericName(_names.GetCodeIdentifier(method).ToString())
                            .AddTypeArgumentListArguments(
                                Enumerable.Repeat(
                                    _names.Type<Capnp.AnyPointer>(Nullability.NonNullable),
                                    method.GenericParameters.Count).ToArray()));
                }
                else
                {
                    yield return Argument(_names.GetCodeIdentifier(method).IdentifierName);
                }
            }
        }

        IEnumerable<ExpressionSyntax> MakeSkeletonMethodResultStructInitializer(Method method)
        {
            foreach (var arg in method.Results)
            {
                yield return AssignmentExpression(
                    SyntaxKind.SimpleAssignmentExpression,
                    _names.GetCodeIdentifier(arg).IdentifierName,
                    IdentifierName(_names.GetCodeIdentifierLowerCamel(arg)));
            }
        }

        IEnumerable<ArgumentSyntax> MakeSkeletonMethodCallArgs(Method method)
        {
            foreach (var arg in method.ParamsStruct.Fields)
            {
                yield return Argument(
                    MemberAccessExpression(
                        SyntaxKind.SimpleMemberAccessExpression,
                        _names.ParamsLocal.IdentifierName,
                        _names.GetCodeIdentifier(arg).IdentifierName));
            }
        }

        StatementSyntax MakeSkeletonMethodSerializerLocalDeclaration(Method method)
        {
            return LocalDeclarationStatement(
                VariableDeclaration(
                    IdentifierName("var"))
                .WithVariables(
                    SingletonSeparatedList(
                        VariableDeclarator(
                            _names.SerializerLocal.Identifier)
                        .WithInitializer(
                            EqualsValueClause(
                                InvocationExpression(
                                    MemberAccessExpression(
                                        SyntaxKind.SimpleMemberAccessExpression,
                                        IdentifierName(nameof(Capnp.SerializerState)),
                                        GenericName(
                                            Identifier(nameof(Capnp.SerializerState.CreateForRpc)))
                                        .WithTypeArgumentList(
                                            TypeArgumentList(
                                                SingletonSeparatedList(
                                                    _names.MakeTypeSyntax(
                                                        method.ResultStruct,
                                                        method.ResultStruct.Definition,
                                                        TypeUsage.Writer, Nullability.NonNullable)))))))))));

        }

        CSharpSyntaxNode MakeMaybeTailCallLambdaBody(Method method)
        {
            var block = Block(
                MakeSkeletonMethodSerializerLocalDeclaration(method));

            if (method.ResultStruct.Definition.SpecialName == SpecialName.MethodResultStruct)
            {
                block = block.AddStatements(
                    LocalDeclarationStatement(
                        VariableDeclaration(
                            IdentifierName("var"))
                            .AddVariables(
                                VariableDeclarator(_names.ResultLocal.Identifier)
                                .WithInitializer(EqualsValueClause(ObjectCreationExpression(
                                    _names.MakeTypeSyntax(
                                        method.ResultStruct,
                                        method.ResultStruct.Definition,
                                        TypeUsage.DomainClass,
                                        Nullability.NonNullable))
                                    .WithInitializer(
                                        InitializerExpression(SyntaxKind.ObjectInitializerExpression)
                                            .AddExpressions(
                                                MakeSkeletonMethodResultStructInitializer(method).ToArray())))))));
            }

            if (method.Results.Count > 0)
            {
                block = block.AddStatements(
                    ExpressionStatement(
                        InvocationExpression(
                            MemberAccessExpression(
                                SyntaxKind.SimpleMemberAccessExpression,
                                _names.ResultLocal.IdentifierName,
                                _names.SerializeMethod.IdentifierName))
                            .AddArgumentListArguments(
                                Argument(_names.SerializerLocal.IdentifierName))));
            }

            block = block.AddStatements(
                ReturnStatement(_names.SerializerLocal.IdentifierName));

            return block;
        }

        IEnumerable<StatementSyntax> MakeSkeletonMethodBody(Method method)
        {
            SimpleNameSyntax methodName;

            if (method.GenericParameters.Count == 0)
            {
                methodName = _names.GetCodeIdentifier(method).IdentifierName;
            }
            else
            {
                methodName = GenericName(_names.GetCodeIdentifier(method).Identifier)
                    .AddTypeArgumentListArguments(
                        method.GenericParameters.Select(
                            p => _names.GetGenericTypeParameter(p).IdentifierName)
                        .ToArray());
            }

            var call = InvocationExpression(
                MemberAccessExpression(
                    SyntaxKind.SimpleMemberAccessExpression,
                    IdentifierName(SkeletonWorder.ImplName),
                    methodName));

            if (method.Params.Count > 0)
            {
                var paramsType = method.ParamsStruct;
                var domainType = _names.MakeTypeSyntax(paramsType, method.ParamsStruct.Definition, TypeUsage.DomainClass, Nullability.NonNullable);

                ExpressionSyntax createDomain = InvocationExpression(
                        MemberAccessExpression(
                            SyntaxKind.SimpleMemberAccessExpression,
                            IdentifierName(nameof(Capnp.CapnpSerializable)),
                            GenericName(nameof(Capnp.CapnpSerializable.Create))
                                .AddTypeArgumentListArguments(MakeNonNullableType(domainType))))
                            .AddArgumentListArguments(
                                Argument(_names.DeserializerLocal.IdentifierName));

                if (_names.NullableEnable)
                {
                    createDomain = PostfixUnaryExpression(
                        SyntaxKind.SuppressNullableWarningExpression,
                        createDomain);
                }

                if (method.ParamsStruct.Definition.SpecialName == SpecialName.MethodParamsStruct)
                {
                    yield return LocalDeclarationStatement(
                        VariableDeclaration(
                            IdentifierName("var"))
                            .AddVariables(
                                VariableDeclarator(_names.ParamsLocal.Identifier)
                                .WithInitializer(EqualsValueClause(createDomain))));

                    call = call.AddArgumentListArguments(
                        MakeSkeletonMethodCallArgs(method).ToArray());
                }
                else
                {
                    call = call.AddArgumentListArguments(
                        Argument(createDomain));
                }
            }

            call = call.AddArgumentListArguments(
                Argument(
                    _names.CancellationTokenParameter.IdentifierName));

            if (method.Results.Count == 0)
            {
                var awaitCall = AwaitExpression(call);
                yield return ExpressionStatement(awaitCall);
                yield return MakeSkeletonMethodSerializerLocalDeclaration(method);
                yield return ReturnStatement(_names.SerializerLocal.IdentifierName);
            }
            else
            {
                ExpressionSyntax lambdaArg;

                if (method.ResultStruct.Definition.SpecialName == SpecialName.MethodResultStruct)
                {
                    if (method.Results.Count == 1)
                    {
                        lambdaArg = SimpleLambdaExpression(
                            Parameter(Identifier(_names.GetCodeIdentifierLowerCamel(method.Results.Single()))),
                            MakeMaybeTailCallLambdaBody(method));
                    }
                    else
                    {
                        // CodeAnalysis.CSharp 3.2.1 has a bug which prevents us from using AddParameterListParameters. :-(

                        var paramList = new List<SyntaxNodeOrToken>();
                        foreach (var arg in method.Results)
                        {
                            if (paramList.Count > 0)
                                paramList.Add(Token(SyntaxKind.CommaToken));
                            paramList.Add(Parameter(Identifier(_names.GetCodeIdentifierLowerCamel(arg))));
                        }
                        lambdaArg = ParenthesizedLambdaExpression(
                            ParameterList(
                                SeparatedList<ParameterSyntax>(paramList)),
                            MakeMaybeTailCallLambdaBody(method));
                    }
                }
                else
                {
                    lambdaArg = SimpleLambdaExpression(
                        Parameter(_names.ResultLocal.Identifier),
                        MakeMaybeTailCallLambdaBody(method));

                }

                var maybeTailCall = InvocationExpression(
                    MemberAccessExpression(
                        SyntaxKind.SimpleMemberAccessExpression,
                        IdentifierName(nameof(Capnp.Rpc.Impatient)),
                        IdentifierName(nameof(Capnp.Rpc.Impatient.MaybeTailCall))))
                        .AddArgumentListArguments(
                            Argument(call),
                            Argument(lambdaArg));

                yield return ReturnStatement(maybeTailCall);
            }
        }

        IEnumerable<MemberDeclarationSyntax> MakeSkeletonMethods(TypeDefinition def)
        {
            foreach (var method in def.Methods)
            {
                var methodDecl = MethodDeclaration(
                    _names.Type<Task<Capnp.Rpc.AnswerOrCounterquestion>>(Nullability.NonNullable),
                    _names.GetCodeIdentifier(method).Identifier)
                    .AddParameterListParameters(
                        Parameter(_names.DeserializerLocal.Identifier)
                            .WithType(_names.Type<Capnp.DeserializerState>(Nullability.NonNullable)),
                        Parameter(_names.CancellationTokenParameter.Identifier)
                            .WithType(_names.Type<CancellationToken>(Nullability.NonNullable)))
                    .AddBodyStatements(
                        UsingStatement(
                            Block(
                                MakeSkeletonMethodBody(method).ToArray()))
                            .WithExpression(_names.DeserializerLocal.IdentifierName));

                if (method.Results.Count == 0)
                {
                    methodDecl = methodDecl.AddModifiers(Async);
                }

                if (method.GenericParameters.Count > 0)
                {
                    methodDecl = methodDecl
                        .AddTypeParameterListParameters(MakeTypeParameters(method).ToArray())
                        .AddConstraintClauses(MakeTypeParameterConstraints(method).ToArray());
                }

                yield return methodDecl;
            }
        }

        public MemberDeclarationSyntax MakeSkeleton(TypeDefinition type)
        {
            var name = _names.MakeTypeName(type, NameUsage.Skeleton).Identifier;
            var classDecl = ClassDeclaration(name)
                .AddAttributeLists(_names.MakeTypeDecorationAttributes(type.Id))
                .AddModifiers(Public)
                .AddBaseListTypes(
                    SimpleBaseType(
                        GenericName(nameof(Capnp.Rpc.Skeleton))
                            .AddTypeArgumentListArguments(
                                _names.MakeGenericTypeName(type, NameUsage.Interface))))
                .AddMembers(
                    // C'tor
                    ConstructorDeclaration(name)
                        .AddModifiers(Public)
                        .AddBodyStatements(
                            ExpressionStatement(
                                InvocationExpression(
                                    IdentifierName(SkeletonWorder.SetMethodTableName))
                                    .AddArgumentListArguments(
                                        MakeSkeletonSetMethodTableArguments(type).ToArray()))),
                    // InterfaceId
                    PropertyDeclaration(_names.Type<ulong>(Nullability.NonNullable), nameof(Capnp.Rpc.Skeleton<object>.InterfaceId))
                        .AddModifiers(Public, Override)
                        .WithExpressionBody(
                            ArrowExpressionClause(
                                ValueOf(type.Id)))
                        .WithSemicolonToken(
                            Token(SyntaxKind.SemicolonToken)));

            if (type.GenericParameters.Count > 0)
            {
                classDecl = classDecl
                    .AddTypeParameterListParameters(MakeTypeParameters(type).ToArray())
                    .AddConstraintClauses(MakeTypeParameterConstraints(type).ToArray());
            }

            classDecl = classDecl.AddMembers(MakeSkeletonMethods(type).ToArray());

            return classDecl;
        }

        public bool RequiresPipeliningSupport(TypeDefinition type)
        {
            return type.Methods.Any(m => ExpandPipeliningPaths(m).Any());
        }

        IEnumerable<IReadOnlyList<Field>> ExpandPipeliningPaths(Method method)
        {
            var stack = new Stack<List<Field>>();
            foreach (var field in method.ResultStruct.Fields)
            {
                stack.Push(new List<Field>() { field });
            }

            while (stack.Count > 0)
            {
                var path = stack.Pop();
                var last = path[path.Count - 1];

                switch (last.Type.Tag)
                {
                    case TypeTag.Interface:
                    case TypeTag.CapabilityPointer:
                        yield return path;
                        break;

                    case TypeTag.Struct:
                        foreach (var field in last.Type.Fields)
                        {
                            if (path.Contains(field))
                            {
                                // Recursive structs protection
                                continue;
                            }

                            var copy = new List<Field>();
                            copy.AddRange(path);
                            copy.Add(field);
                            stack.Push(copy);
                        }
                        break;
                }
            }
        }

        readonly HashSet<(string, string)> _existingExtensionMethods = new HashSet<(string, string)>();

        LocalFunctionStatementSyntax MakeLocalAwaitProxyFunction(Method method, IReadOnlyList<Field> path)
        {
            var members = new List<Name>();
            IEnumerable<Field> fields = path;

            if (method.Results.Count >= 2)
            {
                int index = Array.IndexOf(method.ResultStruct.Fields.ToArray(), path[0]) + 1;
                members.Add(new Name($"Item{index}"));
                fields = path.Skip(1);
            }

            foreach (var field in fields)
            {
                members.Add(_names.GetCodeIdentifier(field));
            }

            ExpressionSyntax memberAccess = 
                ParenthesizedExpression(
                    AwaitExpression(
                        _names.TaskParameter.IdentifierName));

            memberAccess = MemberAccessExpression(
                SyntaxKind.SimpleMemberAccessExpression,
                memberAccess,
                members.First().IdentifierName);

            foreach (var member in members.Skip(1))
            {
                memberAccess = ConditionalAccessExpression(
                    memberAccess,
                    MemberBindingExpression(member.IdentifierName));
            }

            var idisposable = _names.MakeNullableRefType(IdentifierName(nameof(IDisposable)));

            return LocalFunctionStatement(
                    GenericName(
                        Identifier(nameof(Task)))
                    .WithTypeArgumentList(
                        TypeArgumentList(
                            SingletonSeparatedList<TypeSyntax>(
                                idisposable))),
                    _names.AwaitProxy.Identifier)
                .WithModifiers(
                    TokenList(
                        Token(SyntaxKind.AsyncKeyword)))
                .WithExpressionBody(
                    ArrowExpressionClause(memberAccess))
                .WithSemicolonToken(
                    Token(SyntaxKind.SemicolonToken));
        }

        public IEnumerable<MemberDeclarationSyntax> MakePipeliningSupport(TypeDefinition type)
        {
            foreach (var method in type.Methods)
            {
                foreach (var path in ExpandPipeliningPaths(method))
                {
                    if (path.Count == 1 && path[0].Offset == 0)
                    {
                        // The "trivial path" is already covered by the "Eager" extension method.
                        continue;
                    }

                    var accessPath = _names.MakeMemberAccessPathFieldName(method, path);
                    var methodName = _names.MakePipeliningSupportExtensionMethodName(path);
                    var capType = path[path.Count - 1].Type;
                    var capTypeSyntax = _names.MakeTypeSyntax(capType, null, TypeUsage.DomainClass, Nullability.NonNullable);

                    if (!_existingExtensionMethods.Add((capTypeSyntax.ToString(), methodName.ToString())))
                    {
                        continue;
                    }

                    var pathDecl = FieldDeclaration(
                        VariableDeclaration(
                            IdentifierName(nameof(Capnp.Rpc.MemberAccessPath)))
                        .AddVariables(
                            VariableDeclarator(
                                accessPath.Identifier)
                            .WithInitializer(
                                EqualsValueClause(
                                    ObjectCreationExpression(
                                        IdentifierName(nameof(Capnp.Rpc.MemberAccessPath)))
                                        .AddArgumentListArguments(
                                            path.Select(
                                                f => Argument(
                                                        LiteralExpression(SyntaxKind.NumericLiteralExpression,
                                                        Literal(f.Offset)))).ToArray())))))
                        .AddModifiers(Static, Readonly);


                    var methodDecl = MethodDeclaration(capTypeSyntax, methodName.Identifier)
                        .AddModifiers(Public, Static)
                        .AddParameterListParameters(
                            Parameter(
                                _names.TaskParameter.Identifier)
                            .AddModifiers(This)
                            .WithType(TransformReturnType(method)))
                        .AddBodyStatements(
                            MakeLocalAwaitProxyFunction(method, path),
                            ReturnStatement(
                                CastExpression(
                                    capTypeSyntax,
                                    InvocationExpression(
                                        MemberAccessExpression(
                                            SyntaxKind.SimpleMemberAccessExpression,
                                            IdentifierName(nameof(Capnp.Rpc.CapabilityReflection)),
                                            GenericName(
                                                Identifier(nameof(Capnp.Rpc.CapabilityReflection.CreateProxy)))
                                            .AddTypeArgumentListArguments(
                                                capTypeSyntax)))
                                    .AddArgumentListArguments(
                                        Argument(
                                            InvocationExpression(
                                                MemberAccessExpression(
                                                    SyntaxKind.SimpleMemberAccessExpression,
                                                    IdentifierName(nameof(Capnp.Rpc.Impatient)),
                                                    IdentifierName(nameof(Capnp.Rpc.Impatient.Access))))
                                            .AddArgumentListArguments(
                                                Argument(
                                                    _names.TaskParameter.IdentifierName),
                                                Argument(
                                                    accessPath.IdentifierName),
                                                Argument(
                                                    InvocationExpression(
                                                        _names.AwaitProxy.IdentifierName))))))));

                    yield return pathDecl;
                    yield return methodDecl;
                }
            }
        }
    }
}