/*
 * 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-2009 Sun Microsystems, Inc.
 *      Portions Copyright 2012-2015 ForgeRock AS.
 */
package org.opends.server.authorization.dseecompat;

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

import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.forgerock.i18n.LocalizableMessage;

/**
 * This class represents the body of an ACI. The body of the ACI is the
 * version, name, and permission-bind rule pairs.
 */
public class AciBody {

    /**
     * Regular expression group position for the version string.
     */
    private static final int VERSION = 1;

    /**
     * Regular expression group position for the name string.
     */
    private static final int NAME = 2;

    /**
     * Regular expression group position for the permission string.
     */
    private static final int PERM = 1;

    /**
     * Regular expression group position for the rights string.
     */
    private static final int RIGHTS = 2;

    /**
     * Regular expression group position for the bindrule string.
     */
    private static final int BINDRULE = 3;

    /**
     * Index into the ACI string where the ACI body starts.
     */
    private int startPos;

    /**
     * The name of the ACI, currently not used but parsed.
     */
    private String name;

    /**
     * The version of the ACi, current not used but parsed and checked for 3.0.
     */
    private String version;

    /**
     * This structure represents a permission-bind rule pairs. There can be
     * several of these.
     */
    private List<PermBindRulePair> permBindRulePairs;

    /**
     * Regular expression used to match the access type group (allow, deny) and
     * the rights group "(read, write, ...)". The last pattern looks for a group
     * surrounded by parenthesis. The group must contain at least one
     * non-paren character.
     */
    private static final String permissionRegex =
               WORD_GROUP + ZERO_OR_MORE_WHITESPACE + "\\(([^()]+)\\)";

    /**
     * Regular expression that matches a bind rule group at a coarse level. It
     * matches any character one or more times, a single quotation and
     * an optional right parenthesis.
     */
    private static final String bindRuleRegex =
            "(.+?\"[)]*)" + ACI_STATEMENT_SEPARATOR;

    /**
     * Regular expression used to match the actions of the ACI. The actions
     * are permissions and matching bind rules.
     */
    private static final String actionRegex =
            ZERO_OR_MORE_WHITESPACE + permissionRegex +
            ZERO_OR_MORE_WHITESPACE + bindRuleRegex;

    /**
     * Regular expression used to match the version value (digit.digit).
     */
    private static final String versionRegex = "(\\d\\.\\d)";

    /**
     * Regular expression used to match the version token. Case insensitive.
     */
    private static final String versionToken = "(?i)version(?-i)";

    /**
     * Regular expression used to match the acl token. Case insensitive.
     */
    private static final String aclToken = "(?i)acl(?-i)";

    /**
     * Regular expression used to match the body of an ACI. This pattern is
     * a general verification check.
     */
    public static final String bodyRegx =
        "\\(" + ZERO_OR_MORE_WHITESPACE + versionToken +
        ZERO_OR_MORE_WHITESPACE + versionRegex +
        ACI_STATEMENT_SEPARATOR + aclToken + ZERO_OR_MORE_WHITESPACE +
        "\"([^\"]*)\"" + ACI_STATEMENT_SEPARATOR + actionRegex +
        ZERO_OR_MORE_WHITESPACE  + "\\)";

    /**
     * Regular expression used to match the header of the ACI body. The
     * header is version and acl name.
     */
    private static final String header =
       OPEN_PAREN + ZERO_OR_MORE_WHITESPACE + versionToken +
       ZERO_OR_MORE_WHITESPACE +
       versionRegex + ACI_STATEMENT_SEPARATOR + aclToken +
       ZERO_OR_MORE_WHITESPACE +  "\"(.*?)\"" + ACI_STATEMENT_SEPARATOR;

    /**
     * Construct an ACI body from the specified version, name and
     * permission-bind rule pairs.
     *
     * @param verision The version of the ACI.
     * @param name The name of the ACI.
     * @param startPos The start position in the string of the ACI body.
     * @param permBindRulePairs The set of fully parsed permission-bind rule
     * pairs pertaining to this ACI.
     */
    private AciBody(String verision, String name, int startPos,
            List<PermBindRulePair> permBindRulePairs) {
        this.version=verision;
        this.name=name;
        this.startPos=startPos;
        this.permBindRulePairs=permBindRulePairs;
    }

    /**
     * Decode an ACI string representing the ACI body.
     *
     * @param input String representation of the ACI body.
     * @return An AciBody class representing the decoded ACI body string.
     * @throws AciException If the provided string contains errors.
     */
    public static AciBody decode(String input)
    throws AciException {
        String version=null, name=null;
        int startPos=0;
        List<PermBindRulePair> permBindRulePairs = new ArrayList<>();
        Pattern bodyPattern = Pattern.compile(header);
        Matcher bodyMatcher = bodyPattern.matcher(input);
        if(bodyMatcher.find()) {
            startPos=bodyMatcher.start();
            version  = bodyMatcher.group(VERSION);
            if (!version.equalsIgnoreCase(supportedVersion)) {
                LocalizableMessage message = WARN_ACI_SYNTAX_INVAILD_VERSION.get(version);
                throw new AciException(message);
            }
            name = bodyMatcher.group(NAME);
            input = input.substring(bodyMatcher.end());
        }

        Pattern bodyPattern1 = Pattern.compile("\\G" + actionRegex);
        Matcher bodyMatcher1 = bodyPattern1.matcher(input);

        /*
         * The may be many permission-bind rule pairs.
         */
        int lastIndex = -1;
        while(bodyMatcher1.find()) {
         String perm=bodyMatcher1.group(PERM);
         String rights=bodyMatcher1.group(RIGHTS);
         String bRule=bodyMatcher1.group(BINDRULE);
         PermBindRulePair pair = PermBindRulePair.decode(perm, rights, bRule);
         permBindRulePairs.add(pair);
         lastIndex = bodyMatcher1.end();
        }

        if (lastIndex >= 0 && input.charAt(lastIndex) != ')')
        {
          LocalizableMessage message = WARN_ACI_SYNTAX_GENERAL_PARSE_FAILED.get(input);
          throw new AciException(message);
        }

        return new AciBody(version, name, startPos, permBindRulePairs);
    }

