/*
 * 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.base.MoreObjects;
import io.spine.base.EntityState;
import io.spine.option.EntityOption.Visibility;
import io.spine.server.entity.EntityVisibility;
import io.spine.server.entity.Repository;
import io.spine.server.entity.model.EntityClass;
import io.spine.type.TypeName;
import io.spine.type.TypeUrl;

import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.Set;

import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static io.spine.util.Exceptions.newIllegalStateException;

/**
 * A registry of repositories that controls access to them depending on the visibility of
 * corresponding entity states.
 */
final class VisibilityGuard {

    private final Map<Class<? extends EntityState>, RepositoryAccess> repositories
            = new HashMap<>();

    /** Prevent instantiation from outside. */
    private VisibilityGuard() {
    }

    /**
     * Creates a new instance of the guard.
     */
    static VisibilityGuard newInstance() {
        return new VisibilityGuard();
    }

    /**
     * Registers the passed repository with the guard.
     */
    void register(Repository<?, ?> repository) {
        checkNotNull(repository);
        EntityClass<?> entityClass = repository.entityModelClass();
        Class<? extends EntityState> stateClass = entityClass.stateClass();
        checkNotAlreadyRegistered(stateClass);
        repositories.put(stateClass, new RepositoryAccess(repository));
    }

    private void checkNotAlreadyRegistered(Class<? extends EntityState> stateClass) {
        RepositoryAccess alreadyRegistered = repositories.get(stateClass);
        if (alreadyRegistered != null) {
            throw newIllegalStateException(
                    "A repository for the state class %s already registered: `%s`.",
                    stateClass.getName(),
                    alreadyRegistered
            );
        }
    }

    /**
     * Verifies if there is a registered repository for the passed entity state class.
     */
    boolean hasRepository(Class<? extends EntityState> stateClass) {
        checkNotNull(stateClass);
        boolean result = repositories.containsKey(stateClass);
        return result;
    }

    /**
     * Obtains the repository for the passed entity state class.
     *
     * @param stateClass
     *         the class of the state of entities managed by the repository
     * @return the repository wrapped into {@code Optional} or {@code Optional#empty()} if the
     *         entity state is {@linkplain Visibility#NONE not visible}
     * @throws IllegalArgumentException
     *         if the repository for the passed state class was not
     *         {@linkplain #register(Repository) registered} with the guard
     *         prior to this call, or if all repositories were
     *         {@linkplain #shutDownRepositories() shut down}
     */
    Optional<Repository> repositoryFor(Class<? extends EntityState> stateClass) {
        checkNotNull(stateClass);
        RepositoryAccess repositoryAccess = findOrThrow(stateClass);
        return repositoryAccess.get();
    }

    Optional<Repository> repositoryFor(TypeUrl stateType) {
        checkNotNull(stateType);
        RepositoryAccess repositoryAccess = findOrThrow(stateType);
        return repositoryAccess.get();
    }

    private RepositoryAccess findOrThrow(Class<? extends EntityState> stateClass) {
        RepositoryAccess repository = repositories.get(stateClass);
        if (repository == null) {
            throw newIllegalStateException(
                    "A repository for the state class `%s` is not registered.",
                    stateClass.getName()
            );
        }
        return repository;
    }

    private RepositoryAccess findOrThrow(TypeUrl stateType) {
        Optional<RepositoryAccess> result =
                repositories.values()
                            .stream()
                            .filter((r) -> r.repository.entityStateType()
                                                       .equals(stateType))
                            .findFirst();
        if (!result.isPresent()) {
            throw newIllegalStateException(
                    "A repository for the state type `%s` is not registered.",
                    stateType
            );
        }
        return result.get();
    }

    /**
     * Obtains a set of entity type names by their visibility.
     */
    public Set<TypeName> entityStateTypes(Visibility visibility) {
        checkNotNull(visibility);
        Set<TypeName> result = repositories.values()
                                           .stream()
                                           .filter(access -> access.visibility.is(visibility))
                                           .map(access -> access.repository
                                                   .entityStateType()
                                                   .toTypeName())
                                           .collect(toImmutableSet());
        return result;
    }

    Set<TypeName> allEntityTypes() {
        Set<TypeName> result = repositories.values()
                                           .stream()
                                           .map(access -> access.repository
                                                   .entityStateType()
                                                   .toTypeName())
                                           .collect(toImmutableSet());
        return result;
    }

    /**
     * Closes all registered repositories and clears the registration list.
     */
    void shutDownRepositories() {
        for (RepositoryAccess repositoryAccess : repositories.values()) {
            repositoryAccess.repository.close();
        }
        repositories.clear();
    }

    boolean isClosed() {
        return repositories.isEmpty();
    }

    /**
     * Allows to get a reference to repository if states of its entities are visible.
     */
    private static class RepositoryAccess {

        private final Repository repository;
        private final EntityVisibility visibility;

        private RepositoryAccess(Repository repository) {
            this.repository = repository;
            EntityClass entityClass = repository.entityModelClass();
            this.visibility = entityClass.visibility();
        }

        private Optional<Repository> get() {
            return visibility.isNotNone()
                   ? Optional.of(repository)
                   : Optional.empty();
        }

        @Override
        public String toString() {
            return MoreObjects.toStringHelper(this)
                              .add("repository", repository)
                              .add("visibility", visibility)
                              .toString();
        }
    }
}

@@ -1330,38 +1330,8 @@
 ame;
-%0Aimport io.spine.type.TypeUrl;
 %0A%0Aim
@@ -4202,215 +4202,8 @@
  %7D%0A%0A
-    Optional%3CRepository%3E repositoryFor(TypeUrl stateType) %7B%0A        checkNotNull(stateType);%0A        RepositoryAccess repositoryAccess = findOrThrow(stateType);%0A        return repositoryAccess.get();%0A    %7D%0A%0A
     
@@ -4612,630 +4612,8 @@
  %7D%0A%0A
-    private RepositoryAccess findOrThrow(TypeUrl stateType) %7B%0A        Optional%3CRepositoryAccess%3E result =%0A                repositories.values()%0A                            .stream()%0A                            .filter((r) -%3E r.repository.entityStateType()%0A                                                       .equals(stateType))%0A                            .findFirst();%0A        if (!result.isPresent()) %7B%0A            throw newIllegalStateException(%0A                    %22A repository for the state type %60%25s%60 is not registered.%22,%0A                    stateType%0A            );%0A        %7D%0A        return result.get();%0A    %7D%0A%0A
     

Remove an unused method.