/*
 * Copyright 2019, 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.model;

import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableSetMultimap;
import com.google.common.collect.Multimap;
import io.spine.base.FieldPath;
import io.spine.type.MessageClass;

import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

import static io.spine.validate.Validate.isNotDefault;

/**
 * A class method scan operation.
 *
 * <p>Finds handler methods in the given class by the given {@link MethodSignature}.
 *
 * @param <H>
 *         the type of handler method to find
 */
final class MethodScan<H extends HandlerMethod<?, ?, ?, ?>> {

    private final Class<?> declaringClass;
    private final MethodSignature<H, ?> signature;
    private final Multimap<DispatchKey, H> handlers;
    private final Map<MethodParams, H> seenMethods;
    private final Map<MessageClass, SelectiveHandler> selectiveHandlers;

    /**
     * Finds handler methods in the scanned class by the given signature.
     *
     * @param <H>
     *         the type of the handler methods
     * @param declaringClass
     *         the scanned class
     * @param signature
     *         the handler {@linkplain MethodSignature signature}
     * @return map of {@link HandlerTypeInfo}s to the handler methods of the given type
     */
    static <H extends HandlerMethod<?, ?, ?, ?>> ImmutableSetMultimap<DispatchKey, H>
    findMethodsBy(Class<?> declaringClass, MethodSignature<H, ?> signature) {
        MethodScan<H> operation = new MethodScan<>(declaringClass, signature);
        ImmutableSetMultimap<DispatchKey, H> result = operation.perform();
        return result;
    }

    private MethodScan(Class<?> declaringClass, MethodSignature<H, ?> signature) {
        this.declaringClass = declaringClass;
        this.signature = signature;
        this.handlers = HashMultimap.create();
        this.seenMethods = new HashMap<>();
        this.selectiveHandlers = new HashMap<>();
    }

    /**
     * Performs the operation.
     *
     * <p>Multiple calls to this method may cause {@link DuplicateHandlerMethodError}s.
     */
    private ImmutableSetMultimap<DispatchKey, H> perform() {
        Method[] declaredMethods = declaringClass.getDeclaredMethods();
        for (Method method : declaredMethods) {
            scanMethod(method);
        }
        return ImmutableSetMultimap.copyOf(handlers);
    }

    private void scanMethod(Method method) {
        Optional<H> handlerMethod = signature.classify(method);
        if (handlerMethod.isPresent()) {
            H handler = handlerMethod.get();
            remember(handler);
        }
    }

    private void remember(H handler) {
        checkNotRemembered(handler);
        if (handler instanceof SelectiveHandler) {
            checkFilteringNotClashes((SelectiveHandler) handler);
        }
        handlers.put(handler.key(), handler);
    }

    private void checkNotRemembered(H handler) {
        MethodParams params = handler.params();
        if (seenMethods.containsKey(params)) {
            Method alreadyPresent = seenMethods.get(params)
                                               .rawMethod();
            String methodName = alreadyPresent.getName();
            String duplicateMethodName = handler.rawMethod().getName();
            throw new DuplicateHandlerMethodError(
                    declaringClass, params, methodName, duplicateMethodName
            );
        } else {
            seenMethods.put(params.withoutContextAndFilter(), handler);
        }
    }

    private void checkFilteringNotClashes(SelectiveHandler handler) {
        MessageClass handledClass = handler.messageClass();
        FieldPath field = handler.filter().field();
        if (!isNotDefault(field)) {
            return;
        }
        SelectiveHandler existingHandler = selectiveHandlers.put(handledClass, handler);
        if (existingHandler != null) {
            // There is already a handler for this message class.
            // See that the field which is used as the condition for filtering is the same.
            // It is not allowed to have filtered handlers by various fields because it
            // makes the dispatching ambiguous: "Do we need to dispatch to this this handler
            // and that handler too?"
            //
            // We allow multiple handlers for the same message type with filters by
            // different values. It allows to split logic into smaller methods instead of having
            // if-else chains (that branch by different values) inside a bigger handler method.
            //
            FieldPath prevHandlerField = existingHandler.filter().field();
            boolean fieldDiffers = !prevHandlerField.equals(field);
            if (fieldDiffers) {
                throw new HandlerFieldFilterClashError(declaringClass,
                                                       handler.rawMethod(),
                                                       existingHandler.rawMethod());
            }
            // It is OK to keep only the last filtering handler in the map (and not all of them)
            // because filtered fields are required to be the same.
        }
    }
}

@@ -1841,28 +1841,27 @@
 nal Map%3C
-MethodParams
+DispatchKey
 , H%3E see
@@ -3993,27 +3993,23 @@
     
-MethodParams params
+DispatchKey key
  = h
@@ -4015,22 +4015,19 @@
 handler.
-params
+key
 ();%0A    
@@ -4058,22 +4058,19 @@
 ainsKey(
-params
+key
 )) %7B%0A   
@@ -4118,22 +4118,19 @@
 ods.get(
-params
+key
 )%0A      
@@ -4401,22 +4401,19 @@
 gClass, 
-params
+key
 , method
@@ -4502,40 +4502,11 @@
 put(
-params.withoutContextAndFilter()
+key
 , ha

Use dispatch key for remembering methods