    /**
     * Checks all of the permissions in this body for a specific access type.
     * Need to walk down each permission-bind rule pair and call it's
     * hasAccessType method.
     *
     * @param accessType The access type enumeration to search for.
     * @return True if the access type is found in a permission of
     * a permission bind rule pair.
     */
    public boolean hasAccessType(EnumAccessType accessType) {
        List<PermBindRulePair>pairs=getPermBindRulePairs();
         for(PermBindRulePair p : pairs) {
             if(p.hasAccessType(accessType)) {
                 return true;
             }
         }
         return false;
    }

    /**
     * Search through each permission bind rule associated with this body and
     * try and match a single right of the specified rights.
     *
     * @param rights The rights that are used in the match.
     * @return True if a one or more right of the specified rights matches
     * a body's permission rights.
     */
    public boolean hasRights(int rights) {
        List<PermBindRulePair>pairs=getPermBindRulePairs();
        for(PermBindRulePair p : pairs) {
            if(p.hasRights(rights)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Retrieve the permission-bind rule pairs of this ACI body.
     *
     * @return The permission-bind rule pairs.
     */
    List<PermBindRulePair> getPermBindRulePairs() {
        return permBindRulePairs;
    }

    /**
     * Get the start position in the ACI string of the ACI body.
     *
     * @return Index into the ACI string of the ACI body.
     */
    public int getMatcherStartPos() {
        return startPos;
    }

    /**
     * Performs an evaluation of the permission-bind rule pairs
     * using the evaluation context. The method walks down
     * each PermBindRulePair object and:
     *
     *  1. Skips a pair if the evaluation context rights don't
     *     apply to that ACI. For example, an LDAP search would skip
     *     an ACI pair that allows writes.
     *
     *  2. The pair's bind rule is evaluated using the evaluation context.
     *  3. The result of the evaluation is itself evaluated. See comments
     *     below in the code.
     *
     * @param evalCtx The evaluation context to evaluate against.
     * @return An enumeration result of the evaluation.
     */
    public  EnumEvalResult evaluate(AciEvalContext evalCtx) {
        EnumEvalResult res = FALSE;
        List<PermBindRulePair>pairs=getPermBindRulePairs();
        for(PermBindRulePair p : pairs) {
            if (evalCtx.isDenyEval() && p.hasAccessType(EnumAccessType.ALLOW)) {
                continue;
            }
            if(!p.hasRights(getEvalRights(evalCtx))) {
                continue;
            }
            res=p.getBindRule().evaluate(evalCtx);
            // The evaluation result could be FAIL. Stop processing and return
            //FAIL. Maybe an internal search failed.
            if(res != TRUE && res != FALSE) {
                res = FAIL;
                break;
                //If the access type is DENY and the pair evaluated to TRUE,
                //then stop processing and return TRUE. A deny pair succeeded.
            } else if (p.hasAccessType(EnumAccessType.DENY) && res == TRUE) {
                res = TRUE;
                break;
                //An allow access type evaluated TRUE, stop processing and return TRUE.
            } else if (p.hasAccessType(EnumAccessType.ALLOW) && res == TRUE) {
                res = TRUE;
                break;
            }
        }
        return res;
    }

  /**
   * Returns the name string.
   * @return The name string.
   */
  public String getName() {
      return this.name;
    }


  /**
   * Mainly used because geteffectiverights adds flags to the rights that aren't
   * needed in the actual evaluation of the ACI. This routine returns only the
   * rights needed in the evaluation. The order does matter, ACI_SELF evaluation
   * needs to be before ACI_WRITE.
    * <p>
    * JNR: I find the implementation in this method dubious.
    * @see EnumRight#hasRights(int, int)
    *
   * @param evalCtx  The evaluation context to determine the rights of.
   * @return  The evaluation rights to used in the evaluation.
   */
  private int getEvalRights(AciEvalContext evalCtx) {
    if(evalCtx.hasRights(ACI_WRITE) && evalCtx.hasRights(ACI_SELF)) {
      return ACI_SELF;
    } else  if(evalCtx.hasRights(ACI_COMPARE)) {
      return ACI_COMPARE;
    } else if(evalCtx.hasRights(ACI_SEARCH)) {
      return ACI_SEARCH;
    } else if(evalCtx.hasRights(ACI_READ)) {
      return ACI_READ;
    } else if(evalCtx.hasRights(ACI_DELETE)) {
      return ACI_DELETE;
    } else if(evalCtx.hasRights(ACI_ADD)) {
      return ACI_ADD;
    } else if(evalCtx.hasRights(ACI_WRITE)) {
      return ACI_WRITE;
    } else if(evalCtx.hasRights(ACI_PROXY)) {
      return ACI_PROXY;
    } else if(evalCtx.hasRights(ACI_IMPORT)) {
      return ACI_IMPORT;
    } else if(evalCtx.hasRights(ACI_EXPORT)) {
      return ACI_EXPORT;
    }
    return ACI_NULL;
  }

  /**
   * Return version string of the ACI.
   *
   * @return The ACI version string.
   */
  public String getVersion () {
    return version;
  }

  /** {@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)
  {
    buffer.append("(version ").append(this.version);
    buffer.append("; acl \"").append(this.name).append("\"; ");
    for (PermBindRulePair pair : this.permBindRulePairs)
    {
      buffer.append(pair);
    }
  }
}
