/*
 * Copyright 2018, 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.event;

import com.google.common.base.Optional;
import com.google.common.base.Predicate;
import com.google.common.collect.Collections2;
import com.google.protobuf.Any;
import com.google.protobuf.Message;
import io.spine.base.FieldFilter;
import io.spine.core.Event;
import io.spine.core.EventContext;
import io.spine.core.Events;
import io.spine.server.reflect.Field;
import io.spine.type.TypeName;
import io.spine.type.TypeUrl;
import org.checkerframework.checker.nullness.qual.Nullable;

import java.util.Collection;
import java.util.List;

import static io.spine.protobuf.AnyPacker.unpackFunc;

/**
 * The predicate for filtering events by {@link EventFilter}.
 *
 * @author Dmytro Dashenkov
 * @author Alexander Yevsyukov
 */
class MatchFilter implements Predicate<Event> {

    /**
     * The type URL of events to accept.
     *
     * <p>If null, all events are accepted.
     */
    @Nullable
    private final TypeUrl eventTypeUrl;

    /**
     * The list of aggregate IDs of which events to accept.
     *
     * <p>If null, all IDs are accepted.
     */
    @Nullable
    private final List<Any> aggregateIds;

    private final Collection<FieldFilter> eventFieldFilters;
    private final Collection<FieldFilter> contextFieldFilters;

    MatchFilter(EventFilter filter) {
        this.eventTypeUrl = getEventTypeUrl(filter);
        this.aggregateIds = getAggregateIdentifiers(filter);
        this.eventFieldFilters = filter.getEventFieldFilterList();
        this.contextFieldFilters = filter.getContextFieldFilterList();
    }

    @Nullable
    private static TypeUrl getEventTypeUrl(EventFilter filter) {
        final String eventType = filter.getEventType();
        final TypeUrl result = eventType.isEmpty()
                               ? null
                               : TypeName.of(eventType)
                                         .toUrl();
        return result;
    }

    @Nullable
    private static List<Any> getAggregateIdentifiers(EventFilter filter) {
        final List<Any> aggregateIdList = filter.getAggregateIdList();
        final List<Any> result = aggregateIdList.isEmpty()
                            ? null
                            : aggregateIdList;
        return result;
    }

    @SuppressWarnings("MethodWithMoreThanThreeNegations") // OK as we want traceability of exits.
    @Override
    public boolean apply(@Nullable Event event) {
        if (event == null) {
            return false;
        }

        final Message message = Events.getMessage(event);
        final EventContext context = event.getContext();

        if (!checkEventType(message)) {
            return false;
        }

        if (!checkAggregateIds(context)) {
            return false;
        }

        if (!checkEventFields(message)) {
            return false;
        }

        final boolean result = checkContextFields(context);
        return result;
    }

    private boolean checkAggregateIds(EventContext context) {
        if (aggregateIds == null) {
            return true;
        }
        final Any aggregateId = context.getProducerId();
        final boolean result = aggregateIds.contains(aggregateId);
        return result;
    }

    private boolean checkEventType(Message message) {
        final TypeUrl actualTypeUrl = TypeUrl.of(message);
        if (eventTypeUrl == null) {
            return true;
        }
        final boolean result = actualTypeUrl.equals(eventTypeUrl);
        return result;
    }

    private boolean checkContextFields(EventContext context) {
        for (FieldFilter filter : contextFieldFilters) {
            final boolean matchesFilter = checkFields(context, filter);
            if (!matchesFilter) {
                return false;
            }
        }
        return true;
    }

    private boolean checkEventFields(Message message) {
        for (FieldFilter filter : eventFieldFilters) {
            final boolean matchesFilter = checkFields(message, filter);
            if (!matchesFilter) {
                return false;
            }
        }
        return true;
    }

    private static boolean checkFields(Message object, FieldFilter filter) {
        final Optional<Field> fieldOptional = Field.forFilter(object.getClass(), filter);
        if (!fieldOptional.isPresent()) {
            return false;
        }

        final Field field = fieldOptional.get();
        final Optional<Message> value;

        try {
            value = field.getValue(object);
        } catch (IllegalStateException ignored) {
            // Wrong Message class -> does not satisfy the criteria.
            return false;
        }

        final Collection<Any> expectedAnys = filter.getValueList();
        final Collection<Message> expectedValues =
                Collections2.transform(expectedAnys, unpackFunc());

        if (!value.isPresent()) {
            /* If there is no value in the field return `true`
               if the list of required values is also empty. */
            final boolean nothingIsExpected = expectedValues.isEmpty();
            return nothingIsExpected;
        }

        final boolean result = expectedValues.contains(value.get());
        return result;
    }
}

@@ -1123,55 +1123,8 @@
 te;%0A
-import com.google.common.collect.Collections2;%0A
 impo
@@ -1151,16 +1151,16 @@
 uf.Any;%0A
+
 import c
@@ -1579,16 +1579,66 @@
 ackFunc;
+%0Aimport static java.util.stream.Collectors.toList;
 %0A%0A/**%0A *
@@ -1916,38 +1916,24 @@
 ed.%0A     */%0A
-    @Nullable%0A
     private 
@@ -1934,24 +1934,34 @@
 ivate final 
+@Nullable 
 TypeUrl even
@@ -2095,30 +2095,16 @@
      */%0A
-    @Nullable%0A
     priv
@@ -2109,24 +2109,34 @@
 ivate final 
+@Nullable 
 List%3CAny%3E ag
@@ -2566,38 +2566,24 @@
 t();%0A    %7D%0A%0A
-    @Nullable%0A
     private 
@@ -2589,16 +2589,26 @@
  static 
+@Nullable 
 TypeUrl 
@@ -2931,22 +2931,8 @@
  %7D%0A%0A
-    @Nullable%0A
     
@@ -2946,16 +2946,26 @@
  static 
+@Nullable 
 List%3CAny
@@ -5639,33 +5639,32 @@
 alse;%0A        %7D%0A
-%0A
         final Co
@@ -5672,19 +5672,23 @@
 lection%3C
-Any
+Message
 %3E expect
@@ -5689,19 +5689,21 @@
 expected
-Any
+Value
 s = filt
@@ -5715,25 +5715,24 @@
 tValueList()
-;
 %0A        fin
@@ -5732,119 +5732,219 @@
     
-final Collection%3CMessage%3E expectedValues =%0A                Collections2.transform(expectedAnys, unpackFunc());%0A
+                                                 .stream()%0A                                                         .map(unpackFunc())%0A                                                         .collect(toList());
 %0A   

Fix a compilation error.