/*
 * 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.core;

import com.google.protobuf.Any;
import com.google.protobuf.Message;
import io.spine.core.Enrichment.Container;
import io.spine.core.Enrichment.ModeCase;
import io.spine.type.TypeName;

import java.util.Deque;
import java.util.Optional;

import static com.google.common.collect.Queues.newArrayDeque;
import static io.spine.protobuf.AnyPacker.unpack;
import static io.spine.util.Exceptions.newIllegalStateException;

/**
 * Utility class for working with event enrichments.
 */
final class Enrichments {

    /** Prevents instantiation of this utility class. */
    private Enrichments() {
    }

    /**
     * Obtains the container of enrichments from the passed enclosing instance,
     * if it its {@link ModeCase} allows for having enrichments.
     *
     * <p>Otherwise, empty {@code Optional} is returned.
     */
    static Optional<Container> containerIn(EnrichableMessageContext context) {
        Enrichment enrichment = context.getEnrichment();
        if (enrichment.getModeCase() == ModeCase.CONTAINER) {
            return Optional.of(enrichment.getContainer());
        }
        return Optional.empty();
    }

    /**
     * Obtains enrichment from the passed container.
     */
    static <E extends Message>
    Optional<E> find(Class<E> enrichmentClass, Container container) {
        TypeName typeName = TypeName.of(enrichmentClass);
        Optional<E> result = findType(typeName, enrichmentClass, container);
        return result;
    }

    /**
     * Obtains enrichment of the passed class from the container.
     *
     * @throws IllegalStateException if there is no enrichment of this class in the passed container
     */
    static <E extends Message> E get(Class<E> enrichmentClass, Container container) {
        TypeName typeName = TypeName.of(enrichmentClass);
        E result = findType(typeName, enrichmentClass, container)
                .orElseThrow(() -> newIllegalStateException(
                        "Unable to get enrichment of the type `%s` from the container `%s`.",
                        typeName, container));
        return result;
    }

    private static <E extends Message> Optional<E>
    findType(TypeName typeName, Class<E> enrichmentClass, Container container) {
        Any any = container.getItemsMap()
                           .get(typeName.value());
        Optional<E> result = Optional.ofNullable(any)
                                     .map(packed -> unpack(packed, enrichmentClass));
        return result;
    }

    /**
     * Clears the enrichments from the {@code event} and its origin.
     */
    @SuppressWarnings("deprecation") // Uses the deprecated field to be sure to clean up old data.
    static Event clear(Event event) {
        EventContext context = event.getContext();
        EventContext.OriginCase originCase = context.getOriginCase();
        EventContext.Builder resultContext = context.toBuilder()
                                                    .clearEnrichment();
        if (originCase == EventContext.OriginCase.EVENT_CONTEXT) {
            resultContext.setEventContext(context.getEventContext()
                                                 .toBuilder()
                                                 .clearEnrichment()
                                                 .build());
        }
        Event result = event.toBuilder()
                            .setContext(resultContext.build())
                            .build();
        return result;
    }

    /**
     * Clears the enrichments from the {@code event} and all of its origin hierarchy.
     */
    @SuppressWarnings({
            "deprecation" /* Uses the deprecated field to be sure to clean up old data. */,
            "ConstantConditions" /* Checked logically. */})
    static Event clearAll(Event event) {
        EventContext.Builder eventContext = event.getContext()
                                                 .toBuilder()
                                                 .clearEnrichment();
        Deque<EventContext.Builder> contexts = eventContextHierarchy(eventContext);

        EventContext.Builder context = contexts.pollLast();
        EventContext.Builder next = contexts.pollLast();
        while (next != null) {
            context = next.setEventContext(context);
            next = contexts.pollLast();
        }
        Event result = event.toBuilder()
                            .setContext(context.build())
                            .build();
        return result;
    }

    /**
     * Traverses the event origin hierarchy until non-{@link EventContext} origin is found.
     *
     * <p>All of the {@link EventContext}-kind origins are collected and returned as
     * {@code Deque<EventContext.Builder>}.
     */
    @SuppressWarnings("deprecation") // Uses the deprecated field to be sure to clean up old data.
    private static Deque<EventContext.Builder>
    eventContextHierarchy(EventContext.Builder eventContext) {

        EventContext.Builder child = eventContext;
        EventContext.OriginCase originCase = child.getOriginCase();
        Deque<EventContext.Builder> contexts = newArrayDeque();
        contexts.add(child);
        while (originCase == EventContext.OriginCase.EVENT_CONTEXT) {
            EventContext.Builder origin = child.getEventContext()
                                               .toBuilder()
                                               .clearEnrichment();
            contexts.add(origin);
            child = origin;
            originCase = child.getOriginCase();
        }
        return contexts;
    }
}

@@ -5755,16 +5755,55 @@
 uilder%3E%7D
+, where the deepest origin resides last
 .%0A     *

Clarify the doc more