/*
 * Copyright 2017, TeamDev Ltd. 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 org.spine3.server.aggregate;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.Maps;
import com.google.protobuf.Message;
import org.spine3.server.BoundedContext;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.Map;

import static com.google.common.base.Preconditions.checkNotNull;
import static org.spine3.server.aggregate.AggregatePartRepositoryLookup.createLookup;

/**
 * A root object for a larger aggregate.
 *
 * @param <I> the type for IDs of this class of aggregates
 * @author Alexander Yevsyukov
 */
public class AggregateRoot<I> {

    /** The map from a part class to a repository which manages corresponding aggregate part */
    private static final Map<Class<? extends Message>,
                             AggregatePartRepository<?, ?, ?>> partsAccess =
                                                               Maps.newConcurrentMap();

    /** The bounded context to which the aggregate belongs. */
    private final BoundedContext boundedContext;

    /** The aggregate ID. */
    private final I id;

    /**
     * Creates an new instance.
     *
     * @param boundedContext the bounded context to which the aggregate belongs
     * @param id             the ID of the aggregate
     */
    protected AggregateRoot(BoundedContext boundedContext, I id) {
        checkNotNull(boundedContext);
        checkNotNull(id);
        this.boundedContext = boundedContext;
        this.id = id;
    }

    /**
     * Creates a new {@code AggregateRoot} entity and sets it to the default state.
     *
     * @param id        the ID of the {@code AggregatePart} is managed by {@code AggregateRoot}
     * @param ctx       the {@code BoundedContext} to use
     * @param rootClass the class of the {@code AggregateRoot}
     * @param <I>       the type of entity IDs
     * @return an {@code AggregateRoot} instance
     */
    static <I, R extends AggregateRoot<I>> R create(I id,
                                                    BoundedContext ctx,
                                                    Class<R> rootClass) {
        checkNotNull(id);
        checkNotNull(ctx);
        checkNotNull(rootClass);

        try {
            final Constructor<R> rootConstructor =
                    rootClass.getDeclaredConstructor(ctx.getClass(), id.getClass());
            rootConstructor.setAccessible(true);
            final R root = rootConstructor.newInstance(ctx, id);
            return root;
        } catch (NoSuchMethodException | InvocationTargetException |
                InstantiationException | IllegalAccessException e) {
            throw new IllegalStateException(e);
        }
    }

    /**
     * Obtains the aggregate ID.
     */
    public I getId() {
        return this.id;
    }

    /**
     * Obtains the {@code BoundedContext} to which the aggregate belongs.
     */
    private BoundedContext getBoundedContext() {
        return boundedContext;
    }

    /**
     * Obtains a part state by its class.
     *
     * @param partStateClass the class of the state of the part
     * @param <S>            the type of the part state
     * @return the state of the part or a default state if the state was not found
     * @throws IllegalStateException if a repository was not found,
     *                               or the ID type of the part state does not match
     *                               the ID type of this {@code AggregateRoot}
     */
    protected <S extends Message, A extends AggregatePart<I, S, ?, ?>> S
    getPartState(Class<S> partStateClass) {
        final AggregatePartRepository<I, A, ?> repo = getRepository(partStateClass);
        final AggregatePart<I, S, ?, ?> aggregatePart = repo.loadOrCreate(getId());
        final S partState = aggregatePart.getState();
        return partState;
    }

    /**
     * Obtains a repository for the passed state class.
     *
     * @throws IllegalStateException if a repository was not found,
     *                               or the repository ID type does not match
     *                               the ID type of this {@code AggregateRoot}
     */
    private <S extends Message, A extends AggregatePart<I, S, ?, ?>>
    AggregatePartRepository<I, A, ?> getRepository(Class<S> stateClass) {
        @SuppressWarnings("unchecked") // We ensure ID type when adding to the map.
        final AggregatePartRepository<I, A, ?> cached =
                (AggregatePartRepository<I, A, ?>) partsAccess.get(stateClass);

        if (cached != null) {
            return cached;
        }

        // We don't have a cached instance. Obtain from the `BoundedContext` and cache.
        final AggregatePartRepository<I, A, ?> repo = lookup(stateClass);

        partsAccess.put(stateClass, repo);

        return repo;
    }

    @VisibleForTesting
    <S extends Message, A extends AggregatePart<I, S, ?, ?>>
    AggregatePartRepository<I, A, ?> lookup(Class<S> stateClass) {
        @SuppressWarnings("unchecked") // The type is ensured by getId() result.
        final Class<I> idClass = (Class<I>) getId().getClass();
        final AggregatePartRepositoryLookup<I, S> lookup = createLookup(getBoundedContext(),
                                                                        idClass,
                                                                        stateClass);
        final AggregatePartRepository<I, A, ?> result = lookup.find();
        return result;
    }
}

@@ -2593,48 +2593,8 @@
 oot%7D
- entity and sets it to the default state
 .%0A  
@@ -2608,30 +2608,35 @@
    * @param 
-id    
+aggregateId
     the ID o
@@ -2645,60 +2645,17 @@
 the 
-%7B@code AggregatePart%7D is managed by %7B@code A
+a
 ggregate
 Root
@@ -2642,37 +2642,32 @@
 of the aggregate
-Root%7D
 %0A     * @param c
@@ -2669,17 +2669,22 @@
 ram 
-ctx      
+boundedContext
  the
@@ -2734,24 +2734,29 @@
 m rootClass 
+     
 the class of
@@ -2802,24 +2802,29 @@
 m %3CI%3E       
+     
 the type of 
@@ -2853,32 +2853,11 @@
 urn 
-an %7B@code AggregateRoot%7D
+new
  ins
@@ -2924,17 +2924,26 @@
 reate(I 
-i
+aggregateI
 d,%0A     
@@ -3004,19 +3004,30 @@
 Context 
-ctx
+boundedContext
 ,%0A      
@@ -3107,33 +3107,42 @@
    checkNotNull(
-i
+aggregateI
 d);%0A        chec
@@ -3150,19 +3150,30 @@
 NotNull(
-ctx
+boundedContext
 );%0A     
@@ -3323,25 +3323,98 @@
 tor(
-ctx.getClass(), i
+boundedContext.getClass(),%0A                                                     aggregateI
 d.ge
@@ -3532,14 +3532,34 @@
 nce(
-ctx, i
+boundedContext, aggregateI
 d);%0A
@@ -4640,15 +4640,15 @@
  ?%3E%3E
- S
 %0A    
+S 
 getP

Fix Javadoc