/*
 * Copyright 2022, TeamDev. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Redistribution and use in source and/or binary forms, with or without
 * modification, must retain the above copyright notice and the following
 * disclaimer.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package io.spine.server;

import com.google.common.collect.ImmutableCollection;
import io.grpc.BindableService;
import io.grpc.stub.StreamObserver;
import io.spine.logging.Logging;
import io.spine.protobuf.Messages;
import io.spine.type.TypeUrl;
import io.spine.type.UnpublishedLanguageException;
import org.checkerframework.checker.nullness.qual.Nullable;

import java.util.Optional;

import static com.google.common.base.Preconditions.checkNotNull;
import static io.spine.type.MessageExtensions.isInternal;

/**
 * An abstract base for classes implementing server-side services.
 *
 * <p>This class implements a {@linkplain #serve(Object, StreamObserver, Object) common way}
 * services work. Initially, each request is checked not to be for an unpublished part
 * of the domain language. In such a case, an error is returned.
 *
 * <p>Then, a service attempts to find a bounded context which would handle
 * the request. If not found, an error returned. Otherwise, the request is
 * {@linkplain #serve(BoundedContext, Object, StreamObserver, Object) dispatched} to the bounded context.
 *
 * @param <T>
 *         the type of the request handled by the service
 * @param <R>
 *         the type of the response the service sends to the client
 * @implNote A service implementation is arranged using delegation because service classes
 *         have to extend from abstract base classes generated by gRPC. Since we don't have multiple
 *         inheritance in Java, we use the delegation.
 */
abstract class ServiceDelegate<T, R> implements Logging {

    private final BindableService service;
    private final TypeDictionary types;

    /**
     * Creates a new instance with the reference to the parent service and served types.
     */
    ServiceDelegate(BindableService service, TypeDictionary types) {
        this.service = checkNotNull(service);
        this.types = checkNotNull(types);
    }

    /**
     * Handles the posted request.
     */
    final void serve(T request, StreamObserver<R> observer, @Nullable Object params) {
        if (detectInternal(request)) {
            handleInternal(request, observer);
            return;
        }
        var type = enclosedMessageType(request);
        var boundedContext = find(type);
        boundedContext.ifPresentOrElse(
            ctx -> serve(ctx, request, observer, params),
            () -> serveNoContext(request, observer, params)
        );
    }

    /**
     * Obtains the type of the message enclosed into the request.
     *
     * <p>For a command, for example, it would be a type of the command message.
     * The returned type would be used for finding a bounded context to which the enclosed
     * message belongs.
     */
    protected abstract TypeUrl enclosedMessageType(T request);

    /**
     * Handles the request by passing it to corresponding feature of
     * the specified bounded context.
     *
     * @param context
     *         the bounded context which would serve the request
     * @param request
     *         the request to serve
     * @param observer
     *         the observer for obtaining results
     * @param params
     *         optional implementation-dependent parameters, or {@code null} if the service
     *         implementation does not need additional parameters
     */
    protected abstract void serve(BoundedContext context,
                                  T request,
                                  StreamObserver<R> observer,
                                  @Nullable Object params);

    /**
     * Handles the case when no bounded context was found for the incoming request.
     *
     * <p>Implementations may return an error to the given observer.
     */
    protected abstract void serveNoContext(T request,
                                           StreamObserver<R> observer,
                                           @Nullable Object params);

    /**
     * Verifies of the given request contains a message marked as "internal".
     */
    protected boolean detectInternal(T request) {
        var msgClass = enclosedMessageType(request).getMessageClass();
        var result = isInternal(msgClass);
        return result;
    }

    /**
     * Handles the case of internal message enclosed into the specified request.
     *
     * <p>Default implementation reports {@link UnpublishedLanguageException} to
     * the given observer.
     */
    protected void handleInternal(T request, StreamObserver<R> observer) {
        var targetType = enclosedMessageType(request);
        var defTarget = Messages.defaultInstance(targetType.getMessageClass());
        var unpublishedLanguage = new UnpublishedLanguageException(defTarget);
        _error().withCause(unpublishedLanguage)
                .log("Unpublished type (`%s`) posted to `%s`.", targetType, serviceName());
        observer.onError(unpublishedLanguage);
    }

    final Optional<BoundedContext> find(TypeUrl type) {
        return types.find(type);
    }

    final String serviceName() {
        var result = service.getClass().getSimpleName();
        return result;
    }

    final ImmutableCollection<BoundedContext> contexts() {
        return types.contexts();
    }
}

@@ -1421,43 +1421,8 @@
 ng;%0A
-import io.spine.protobuf.Messages;%0A
 impo
@@ -2268,16 +2268,19 @@
 ched%7D to
+%0A *
  the bou
@@ -5740,88 +5740,8 @@
 t);%0A
-        var defTarget = Messages.defaultInstance(targetType.getMessageClass());%0A
     
@@ -5807,17 +5807,31 @@
 ion(
-defT
+t
 arget
+Type.toTypeName()
 );%0A 
@@ -6015,32 +6015,112 @@
 nguage);%0A    %7D%0A%0A
+    /**%0A     * Finds a bounded context to which the given type belongs.%0A     */%0A
     final Option
@@ -6191,32 +6191,136 @@
 d(type);%0A    %7D%0A%0A
+    /**%0A     * Obtains the simple class name of the associated service for diagnostic messages.%0A     */%0A
     final String
@@ -6419,24 +6419,102 @@
 ult;%0A    %7D%0A%0A
+    /**%0A     * Obtains all the bounded contexts known to the service.%0A     */%0A
     final Im

Use exception ctor with type name