package org.hl7.fhir.r4.validation;

import java.util.*;
import java.util.stream.*;

import org.hl7.fhir.exceptions.FHIRException;
import org.hl7.fhir.r4.elementmodel.Element;
import org.hl7.fhir.r4.model.*;
import org.hl7.fhir.r4.model.Questionnaire.*;

/**
 * Evaluates Questionnaire.item.enableWhen against a QuestionnaireResponse.
 * Ignores possible modifierExtensions and extensions.
 *
 */
public class DefaultEnableWhenEvaluator implements IEnableWhenEvaluator {
    public static final String LINKID_ELEMENT = "linkId";
    public static final String ITEM_ELEMENT = "item";
    public static final String ANSWER_ELEMENT = "answer";

    @Override
    public boolean isQuestionEnabled(QuestionnaireItemComponent questionnaireItem, Element questionnaireResponse) {
        if (!questionnaireItem.hasEnableWhen()) {
            return true;
        }
        List<EnableWhenResult> evaluationResults = questionnaireItem.getEnableWhen()
                .stream()
                .map(enableCondition -> evaluateCondition(enableCondition, questionnaireResponse,
                        questionnaireItem.getLinkId()))
                .collect(Collectors.toList());
        return checkConditionResults(evaluationResults, questionnaireItem);
    }
   
    
    public boolean checkConditionResults(List<EnableWhenResult> evaluationResults,
            QuestionnaireItemComponent questionnaireItem) {        
        if (questionnaireItem.hasEnableBehavior() && questionnaireItem.getEnableBehavior() == EnableWhenBehavior.ANY){
            return evaluationResults.stream().anyMatch(EnableWhenResult::isEnabled);
        } if (questionnaireItem.hasEnableBehavior() && questionnaireItem.getEnableBehavior() == EnableWhenBehavior.ALL){
            return evaluationResults.stream().allMatch(EnableWhenResult::isEnabled);
        }
        //TODO: Throw exception? enableBehavior is mandatory when there are multiple conditions
        return true;
    }


    protected EnableWhenResult evaluateCondition(QuestionnaireItemEnableWhenComponent enableCondition,
            Element questionnaireResponse, String linkId) {
        List<Element> answerItems = findQuestionAnswers(questionnaireResponse,
                enableCondition.getQuestion());        
        QuestionnaireItemOperator operator = enableCondition.getOperator();
        if (operator == QuestionnaireItemOperator.EXISTS){
            Type answer = enableCondition.getAnswer();
            if (!(answer instanceof BooleanType)){
                throw new RuntimeException("Exists-operator requires answerBoolean");                
            }
            return new EnableWhenResult(((BooleanType)answer).booleanValue() != answerItems.isEmpty(), 
                    linkId, enableCondition, questionnaireResponse);
        }        
        boolean result = answerItems
                .stream()
                .anyMatch(answer -> evaluateAnswer(answer, enableCondition.getAnswer(), enableCondition.getOperator()));            
        return new EnableWhenResult(result, linkId, enableCondition, questionnaireResponse);
    }

