/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License, Version 1.0 only
 * (the "License").  You may not use this file except in compliance
 * with the License.
 *
 * You can obtain a copy of the license at
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE
 * or https://OpenDS.dev.java.net/OpenDS.LICENSE.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE.  If applicable,
 * add the following below this CDDL HEADER, with the fields enclosed
 * by brackets "[]" replaced with your own identifying information:
 *      Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 *
 *
 *      Copyright 2008 Sun Microsystems, Inc.
 */

package org.opends.server.authorization.dseecompat;
import org.opends.messages.Message;

import static org.opends.messages.AccessControlMessages.*;
import static org.opends.server.authorization.dseecompat.Aci.*;
import java.util.regex.Pattern;
import java.util.regex.Matcher;
import java.util.HashMap;

/**
 * This class represents a single bind rule of an ACI permission-bind rule
 * pair.
 */
public class BindRule {

    /*
     * This hash table holds the keyword bind rule mapping.
     */
    private HashMap<String, KeywordBindRule> keywordRuleMap =
                                    new HashMap<String, KeywordBindRule>();

    /*
     * True is a boolean "not" was seen.
     */
    private boolean negate=false;

    /*
     * Complex bind rules have left and right values.
     */
    private BindRule left = null;
    private BindRule right = null;

    /*
     * Enumeration of the boolean type of the complex bind rule ("and" or "or").
     */
    private EnumBooleanTypes booleanType = null;

    /*
     * The keyword of a simple bind rule.
     */
    private EnumBindRuleKeyword keyword = null;

    /*
     * Regular expression group position of a bind rule keyword.
     */
    private static final int keywordPos = 1;

    /*
     * Regular expression group position of a bind rule operation.
     */
    private static final int opPos = 2;

    /*
     * Regular expression group position of a bind rule expression.
     */
    private static final int expressionPos = 3;

    /*
     * Regular expression group position of the remainder part of an operand.
     */
    private static final int remainingOperandPos = 1;

    /*
     * Regular expression group position of the remainder of the bind rule.
     */
    private static final int remainingBindrulePos = 2;

    /*
     * Regular expression for valid bind rule operator group.
     */
    private static final String opRegGroup = "([!=<>]+)";

    /*
     * Regular expression for the expression part of a partially parsed
     * bind rule.
     */
    private static final String expressionRegex =
                                  "\"([^\"]+)\"" + ZERO_OR_MORE_WHITESPACE;

    /*
     * Regular expression for a single bind rule.
     */
    private static final String bindruleRegex =
        WORD_GROUP_START_PATTERN + ZERO_OR_MORE_WHITESPACE +
        opRegGroup + ZERO_OR_MORE_WHITESPACE + expressionRegex;

    /*
     * Regular expression of the remainder part of a partially parsed bind rule.
     */
    private static final String remainingBindruleRegex =
        ZERO_OR_MORE_WHITESPACE_START_PATTERN + WORD_GROUP +
        ZERO_OR_MORE_WHITESPACE + "(.*)$";

    /**
     * Constructor that takes an keyword enumeration and corresponding
     * simple bind rule. The keyword string is the key for the keyword rule in
     * the keywordRuleMap. This is a simple bind rule representation:

     * keyword  op  rule
     *
     * An example of a simple bind rule is:
     *
     *  userdn = "ldap:///anyone"
     *
     * @param keyword The keyword enumeration.
     * @param rule The rule corresponding to this keyword.
     */
    private BindRule(EnumBindRuleKeyword keyword, KeywordBindRule rule) {
        this.keyword=keyword;
        this.keywordRuleMap.put(keyword.toString(), rule);
    }


    /*
     * TODO Verify that this handles the NOT boolean properly by
     * creating a unit test.
     *
     * I'm a bit confused by the constructor which takes left and right
     * arguments. Is it always supposed to have exactly two elements?
     * Is it supposed to keep nesting bind rules in a chain until all of
     * them have been processed?  The documentation for this method needs
     * to be a lot clearer.  Also, it doesn't look like it handles the NOT
     * type properly.
     */
    /**
     * Constructor that represents a complex bind rule. The left and right
     * bind rules are saved along with the boolean type operator. A complex
     * bind rule looks like:
     *
     *  bindrule   booleantype   bindrule
     *
     * Each side of the complex bind rule can be complex bind rule(s)
     * itself. An example of a complex bind rule would be:
     *
     * (dns="*.example.com" and (userdn="ldap:///anyone" or
     * (userdn="ldap:///cn=foo,dc=example,dc=com and ip=129.34.56.66)))
     *
     * This constructor should always have two elements. The processing
     * of a complex bind rule is dependent on the boolean operator type.
     * See the evalComplex method for more information.
     *
     *
     * @param left The bind rule left of the boolean.
     * @param right The right bind rule.
     * @param booleanType The boolean type enumeration ("and" or "or").
     */
    private BindRule(BindRule left, BindRule right,
            EnumBooleanTypes booleanType) {
        this.booleanType = booleanType;
        this.left = left;
        this.right = right;
    }

