/*
 * 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 legal-notices/CDDLv1_0.txt
 * or http://forgerock.org/license/CDDLv1.0.html.
 * 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 legal-notices/CDDLv1_0.txt.
 * 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.
 *      Portions Copyright 2013-2015 ForgeRock AS
 */
package org.opends.server.authorization.dseecompat;

import static org.opends.messages.AccessControlMessages.*;
import static org.opends.server.authorization.dseecompat.Aci.*;

import java.util.HashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.forgerock.i18n.LocalizableMessage;

/**
 * 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 final HashMap<String, KeywordBindRule> keywordRuleMap = new HashMap<>();

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

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

    /** Enumeration of the boolean type of the complex bind rule ("and" or "or"). */
    private EnumBooleanTypes booleanType;
    /** The keyword of a simple bind rule. */
    private EnumBindRuleKeyword keyword;

    /** 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) {
              throw new AciException(WARN_ACI_SYNTAX_BIND_RULE_MISSING_CLOSE_PAREN.get(input));
          }
          /*
           * 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);
          }
          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 {
              throw new AciException(WARN_ACI_SYNTAX_INVALID_BIND_RULE_SYNTAX.get(input));
          }
        }
    }


    /**
     * 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);

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

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

        //expression can't be null
        if (expression == null) {
            throw new AciException(WARN_ACI_SYNTAX_MISSING_BIND_RULE_EXPRESSION.get(operatorStr));
        }
        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)) {
                LocalizableMessage 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);
        }
        throw new AciException(WARN_ACI_SYNTAX_INVALID_BIND_RULE_SYNTAX.get(remainingBindruleStr));
    }

    /**
     * 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) {
                throw new AciException(
                    WARN_ACI_SYNTAX_INVALID_BIND_RULE_KEYWORD_OPERATOR_COMBO.get(keyword, op));
            }
        }
    }

    /*
     * 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  {
        switch (keyword) {
        case USERDN:
            return UserDN.decode(expr, op);
        case ROLEDN:
            //The roledn keyword is not supported. Throw an exception with
            //a message if it is seen in the ACI.
            throw new AciException(WARN_ACI_SYNTAX_ROLEDN_NOT_SUPPORTED.get(expr));
        case GROUPDN:
            return GroupDN.decode(expr, op);
        case IP:
            return IP.decode(expr, op);
        case DNS:
            return DNS.decode(expr, op);
        case DAYOFWEEK:
            return DayOfWeek.decode(expr, op);
        case TIMEOFDAY:
            return TimeOfDay.decode(expr, op);
        case AUTHMETHOD:
            return AuthMethod.decode(expr, op);
        case USERATTR:
            return UserAttr.decode(expr, op);
        case SSF:
            return SSF.decode(expr, op);
        default:
            throw new AciException(WARN_ACI_SYNTAX_INVALID_BIND_RULE_KEYWORD.get(keyword));
        }
    }

    /**
     * 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) {
        if (booleanType == EnumBooleanTypes.AND_BOOLEAN_TYPE) {
          if (left == EnumEvalResult.TRUE && right == EnumEvalResult.TRUE) {
            return EnumEvalResult.TRUE;
          }
        } else if (left == EnumEvalResult.TRUE || right == EnumEvalResult.TRUE) {
          return EnumEvalResult.TRUE;
        }
       return EnumEvalResult.FALSE;
    }

    /**
     * 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);
    }

    /** {@inheritDoc} */
    @Override
    public String toString() {
        final StringBuilder sb = new StringBuilder();
        toString(sb);
        return sb.toString();
    }

    /**
     * Appends a string representation of this object to the provided buffer.
     *
     * @param buffer
     *          The buffer into which a string representation of this object
     *          should be appended.
     */
    public final void toString(StringBuilder buffer) {
        if (this.keywordRuleMap != null) {
            for (KeywordBindRule rule : this.keywordRuleMap.values()) {
                rule.toString(buffer);
                buffer.append(";");
            }
        }
    }
}