    protected boolean evaluateAnswer(Element answer, Type expectedAnswer, QuestionnaireItemOperator questionnaireItemOperator) {
        Type actualAnswer;
        try {
            actualAnswer = answer.asType();
        } catch (FHIRException e) {
            throw new RuntimeException("Unexpected answer type", e);
        }
        if (!actualAnswer.getClass().equals(expectedAnswer.getClass())) {
            throw new RuntimeException("Expected answer and actual answer have incompatible types");
        }                
        if (expectedAnswer instanceof Coding) {
            return compareCodingAnswer((Coding)expectedAnswer, (Coding)actualAnswer, questionnaireItemOperator);
        } else if ((expectedAnswer instanceof PrimitiveType)) {
            return comparePrimitiveAnswer((PrimitiveType<?>)actualAnswer, (PrimitiveType<?>)expectedAnswer, questionnaireItemOperator);
        }
        // TODO: Quantity, Attachment, reference?
        throw new RuntimeException("Unimplemented answer type: " + expectedAnswer.getClass());
    }
    private boolean comparePrimitiveAnswer(PrimitiveType<?> actualAnswer, PrimitiveType<?> expectedAnswer, QuestionnaireItemOperator questionnaireItemOperator) {                
        if (actualAnswer.getValue() instanceof Comparable){            
            @SuppressWarnings({ "rawtypes", "unchecked" })
            int result = ((Comparable)actualAnswer.getValue()).compareTo(expectedAnswer.getValue());
            if (questionnaireItemOperator == QuestionnaireItemOperator.EQUAL){
                return result == 0;
            } else if (questionnaireItemOperator == QuestionnaireItemOperator.NOT_EQUAL){
                return result != 0;
            } else if (questionnaireItemOperator == QuestionnaireItemOperator.GREATER_OR_EQUAL){
                return result >= 0;
            } else if (questionnaireItemOperator == QuestionnaireItemOperator.LESS_OR_EQUAL){
                return result <= 0;
            } else if (questionnaireItemOperator == QuestionnaireItemOperator.LESS_THAN){
                return result < 0;
            } else if (questionnaireItemOperator == QuestionnaireItemOperator.GREATER_THAN){
                return result > 0;
            }                  
            throw new RuntimeException("Bad operator for PrimitiveType comparison");
        } else if (questionnaireItemOperator == QuestionnaireItemOperator.EQUAL){
            return actualAnswer.equalsShallow(expectedAnswer);
        } else if (questionnaireItemOperator == QuestionnaireItemOperator.NOT_EQUAL){
            return !actualAnswer.equalsShallow(expectedAnswer);
        }
        throw new RuntimeException("Bad operator for PrimitiveType comparison");
    }

    private List<Element> findQuestionAnswers(Element questionnaireResponse, String question) {
        List<Element> matchingItems = questionnaireResponse.getChildren(ITEM_ELEMENT)
                .stream()
                .flatMap(i -> findSubItems(i).stream())
                .filter(i -> hasLinkId(i, question))
                .collect(Collectors.toList());        
        return matchingItems
                .stream()
                .flatMap(e -> extractAnswer(e).stream())
                .collect(Collectors.toList());        
    }
    
    private List<Element> extractAnswer(Element item) {
        return item.getChildrenByName(ANSWER_ELEMENT)
                .stream()
                .flatMap(c -> c.getChildren().stream())
                .collect(Collectors.toList());
    }

    private boolean compareCodingAnswer(Coding expectedAnswer, Coding actualAnswer, QuestionnaireItemOperator questionnaireItemOperator) {
        boolean result = compareSystems(expectedAnswer, actualAnswer) && compareCodes(expectedAnswer, actualAnswer);
        if (questionnaireItemOperator == QuestionnaireItemOperator.EQUAL){
            return result == true;
        } else if (questionnaireItemOperator == QuestionnaireItemOperator.NOT_EQUAL){
            return result == false;
        }
        throw new RuntimeException("Bad operator for Coding comparison");
    }

    private boolean compareCodes(Coding expectedCoding, Coding value) {
        if (expectedCoding.hasCode() != value.hasCode()) {
            return false;
        }
        if (expectedCoding.hasCode()) {
            return expectedCoding.getCode().equals(value.getCode());
        }
        return true;
    }

    private boolean compareSystems(Coding expectedCoding, Coding value) {
        if (expectedCoding.hasSystem() != value.hasSystem()) {
            return false;
        }
        if (expectedCoding.hasSystem()) {
            return expectedCoding.getSystem().equals(value.getSystem());
        }
        return true;
    }
    private List<Element> findSubItems(Element item) {
        List<Element> results = item.getChildren(ITEM_ELEMENT)
                .stream()
                .flatMap(i -> findSubItems(i).stream())
                .collect(Collectors.toList());
        results.add(item);
        return results;
    }

    private boolean hasLinkId(Element item, String linkId) {
        Element linkIdChild = item.getNamedChild(LINKID_ELEMENT);
        if (linkIdChild != null && linkIdChild.getValue().equals(linkId)){
            return true;
        }
        return false;
    }


   
}

@@ -5346,93 +5346,8 @@
    %0A
-            throw new RuntimeException(%22Bad operator for PrimitiveType comparison%22);%0A
     

Remove unnecessary throw