    /*
     * TODO Verify this method handles escaped parentheses by writing
     * a unit test.
     *
     * It doesn't look like the decode() method handles the possibility of
     * escaped parentheses in a bind rule.
     */
    /**
     * Decode an ACI bind rule string representation.
     * @param input The string representation of the bind rule.
     * @return A BindRule class representing the bind rule.
     * @throws AciException If the string is an invalid bind rule.
     */
    public static BindRule decode (String input)
    throws AciException {
        if ((input == null) || (input.length() == 0))
        {
          return null;
        }
        String bindruleStr = input.trim();
        char firstChar = bindruleStr.charAt(0);
        char[] bindruleArray = bindruleStr.toCharArray();

        if (firstChar == '(')
        {
          BindRule bindrule_1 = null;
          int currentPos;
          int numOpen = 0;
          int numClose = 0;

          // Find the associated closed parenthesis
          for (currentPos = 0; currentPos < bindruleArray.length; currentPos++)
          {
            if (bindruleArray[currentPos] == '(')
            {
              numOpen++;
            }
            else if (bindruleArray[currentPos] == ')')
            {
              numClose++;
            }
            if (numClose == numOpen)
            {
              //We found the associated closed parenthesis
              //the parenthesis are removed
              String bindruleStr1 = bindruleStr.substring(1, currentPos);
              bindrule_1 = BindRule.decode(bindruleStr1);
              break;
            }
          }
          /*
           * Check that the number of open parenthesis is the same as
           * the number of closed parenthesis.
           * Raise an exception otherwise.
           */
          if (numOpen > numClose) {
              Message message =
                  ERR_ACI_SYNTAX_BIND_RULE_MISSING_CLOSE_PAREN.get(input);
              throw new AciException(message);
          }
          /*
           * If there are remaining chars => there MUST be an
           * operand (AND / OR)
           * otherwise there is a syntax error
           */
          if (currentPos < (bindruleArray.length - 1))
          {
            String remainingBindruleStr =
                bindruleStr.substring(currentPos + 1);
            return createBindRule(bindrule_1, remainingBindruleStr);
          }
          else
          {
            return bindrule_1;
          }
        }
        else
        {
          StringBuilder b=new StringBuilder(bindruleStr);
          /*
           * TODO Verify by unit test that this negation
           * is correct. This code handles a simple bind rule negation such
           * as:
           *
           *  not userdn="ldap:///anyone"
           */
          boolean negate=determineNegation(b);
          bindruleStr=b.toString();
          Pattern bindrulePattern = Pattern.compile(bindruleRegex);
          Matcher bindruleMatcher = bindrulePattern.matcher(bindruleStr);
          int bindruleEndIndex;
          if (bindruleMatcher.find())
          {
            bindruleEndIndex = bindruleMatcher.end();
            BindRule bindrule_1 = parseAndCreateBindrule(bindruleMatcher);
            bindrule_1.setNegate(negate);
            if (bindruleEndIndex < bindruleStr.length())
            {
              String remainingBindruleStr =
                  bindruleStr.substring(bindruleEndIndex);
              return createBindRule(bindrule_1, remainingBindruleStr);
            }
            else {
              return bindrule_1;
            }
          }
          else {
              Message message =
                  ERR_ACI_SYNTAX_INVALID_BIND_RULE_SYNTAX.get(input);
              throw new AciException(message);
          }
        }
    }


    /**
     * Parses a simple bind rule using the regular expression matcher.
     * @param bindruleMatcher A regular expression matcher holding
     * the engine to use in the creation of a simple bind rule.
     * @return A BindRule determined by the matcher.
     * @throws AciException If the bind rule matcher found errors.
     */
    private static BindRule parseAndCreateBindrule(Matcher bindruleMatcher)
    throws AciException {
        String keywordStr = bindruleMatcher.group(keywordPos);
        String operatorStr = bindruleMatcher.group(opPos);
        String expression = bindruleMatcher.group(expressionPos);
        EnumBindRuleKeyword keyword;
        EnumBindRuleType operator;

        // Get the Keyword
        keyword = EnumBindRuleKeyword.createBindRuleKeyword(keywordStr);
        if (keyword == null)
        {
            Message message =
                WARN_ACI_SYNTAX_INVALID_BIND_RULE_KEYWORD.get(keywordStr);
            throw new AciException(message);
        }

        // Get the operator
        operator = EnumBindRuleType.createBindruleOperand(operatorStr);
        if (operator == null) {
            Message message =
                WARN_ACI_SYNTAX_INVALID_BIND_RULE_OPERATOR.get(operatorStr);
            throw new AciException(message);
        }

        //expression can't be null
        if (expression == null) {
            Message message =
                WARN_ACI_SYNTAX_MISSING_BIND_RULE_EXPRESSION.get(operatorStr);
            throw new AciException(message);
        }
        validateOperation(keyword, operator);
        KeywordBindRule rule = decode(expression, keyword, operator);
        return new BindRule(keyword, rule);
    }

