/*
 * 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.collect.ImmutableList;
import com.google.protobuf.Descriptors;
import io.spine.option.OnDuplicate;
import io.spine.option.OnDuplicate.DuplicatePolicy;
import io.spine.option.OptionsProto;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;

/**
 * An option that can be applied to {@code repeated} Protobuf fields to specify that values
 * represented by that {@code repeated} field don't contain duplicates.
 */
final class DistinctFieldOption extends FieldValidatingOption<OnDuplicate.DuplicatePolicy> {

    @Override
    boolean applicableTo(Descriptors.FieldDescriptor field) {
        return field.isRepeated();
    }

    @Override
    ValidationException onInapplicable(Descriptors.FieldDescriptor field) {
        ConstraintViolation inapplicable = ConstraintViolation
                .newBuilder()
                .setMsgFormat("Error for field %s. A non-repeated field cannot be `distinct`.")
                .addParam(field.getFullName())
                .build();
        ValidationException exception = new ValidationException(ImmutableList.of(inapplicable));
        return exception;
    }

    @Override
    List<ConstraintViolation> applyValidatingRules(FieldValue value) {
        return checkForDuplicates(value);
    }

    @Override
    boolean optionPresentFor(FieldValue value) {
        return policyFor(value).getNumber() > 0;
    }

    @SuppressWarnings("SuspiciousMethodCalls")
    private static List<ConstraintViolation> checkForDuplicates(FieldValue value) {
        List<?> potentialDuplicates = new ArrayList<>(value.asList());
        List<ConstraintViolation> violations =
                potentialDuplicates.stream()
                                   .distinct()
                                   .filter(element -> Collections.frequency(potentialDuplicates,
                                                                            element) > 1)
                                   .map(element -> duplicateFound(element, value))
                                   .collect(Collectors.toList());
        return violations;
    }

    private static <T> ConstraintViolation duplicateFound(T duplicate, FieldValue value) {
        String fieldName = value.declaration()
                                .name()
                                .value();
        ConstraintViolation duplicateFound = ConstraintViolation
                .newBuilder()
                .setMsgFormat("Found a duplicate element in a `distinct` field %s. " +
                              "Duplicate element: %s.")
                .addParam(fieldName)
                .addParam(value.asList().toString())
                .addParam(duplicate.toString())
                .build();
        return duplicateFound;
    }

    private static DuplicatePolicy policyFor(FieldValue field) {
        return field.valueOf(OptionsProto.onDuplicate)
                    .getDuplicatePolicy();
    }

    @Override
    public DuplicatePolicy getValueFor(FieldValue something) {
        return something.valueOf(OptionsProto.onDuplicate).getDuplicatePolicy();
    }
}

@@ -1078,24 +1078,71 @@
 utableList;%0A
+import com.google.common.collect.ImmutableSet;%0A
 import com.g
@@ -1340,27 +1340,23 @@
 va.util.
-Collections
+HashSet
 ;%0Aimport
@@ -1368,24 +1368,46 @@
 .util.List;%0A
+import java.util.Set;%0A
 import java.
@@ -2764,325 +2764,520 @@
     
-List%3CConstraintViolation%3E violations =%0A                potentialDuplicates.stream()%0A                                   .distinct()%0A                                   .filter(element -%3E Collections.frequency(potentialDuplicates,%0A                                                                            element) %3E 1)
+Set%3C?%3E duplicates = findDuplicates(potentialDuplicates);%0A        return ImmutableList.of(duplicateFound(value, duplicates));%0A    %7D%0A%0A    private static Set%3C?%3E findDuplicates(Iterable%3C?%3E potentialDuplicates) %7B%0A        Set%3CObject%3E duplicateLess = new HashSet%3C%3E();%0A        ImmutableSet.Builder%3CObject%3E duplicates = ImmutableSet.builder();%0A        for (Object potentialDuplicate : potentialDuplicates) %7B%0A            if (duplicateLess.contains(potentialDuplicate)) %7B%0A                duplicates.add(potentialDuplicate);
 %0A   
@@ -3277,39 +3277,41 @@
 e);%0A            
-       
+%7D else %7B%0A
                 
@@ -3314,60 +3314,47 @@
     
-.map(element -%3E duplicateFound(element, value))%0A    
+duplicateLess.add(potentialDuplicate);%0A
     
@@ -3357,32 +3357,34 @@
             
+%7D%0A
         
            .
@@ -3375,75 +3375,43 @@
     
-           .collect(Collectors.toList());%0A        return violations
+%7D%0A        return duplicates.build()
 ;%0A  
@@ -3477,21 +3477,8 @@
 und(
-T duplicate, 
 Fiel
@@ -3481,32 +3481,51 @@
 FieldValue value
+, Set%3CT%3E duplicates
 ) %7B%0A        Stri
@@ -3653,125 +3653,308 @@
     
-Con
+List%3CString%3E 
 str
-a
 in
-tViolation duplicateFound = ConstraintViolation%0A                .newBuilder()%0A                .setMsgFormat(
+gValues = duplicates.stream()%0A                                              .map(Object::toString)%0A                                              .collect(Collectors.toList());%0A        String formatParams = String.join(%22, %22, stringValues);%0A        String msgFormat =%0A                
 %22Fou
@@ -4022,30 +4022,16 @@
         
-         
-     
 %22Duplica
@@ -4044,15 +4044,14 @@
 ment
+s
 : 
-%25s.%22)
+%22 +
 %0A   
@@ -4067,153 +4067,263 @@
     
-.addParam(fieldName)%0A                .addParam(value.asList().toString())%0A                .addParam(duplicate.toString())%0A                .build(
+formatParams;%0A        ConstraintViolation.Builder duplicatesBuilder = ConstraintViolation%0A                .newBuilder()%0A                .setMsgFormat(msgFormat)%0A                .addParam(fieldName);%0A        stringValues.forEach(duplicatesBuilder::addParam
 );%0A 
@@ -4345,21 +4345,32 @@
 uplicate
-Found
+sBuilder.build()
 ;%0A    %7D%0A
@@ -4675,16 +4675,41 @@
 plicate)
+%0A                        
 .getDupl

Improve duplicate lookup performance.