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

import com.google.common.base.Objects;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.ImmutableList;
import com.google.errorprone.annotations.Immutable;
import io.spine.code.proto.FieldContext;
import io.spine.type.MessageType;
import io.spine.validate.option.RequiredField;

import java.util.stream.Stream;

import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static io.spine.util.Exceptions.illegalStateWithCauseOf;
import static io.spine.validate.FieldConstraints.customFactoriesExist;

/**
 * Validation constraints of a single Protobuf message type.
 */
@Immutable
public final class Constraints {

    private static final int CACHE_SIZE = 1000;

    private static final LoadingCache<CacheKey, Constraints> allConstraints = CacheBuilder
            .newBuilder()
            .maximumSize(CACHE_SIZE)
            .build(new ConstraintCacheLoader());
    private static final LoadingCache<CacheKey, Constraints> customConstraints = CacheBuilder
            .newBuilder()
            .maximumSize(CACHE_SIZE)
            .build(new CustomConstraintCacheLoader());

    private final ImmutableList<Constraint> constraints;

    private Constraints(ImmutableList<Constraint> constraints) {
        this.constraints = constraints;
    }

    /**
     * Assembles constraints from the given message type.
     */
    public static Constraints of(MessageType type) {
        return of(type, FieldContext.empty());
    }

    /**
     * Assembles constraints from the given message type in the given field context.
     *
     * <p>The field context is not empty if the constraints must consider messages values of a field
     * rather than independent messages.
     */
    public static Constraints of(MessageType type, FieldContext context) {
        checkNotNull(type);
        checkNotNull(context);
        CacheKey key = new CacheKey(type, context);
        return fromCache(allConstraints, key);
    }

    /**
     * Obtains custom {@link Constraint}s for the given type in the given context.
     *
     * @param type
     * @param context
     * @return
     */
    static Constraints onlyCustom(MessageType type, FieldContext context) {
        checkNotNull(type);
        checkNotNull(context);
        CacheKey key = new CacheKey(type, context);
        return fromCache(customConstraints, key);
    }

    private static Constraints
    fromCache(LoadingCache<CacheKey, Constraints> constraints, CacheKey key) {
        try {
            return constraints.get(key);
        } catch (@SuppressWarnings("OverlyBroadCatchBlock")
                 // `get(..)` Can throw checked and unchecked Exceptions and Errors.
                 Throwable e) {
            throw illegalStateWithCauseOf(e);
        }
    }

    /**
     * Feeds these constraints to the given {@link ConstraintTranslator} and obtains the result of
     * translation.
     *
     * @param constraintTranslator
     *         the {@code ConstraintTranslator} which reduces the constrains to
     *         a value of {@code T}
     * @param <T>
     *         type of the translation result
     * @return the translation result
     */
    public <T> T runThrough(ConstraintTranslator<T> constraintTranslator) {
        constraints.forEach(c -> c.accept(constraintTranslator));
        return constraintTranslator.translate();
    }

    private static final class CacheKey {

        private final MessageType type;
        private final FieldContext fieldContext;

        private CacheKey(MessageType type, FieldContext context) {
            this.type = checkNotNull(type);
            this.fieldContext = checkNotNull(context);
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) {
                return true;
            }
            if (!(o instanceof CacheKey)) {
                return false;
            }
            CacheKey key = (CacheKey) o;
            return Objects.equal(type, key.type) &&
                    Objects.equal(fieldContext, key.fieldContext);
        }

        @Override
        public int hashCode() {
            return Objects.hashCode(type, fieldContext);
        }
    }

    private static final class ConstraintCacheLoader extends CacheLoader<CacheKey, Constraints> {

        @Override
        public Constraints load(CacheKey key) {
            ImmutableList.Builder<Constraint> constraintBuilder = ImmutableList.builder();
            fieldConstraints(key.type, key.fieldContext)
                    .forEach(constraintBuilder::add);
            addRequiredField(key.type, constraintBuilder);
            return new Constraints(constraintBuilder.build());
        }

        private static Stream<Constraint> fieldConstraints(MessageType type, FieldContext context) {
            return type
                    .fields()
                    .stream()
                    .map(field -> context.forChild(field.descriptor()))
                    .flatMap(FieldConstraints::of);
        }

        private static void addRequiredField(MessageType type,
                                             ImmutableList.Builder<Constraint> constraintBuilder) {
            RequiredField requiredField = new RequiredField();
            if (requiredField.valuePresent(type.descriptor())) {
                Constraint requiredFieldConstraint = requiredField.constraintFor(type);
                constraintBuilder.add(requiredFieldConstraint);
            }
        }
    }

    private static final class CustomConstraintCacheLoader extends CacheLoader<CacheKey, Constraints> {

        @Override
        public Constraints load(CacheKey key) {
            ImmutableList<Constraint> constraintBuilder =
                    customFieldConstraints(key.type, key.fieldContext)
                            .collect(toImmutableList());
            return new Constraints(constraintBuilder);
        }

        private static Stream<Constraint>
        customFieldConstraints(MessageType type, FieldContext context) {
            return customFactoriesExist()
                   ? type.fields()
                         .stream()
                         .map(field -> context.forChild(field.descriptor()))
                         .flatMap(FieldConstraints::customConstraintsFor)
                   : Stream.of();
        }
    }
}

@@ -3174,31 +3174,32 @@
   * 
-Obtains custom %7B@link C
+Assembles non-standard c
 onst
@@ -3207,14 +3207,14 @@
 aint
-%7D
 s f
-o
 r
+om
  the
@@ -3220,16 +3220,24 @@
 e given 
+message 
 type in 
@@ -3246,24 +3246,30 @@
 e given 
+field 
 context.
 %0A     *%0A
@@ -3264,71 +3264,8 @@
 ext.
-%0A     *%0A     * @param type%0A     * @param context%0A     * @return
 %0A   
@@ -5328,32 +5328,116 @@
        %7D%0A    %7D%0A%0A
+    /**%0A     * Loads a cache of all constraints per type per field context.%0A     */%0A
     private stat
@@ -6706,32 +6706,125 @@
        %7D%0A    %7D%0A%0A
+    /**%0A     * Loads a cache of non-standard constraints per type per field context.%0A     */%0A
     private stat
@@ -6857,32 +6857,44 @@
 raintCacheLoader
+%0A           
  extends CacheLo

Add doc