/*
 *  Copyright (c) 2019, Salesforce.com, Inc.
 *  All rights reserved.
 *  Licensed under the BSD 3-Clause license.
 *  For full license text, see LICENSE.txt file in the repo root  or https://opensource.org/licenses/BSD-3-Clause
 */

package com.salesforce.grpc.contrib.instancemode;

import io.grpc.*;

import java.util.Collection;
import java.util.function.Supplier;

/**
 * {@code PerCallService} forces gRPC to instantiate a fresh service implementation object with every request,
 * instead of reusing a well-known singleton instance.
 *
 * <p>{@code PerCallService} is useful when you need total isolation between requests. Since every request instantiates
 * a fresh service implementation object, there is no opportunity for shared state to leak between requests. However,
 * isolation comes at a cost to performance. Service implementation initialization time is added to every request. If
 * initialization is costly or time consuming, gRPC throughput will noticeably degrade.
 *
 * <p>{@code PerCallService} is also useful when you only want to hold references to expensive resources for the
 * duration of a single operation -- for example, database connections or file handles. With a traditional singleton
 * gRPC service implementation, you would be responsible for acquiring and freeing resources manually with every
 * request. Using a {@code PerCallService}, you can use more traditional Object Oriented patterns for resource
 * management, like constructors and {@link AutoCloseable}.
 *
 * <p>If the decorated service instance implements {@link AutoCloseable}, the instance's {@link AutoCloseable#close()}
 * method will be called upon completion or cancellation of each gRPC request. Use this opportunity to free any
 * shared resources.
 *
 * @param <T> a {@code BindableService} implementation to decorate
 */
public class PerCallService<T extends BindableService> implements BindableService {
    private ServerServiceDefinition perCallBinding;

    /**
     * Create a {@code PerCallService} for a provided service implementation class, generated by a factory method.
     *
     * @param factory A factory that will initialize a new service implementation object for every call.
     */
    public PerCallService(Supplier<T> factory) {
        perCallBinding = bindService(factory);
    }

    /**
     * Create a {@code PerCallService} for a provided service implementation class. The provided class must have a
     * default constructor.
     *
     * @param clazz The service implementation class to decorate.
     */
    public PerCallService(Class<T> clazz) {
        this (() -> {
            try {
                return clazz.newInstance();
            } catch (ReflectiveOperationException e) {
                throw new IllegalArgumentException("Class " + clazz.getName() + " must have a public default constructor", e);
            }
        });
    }

    @SuppressWarnings("unchecked")
    private ServerServiceDefinition bindService(Supplier<T> factory) {
        ServerServiceDefinition baseDefinition = factory.get().bindService();
        ServiceDescriptor descriptor = baseDefinition.getServiceDescriptor();
        Collection<ServerMethodDefinition<?, ?>> methods =  baseDefinition.getMethods();

        ServerServiceDefinition.Builder builder = ServerServiceDefinition.builder(descriptor);
        methods.forEach(method -> builder.addMethod(ServerMethodDefinition.create(method.getMethodDescriptor(), new PerCallServerCallHandler(factory))));
        return builder.build();
    }

    @Override
    public ServerServiceDefinition bindService() {
        return perCallBinding;
    }

    /**
     * Internal class implementing the per-call service pattern.
     */
    private class PerCallServerCallHandler implements ServerCallHandler {
        private Supplier<T> factory;

        PerCallServerCallHandler(Supplier<T> factory) {
            this.factory = factory;
        }

        @Override
        @SuppressWarnings("unchecked")
        public ServerCall.Listener startCall(ServerCall call, Metadata headers) {
            BindableService instance = factory.get();
            ServerServiceDefinition definition =  instance.bindService();
            ServerMethodDefinition method = definition.getMethod(call.getMethodDescriptor().getFullMethodName());

            return new ForwardingServerCallListener.SimpleForwardingServerCallListener<T>(method.getServerCallHandler().startCall(call, headers)) {
                @Override
                public void onCancel() {
                    super.onCancel();
                    close();
                }

                @Override
                public void onComplete() {
                    super.onComplete();
                    close();
                }

                private void close() {
                    if (instance instanceof AutoCloseable) {
                        try {
                            ((AutoCloseable) instance).close();
                        } catch (Throwable t) {
                            throw new RuntimeException(t);
                        }
                    }
                }
            };
        }
    }
}