    /**
     * Create a complex bind rule from a substring
     * parsed from the ACI string.
     * @param bindrule The left hand part of a complex bind rule
     * parsed previously.
     * @param remainingBindruleStr The string used to determine the right
     * hand part.
     * @return A BindRule representing a complex bind rule.
     * @throws AciException If the string contains an invalid
     * right hand bind rule string.
     */
    private static BindRule createBindRule(BindRule bindrule,
            String remainingBindruleStr) throws AciException {
        Pattern remainingBindrulePattern =
            Pattern.compile(remainingBindruleRegex);
        Matcher remainingBindruleMatcher =
            remainingBindrulePattern.matcher(remainingBindruleStr);
        if (remainingBindruleMatcher.find()) {
            String remainingOperand =
                remainingBindruleMatcher.group(remainingOperandPos);
            String remainingBindrule =
                remainingBindruleMatcher.group(remainingBindrulePos);
            EnumBooleanTypes operand =
                EnumBooleanTypes.createBindruleOperand(remainingOperand);
            if ((operand == null)
                    || ((operand != EnumBooleanTypes.AND_BOOLEAN_TYPE) &&
                            (operand != EnumBooleanTypes.OR_BOOLEAN_TYPE))) {
                Message message =
                        WARN_ACI_SYNTAX_INVALID_BIND_RULE_BOOLEAN_OPERATOR
                                .get(remainingOperand);
                throw new AciException(message);
            }
            StringBuilder ruleExpr=new StringBuilder(remainingBindrule);
            /* TODO write a unit test to verify.
             * This is a check for something like:
             * bindrule and not (bindrule)
             * or something ill-advised like:
             * and not not not (bindrule).
             */
            boolean negate=determineNegation(ruleExpr);
            remainingBindrule=ruleExpr.toString();
            BindRule bindrule_2 =
                BindRule.decode(remainingBindrule);
            bindrule_2.setNegate(negate);
            return new BindRule(bindrule, bindrule_2, operand);
        } else {
            Message message = ERR_ACI_SYNTAX_INVALID_BIND_RULE_SYNTAX.get(
                remainingBindruleStr);
            throw new AciException(message);
        }
    }

    /**
     * Tries to strip an "not" boolean modifier from the string and
     * determine at the same time if the value should be flipped.
     * For example:
     *
     * not not not bindrule
     *
     * is true.
     *
     * @param ruleExpr The bindrule expression to evaluate. This
     * string will be changed if needed.
     * @return True if the boolean needs to be negated.
     */
    private static boolean determineNegation(StringBuilder ruleExpr)  {
        boolean negate=false;
        String ruleStr=ruleExpr.toString();
        while(ruleStr.regionMatches(true, 0, "not ", 0, 4)) {
            negate = !negate;
            ruleStr = ruleStr.substring(4);
        }
        ruleExpr.replace(0, ruleExpr.length(), ruleStr);
        return negate;
    }

    /**
     * Set the negation parameter as determined by the function above.
     * @param v The value to assign negate to.
     */
    private void setNegate(boolean v) {
        negate=v;
    }

    /*
     * TODO This method needs to handle the userattr keyword. Also verify
     * that the rest of the keywords are handled correctly.
     * TODO Investigate moving this method into EnumBindRuleKeyword class.
     *
     * Does validateOperation need a default case?  Why is USERATTR not in this
     * list? Why is TIMEOFDAY not in this list when DAYOFWEEK is in the list?
     * Would it be more appropriate to put this logic in the
     * EnumBindRuleKeyword class so we can be sure it's always handled properly
     *  for all keywords?
     */
    /**
     * Checks the keyword operator enumeration to make sure it is valid.
     * This method doesn't handle all cases.
     * @param keyword The keyword enumeration to evaluate.
     * @param op The operation enumeration to evaluate.
     * @throws AciException If the operation is not valid for the keyword.
     */
    private static void validateOperation(EnumBindRuleKeyword keyword,
                                        EnumBindRuleType op)
    throws AciException {
        switch (keyword) {
        case USERDN:
        case ROLEDN:
        case GROUPDN:
        case IP:
        case DNS:
        case AUTHMETHOD:
        case DAYOFWEEK:
            if ((op != EnumBindRuleType.EQUAL_BINDRULE_TYPE)
                    && (op != EnumBindRuleType.NOT_EQUAL_BINDRULE_TYPE)) {
                Message message =
                  WARN_ACI_SYNTAX_INVALID_BIND_RULE_KEYWORD_OPERATOR_COMBO
                          .get(keyword.toString(), op.toString());
                throw new AciException(message);
            }
        }
    }

