/*
*  Copyright (c) WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
*
*  WSO2 Inc. licenses this file to you under the Apache License,
*  Version 2.0 (the "License"); you may not use this file except
*  in compliance with the License.
*  You may obtain a copy of the License at
*
*    http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied.  See the License for the
* specific language governing permissions and limitations
* under the License.
*/


package org.wso2.balana.combine.xacml3;

import org.wso2.balana.ObligationResult;
import org.wso2.balana.ctx.ResultFactory;
import org.wso2.balana.Rule;
import org.wso2.balana.combine.RuleCombinerElement;
import org.wso2.balana.combine.RuleCombiningAlgorithm;
import org.wso2.balana.ctx.AbstractResult;
import org.wso2.balana.ctx.EvaluationCtx;
import org.wso2.balana.xacml3.Advice;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.*;

/**
 * This is the new (XACML 3.0) the standard Permit Overrides rule combining algorithm. It allows a single evaluation of
 * Permit to take precedence over any number of deny, not applicable or indeterminate results. Note
 * that since this implementation does an ordered evaluation, this class also supports the Ordered
 * Permit Overrides algorithm.
 */
public class PermitOverridesRuleAlg extends RuleCombiningAlgorithm{

    /**
     * The standard URN used to identify this algorithm
     */
    public static final String algId = "urn:oasis:names:tc:xacml:3.0:rule-combining-algorithm:"
            + "permit-overrides";

    // a URI form of the identifier
    private static URI identifierURI;
    // exception if the URI was invalid, which should never be a problem
    private static RuntimeException earlyException;

    static {
        try {
            identifierURI = new URI(algId);
        } catch (URISyntaxException se) {
            earlyException = new IllegalArgumentException();
            earlyException.initCause(se);
        }
    }

    /**
     * Standard constructor.
     */
    public PermitOverridesRuleAlg() {
        super(identifierURI);

        if (earlyException != null)
            throw earlyException;
    }

    /**
     * Protected constructor used by the ordered version of this algorithm.
     *
     * @param identifier the algorithm's identifier
     */
    protected PermitOverridesRuleAlg(URI identifier) {
        super(identifier);
    }

    /**
     * Applies the combining rule to the set of rules based on the evaluation context.
     *
     * @param context the context from the request
     * @param parameters a (possibly empty) non-null <code>List</code> of
     *            <code>CombinerParameter<code>s
     * @param ruleElements the rules to combine
     *
     * @return the result of running the combining algorithm
     */
    public AbstractResult combine(EvaluationCtx context, List parameters, List ruleElements) {

        boolean atLeastOneErrorD = false;
        boolean atLeastOneErrorP = false;
        boolean atLeastOneDeny = false;
        AbstractResult firstIndeterminateResultD = null;
        AbstractResult firstIndeterminateResultP = null;
        List<ObligationResult> denyObligations = new ArrayList<ObligationResult>();
        List<Advice> denyAdvices = new ArrayList<Advice>();

        Iterator it = ruleElements.iterator();

        while (it.hasNext()) {
            Rule rule = ((RuleCombinerElement) (it.next())).getRule();
            AbstractResult result = rule.evaluate(context);
            int value = result.getDecision();

            // if there was a value of PERMIT, then regardless of what
            // else we've seen, we always return PERMIT
            if (value == AbstractResult.DECISION_PERMIT){
                return result;
            }

            if(value == AbstractResult.DECISION_NOT_APPLICABLE){
                continue;
            }

            // keep track of whether we had at least one rule that
            // actually pertained to the request
            if (value == AbstractResult.DECISION_DENY){

                atLeastOneDeny = true;
                denyAdvices.addAll(result.getAdvices());
                denyObligations.addAll(result.getObligations());

            } else {

                // if it was INDETERMINATE, check extended results
                if (value == AbstractResult.DECISION_INDETERMINATE_DENY){
                    atLeastOneErrorD = true;
                    // there are no rules about what to do if multiple cases
                    // cause errors, so we'll just return the first one
                    if(firstIndeterminateResultD == null){
                        firstIndeterminateResultD = result;
                    }
                } else if (value== AbstractResult.DECISION_INDETERMINATE_PERMIT){
                    atLeastOneErrorP = true;
                    // there are no rules about what to do if multiple cases
                    // cause errors, so we'll just return the first one
                    if(firstIndeterminateResultP == null){
                        firstIndeterminateResultP = result;
                    }
                }
            }
        }

        if (atLeastOneErrorP && (atLeastOneErrorD || atLeastOneDeny)){

            return ResultFactory.getFactory().getResult(AbstractResult.DECISION_INDETERMINATE_DENY_OR_PERMIT,
                                                   firstIndeterminateResultP.getStatus(), context);
        }

        if(atLeastOneErrorP){
            return ResultFactory.getFactory().getResult(AbstractResult.DECISION_INDETERMINATE_PERMIT,
                                                   firstIndeterminateResultP.getStatus(), context);
        }

        if (atLeastOneDeny) {
            return ResultFactory.getFactory().getResult(AbstractResult.DECISION_DENY,
                                                        denyObligations, denyAdvices, context);
        }
        // if we hit this point, then none of the rules actually applied
        // to us, so we return NOT_APPLICABLE
        return ResultFactory.getFactory().getResult(AbstractResult.DECISION_NOT_APPLICABLE, context);
    }
    
}
