/*
 * Copyright 2020, TeamDev. All rights reserved.
 *
 * 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.ImmutableMap;
import com.google.common.collect.Sets;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import io.grpc.stub.StreamObserver;
import io.spine.base.Error;
import io.spine.client.grpc.CommandServiceGrpc;
import io.spine.core.Ack;
import io.spine.core.Command;
import io.spine.logging.Logging;
import io.spine.server.commandbus.CommandBus;
import io.spine.server.commandbus.UnsupportedCommandException;
import io.spine.server.type.CommandClass;

import java.util.Map;
import java.util.Set;

import static io.spine.server.bus.Buses.reject;

/**
 * The {@code CommandService} allows client applications to post commands and
 * receive updates from the application backend.
 */
public final class CommandService
        extends CommandServiceGrpc.CommandServiceImplBase
        implements Logging {

    private final ImmutableMap<CommandClass, BoundedContext> boundedContextMap;

    /**
     * Constructs new instance using the map from a {@code CommandClass} to
     * a {@code BoundedContext} instance which handles the command.
     */
    private CommandService(Map<CommandClass, BoundedContext> map) {
        super();
        this.boundedContextMap = ImmutableMap.copyOf(map);
    }

    /**
     * Creates a new builder for {@code CommandService}.
     */
    public static Builder newBuilder() {
        return new Builder();
    }

    @Override
    public void post(Command request, StreamObserver<Ack> responseObserver) {
        CommandClass commandClass = CommandClass.of(request);
        BoundedContext boundedContext = boundedContextMap.get(commandClass);
        if (boundedContext == null) {
            handleUnsupported(request, responseObserver);
        } else {
            CommandBus commandBus = boundedContext.commandBus();
            commandBus.post(request, responseObserver);
        }
    }

    private void handleUnsupported(Command command, StreamObserver<Ack> responseObserver) {
        UnsupportedCommandException unsupported = new UnsupportedCommandException(command);
        _error().withCause(unsupported)
                .log("Unsupported command posted to `CommandService`.");
        Error error = unsupported.asError();
        Ack response = reject(command.getId(), error);
        responseObserver.onNext(response);
        responseObserver.onCompleted();
    }

    public static class Builder {

        private final Set<BoundedContext> boundedContexts = Sets.newHashSet();

        /**
         * Adds the {@code BoundedContext} to the builder.
         */
        @CanIgnoreReturnValue
        public Builder add(BoundedContext boundedContext) {
            // Saves it to a temporary set so that it is easy to remove it if needed.
            boundedContexts.add(boundedContext);
            return this;
        }

        /**
         * Removes the {@code BoundedContext} from the builder.
         */
        @CanIgnoreReturnValue
        public Builder remove(BoundedContext boundedContext) {
            boundedContexts.remove(boundedContext);
            return this;
        }

        /**
         * Verifies if the passed {@code BoundedContext} was previously added to the builder.
         *
         * @param boundedContext the instance to check
         * @return {@code true} if the instance was added to the builder, {@code false} otherwise
         */
        public boolean contains(BoundedContext boundedContext) {
            boolean contains = boundedContexts.contains(boundedContext);
            return contains;
        }

        /**
         * Builds a new {@link CommandService}.
         */
        public CommandService build() {
            ImmutableMap<CommandClass, BoundedContext> map = createMap();
            CommandService result = new CommandService(map);
            return result;
        }

        /**
         * Creates a map from {@code CommandClass}es to {@code BoundedContext}s that
         * handle such commands.
         */
        private ImmutableMap<CommandClass, BoundedContext> createMap() {
            ImmutableMap.Builder<CommandClass, BoundedContext> builder = ImmutableMap.builder();
            for (BoundedContext boundedContext : boundedContexts) {
                putIntoMap(boundedContext, builder);
            }
            return builder.build();
        }

        /**
         * Associates {@code CommandClass}es with the instance of {@code BoundedContext}
         * that handles such commands.
         */
        private static void putIntoMap(BoundedContext boundedContext,
                                       ImmutableMap.Builder<CommandClass, BoundedContext> builder) {
            CommandBus commandBus = boundedContext.commandBus();
            Set<CommandClass> cmdClasses = commandBus.registeredCommandClasses();
            for (CommandClass commandClass : cmdClasses) {
                builder.put(commandClass, boundedContext);
            }
        }
    }
}

@@ -1946,33 +1946,32 @@
 ontext%3E 
-bounded
+commandTo
 Context
-Map
 ;%0A%0A    /
@@ -2223,33 +2223,32 @@
    this.
-bounded
+commandTo
 Context
-Map
  = Immut
@@ -2599,32 +2599,25 @@
 ndedContext 
-boundedC
+c
 ontext = bou
@@ -2617,25 +2617,24 @@
 t = 
-bounded
+commandTo
 Context
-Map
 .get
@@ -2661,24 +2661,17 @@
     if (
-boundedC
+c
 ontext =
@@ -2787,32 +2787,25 @@
 ommandBus = 
-boundedC
+c
 ontext.comma
@@ -3450,24 +3450,17 @@
 ontext%3E 
-boundedC
+c
 ontexts 
@@ -3632,32 +3632,25 @@
 ndedContext 
-boundedC
+c
 ontext) %7B%0A  
@@ -3741,32 +3741,25 @@
             
-boundedC
+c
 ontexts.add(
@@ -3758,24 +3758,17 @@
 xts.add(
-boundedC
+c
 ontext);
@@ -3963,32 +3963,25 @@
 ndedContext 
-boundedC
+c
 ontext) %7B%0A  
@@ -3990,24 +3990,17 @@
         
-boundedC
+c
 ontexts.
@@ -4006,24 +4006,17 @@
 .remove(
-boundedC
+c
 ontext);
@@ -4187,24 +4187,17 @@
  @param 
-boundedC
+c
 ontext t
@@ -4370,32 +4370,25 @@
 ndedContext 
-boundedC
+c
 ontext) %7B%0A  
@@ -4412,32 +4412,25 @@
  contains = 
-boundedC
+c
 ontexts.cont
@@ -4434,24 +4434,17 @@
 ontains(
-boundedC
+c
 ontext);
@@ -5130,24 +5130,17 @@
 ntext : 
-boundedC
+c
 ontexts)
@@ -5458,32 +5458,25 @@
 ndedContext 
-boundedC
+c
 ontext,%0A    
@@ -5608,24 +5608,17 @@
 ndBus = 
-boundedC
+c
 ontext.c
@@ -5813,24 +5813,17 @@
 dClass, 
-boundedC
+c
 ontext);

Improve names