    /*
     * TODO Investigate moving into the EnumBindRuleKeyword class.
     *
     * Should we move the logic in the
     * decode(String,EnumBindRuleKeyword,EnumBindRuleType) method into the
     * EnumBindRuleKeyword class so we can be sure that it's always
     * handled properly for all keywords?
     */
    /**
     * Creates a keyword bind rule suitable for saving in the keyword
     * rule map table. Each individual keyword class will do further
     * parsing and validation of the expression string.  This processing
     * is part of the simple bind rule creation.
     * @param expr The expression string to further parse.
     * @param keyword The keyword to create.
     * @param op The operation part of the bind rule.
     * @return A keyword bind rule class that can be stored in the
     * map table.
     * @throws AciException If the expr string contains a invalid
     * bind rule.
     */
    private static KeywordBindRule decode(String expr,
                                          EnumBindRuleKeyword keyword,
                                          EnumBindRuleType op)
            throws AciException  {
        KeywordBindRule rule ;
        switch (keyword) {
            case USERDN:
            {
                rule = UserDN.decode(expr, op);
                break;
            }
            case ROLEDN:
            {
                //The roledn keyword is not supported. Throw an exception with
                //a message if it is seen in the ACI.
                Message message =
                    WARN_ACI_SYNTAX_ROLEDN_NOT_SUPPORTED.get(expr);
                throw new AciException(message);
            }
            case GROUPDN:
            {
                rule = GroupDN.decode(expr, op);
                break;
            }
            case IP:
            {
                rule=IP.decode(expr, op);
                break;
            }
            case DNS:
            {
                rule = DNS.decode(expr, op);
                break;
            }
            case DAYOFWEEK:
            {
                rule = DayOfWeek.decode(expr, op);
                break;
            }
            case TIMEOFDAY:
            {
                rule=TimeOfDay.decode(expr, op);
                break;
            }
            case AUTHMETHOD:
            {
                rule = AuthMethod.decode(expr, op);
                break;
            }
            case USERATTR:
            {
                rule = UserAttr.decode(expr, op);
                break;
            }
            case SSF:
            {
                rule = SSF.decode(expr, op);
                break;
            }
            default:  {
                Message message = WARN_ACI_SYNTAX_INVALID_BIND_RULE_KEYWORD.get(
                    keyword.toString());
                throw new AciException(message);
            }
        }
        return rule;
    }

    /**
     * Evaluate the results of a complex bind rule. If the boolean
     * is an AND type then left and right must be TRUE, else
     * it must be an OR result and one of the bind rules must be
     * TRUE.
     * @param left The left bind rule result to evaluate.
     * @param right The right bind result to evaluate.
     * @return The result of the complex evaluation.
     */
    private EnumEvalResult evalComplex(EnumEvalResult left,
                                       EnumEvalResult right) {
        EnumEvalResult ret=EnumEvalResult.FALSE;
        if(booleanType == EnumBooleanTypes.AND_BOOLEAN_TYPE) {
           if((left == EnumEvalResult.TRUE) && (right == EnumEvalResult.TRUE))
                ret=EnumEvalResult.TRUE;
        } else if((left == EnumEvalResult.TRUE) ||
                  (right == EnumEvalResult.TRUE))
            ret=EnumEvalResult.TRUE;
       return ret;
    }

    /**
     * Evaluate an bind rule against an evaluation context. If it is a simple
     * bind rule (no boolean type) then grab the keyword rule from the map
     * table and call the corresponding evaluate function. If it is a
     * complex rule call the routine above "evalComplex()".
     * @param evalCtx The evaluation context to pass to the keyword
     * evaluation function.
     * @return An result enumeration containing the result of the evaluation.
     */
    public EnumEvalResult evaluate(AciEvalContext evalCtx) {
        EnumEvalResult ret;
        //Simple bind rules have a null booleanType enumeration.
        if(this.booleanType == null) {
            KeywordBindRule rule=keywordRuleMap.get(keyword.toString());
            ret = rule.evaluate(evalCtx);
        }  else
            ret=evalComplex(left.evaluate(evalCtx),right.evaluate(evalCtx));
        return EnumEvalResult.negateIfNeeded(ret, negate);
    }
}
