// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements.  See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership.  The ASF 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 com.cloud.bridge.util;

import java.util.ArrayList;
import java.util.List;

import org.apache.log4j.Logger;
import org.json.simple.parser.ContentHandler;
import org.json.simple.parser.JSONParser;
import org.json.simple.parser.ParseException;

import com.cloud.bridge.service.core.s3.S3BucketPolicy;
import com.cloud.bridge.service.core.s3.S3BucketPolicy.PolicyAccess;
import com.cloud.bridge.service.core.s3.S3ConditionFactory;
import com.cloud.bridge.service.core.s3.S3PolicyAction;
import com.cloud.bridge.service.core.s3.S3PolicyAction.PolicyActions;
import com.cloud.bridge.service.core.s3.S3PolicyCondition;
import com.cloud.bridge.service.core.s3.S3PolicyCondition.ConditionKeys;
import com.cloud.bridge.service.core.s3.S3PolicyConditionBlock;
import com.cloud.bridge.service.core.s3.S3PolicyPrincipal;
import com.cloud.bridge.service.core.s3.S3PolicyStatement;
import com.cloud.bridge.service.exception.PermissionDeniedException;

/**
 * This class uses the JSON simple parser to convert the JSON of a Bucket Policy
 * into internal objects.
 *
 * Another way to implement this by use of a stack to keep track of where the current
 * parsing is being done.   However, since we are only handling a limited JSON sequence
 * here simple counts and flags will do the same as a stack.
 */
public class PolicyParser {
    protected final static Logger logger = Logger.getLogger(PolicyParser.class);

    private S3BucketPolicy bucketPolicy = null;
    private S3PolicyPrincipal principals = null;
    private S3PolicyStatement statement = null;
    private S3PolicyAction actions = null;
    private S3PolicyAction convertActions = new S3PolicyAction();
    private S3PolicyCondition condition = null;
    private S3ConditionFactory condFactory = null;
    private S3PolicyConditionBlock block = null;
    private PolicyActions notAction = PolicyActions.UnknownAction;
    private String id = null;
    private String sid = null;
    private String effect = null;
    private String resource = null;
    private String condKey = null;   // -> the next key in a condition
    private String toUser = null;    // -> text to user of a problem
    private List<String> valueList = new ArrayList<String>();

    private JSONParser jparser = null;
    private int entryNesting = 0;    // -> startObjectEntry() .. nesting count
    private int condNested = 0;      // -> at what level of nesting is the condition defined
    private int keyNested = 0;       // -> at what level of nesting is the condition key defined

    private boolean inId = false;   // -> currently in an "Id" element
    private boolean inSid = false;
    private boolean inAWS = false;
    private boolean inEffect = false;
    private boolean inResource = false;
    private boolean inNotAction = false;
    private boolean inVersion = false;
    private boolean inStatement = false;

    public PolicyParser() {
        jparser = new JSONParser();
        condFactory = new S3ConditionFactory();
    }

    ContentHandler myHandler = new ContentHandler() {
        public boolean endArray() throws ParseException {
            logger.debug("endArray()");
            return true;
        }

        public void endJSON() throws ParseException, PermissionDeniedException {
            logger.debug("endJSON()");

            if (null != statement) {
                //System.out.println( "endJSON() - statement");
                if (null != block) {
                    block.verify();
                    statement.setConditionBlock(block);
                }
                if (null != bucketPolicy) {
                    statement.verify();
                    bucketPolicy.addStatement(statement);
                }
                statement = null;
                block = null;
            }
        }

        public boolean endObject() throws ParseException, PermissionDeniedException {
            logger.debug("endObject(), nesting: " + entryNesting);

            if (null != statement && 1 >= entryNesting) {
                //System.out.println( "endObject() - statement");
                if (null != block) {
                    block.verify();
                    statement.setConditionBlock(block);
                }
                if (null != bucketPolicy) {
                    statement.verify();
                    bucketPolicy.addStatement(statement);
                }
                statement = null;
                block = null;
            }

            if (0 == entryNesting)
                inStatement = false;
            return true;
        }

        public boolean endObjectEntry() throws ParseException, PermissionDeniedException {
            logger.debug("endObjectEntry(), nesting: " + entryNesting);

            if (inSid) {
                if (null != statement)
                    statement.setSid(sid);
                inSid = false;
            } else if (inEffect) {
                if (null != statement) {
                    if (effect.equalsIgnoreCase("Allow"))
                        statement.setEffect(PolicyAccess.ALLOW);
                    else if (effect.equalsIgnoreCase("Deny"))
                        statement.setEffect(PolicyAccess.DENY);
                    else
                        badPolicy("Effect", effect);
                }
                inEffect = false;
            } else if (inResource) {
                if (null != statement && resource.startsWith("arn:aws:s3:::")) {
                    String resourcePath = resource.substring(13);
                    verifySameBucket(resourcePath);
                    statement.setResource(resourcePath);
                }
                inResource = false;
            } else if (inNotAction) {
                if (null != statement)
                    statement.setNotAction(notAction);
                inNotAction = false;
            } else if (inVersion) {
                inVersion = false;
            } else if (inId) {
                if (null != bucketPolicy)
                    bucketPolicy.setId(id);
                inId = false;
            } else if (null != actions) {
                if (null != statement)
                    statement.setActions(actions);
                actions = null;
            } else if (null != principals) {
                if (inAWS && null != statement)
                    statement.setPrincipals(principals);
                principals = null;
            } else if (null != condition) {
                //System.out.println( "in condition: " + condNested + " " + entryNesting + " " + keyNested );
                // -> is it just the current key that is done?
                try {
                    if (keyNested == entryNesting) {
                        String[] values = valueList.toArray(new String[0]);
                        ConditionKeys tempKey = S3PolicyCondition.toConditionKeys(condKey);
                        if (ConditionKeys.UnknownKey == tempKey)
                            badPolicy("Condition Key", condKey);
                        condition.setKey(tempKey, values);
                        valueList.clear();
                        condKey = null;
                    }
                } catch (ParseException e) {
                    logger.error("Policy Parser condition error: ", e);
                    throw e;
                } catch (Exception e) {
                    logger.error("Policy Parser condition error: ", e);
                    badPolicy("Condition Key (" + condKey + ")", e.toString());
                }

                // -> is the condition completely done?
                if (condNested == entryNesting) {
                    condition.verify();
                    block.addCondition(condition);
                    condition = null;
                }
            } else if (null != statement && 1 == entryNesting) {
                if (null != block) {
                    block.verify();
                    statement.setConditionBlock(block);
                }
                if (null != bucketPolicy) {
                    statement.verify();
                    bucketPolicy.addStatement(statement);
                }
                statement = null;
                block = null;
            }

            entryNesting--;
            return true;
        }

        public boolean primitive(Object value) throws ParseException, PermissionDeniedException {
            logger.debug("primitive(): " + value);

            if (inSid) {
                sid = (String)value;
            } else if (inEffect) {
                effect = (String)value;
            } else if (inResource) {
                resource = (String)value;
            } else if (inNotAction) {
                notAction = convertActions.toPolicyActions((String)value);
                if (notAction == PolicyActions.UnknownAction)
                    badPolicy("NotAction", (String)value);
            } else if (inId) {
                id = (String)value;
            } else if (null != actions) {
                PolicyActions tempAction = convertActions.toPolicyActions((String)value);
                if (tempAction == PolicyActions.UnknownAction)
                    badPolicy("Action", (String)value);
                actions.addAction(tempAction);
            } else if (null != principals) {
                principals.addPrincipal((String)value);
            } else if (null != condition) {
                // -> a condition key can have one or more values
                valueList.add((String)value);
            } else if (inVersion) {
                String version = (String)value;
                if (!version.equals("2008-10-17"))
                    badPolicy("Version", (String)value);
            }

            return true;
        }

        public boolean startArray() throws ParseException {
            logger.debug("startArray()");
            return true;
        }

        public void startJSON() throws ParseException {
            logger.debug("startJSON()");
        }

        public boolean startObject() throws ParseException {
            logger.debug("startObject(), nesting: " + entryNesting);

            if (1 == entryNesting && inStatement)
                statement = new S3PolicyStatement();

            return true;
        }

        /**
         * Note: A statement does not have to have a condition block to be valid.
         */
        public boolean startObjectEntry(String key) throws ParseException {
            entryNesting++;
            logger.debug("startObjectEntry(), key: [" + key + "]");

            inSid = false;
            inAWS = false;
            inEffect = false;
            inResource = false;
            inNotAction = false;
            inVersion = false;
            inId = false;

            if (key.equalsIgnoreCase("Statement"))
                inStatement = true;
            else if (key.equalsIgnoreCase("Action"))
                actions = new S3PolicyAction();
            else if (key.equalsIgnoreCase("Principal"))
                principals = new S3PolicyPrincipal();
            else if (key.equalsIgnoreCase("Condition"))
                block = new S3PolicyConditionBlock();
            else if (key.equalsIgnoreCase("AWS") && null != principals)
                inAWS = true;
            else if (key.equalsIgnoreCase("CanonicalUser") && null != principals)
                inAWS = true;
            else if (key.equalsIgnoreCase("Sid"))
                inSid = true;
            else if (key.equalsIgnoreCase("Effect"))
                inEffect = true;
            else if (key.equalsIgnoreCase("Resource"))
                inResource = true;
            else if (key.equalsIgnoreCase("NotAction"))
                inNotAction = true;
            else if (key.equalsIgnoreCase("Version"))
                inVersion = true;
            else if (key.equalsIgnoreCase("Id"))
                inId = true;
            else if (null != condition) {
                condKey = key;
                keyNested = entryNesting;
            } else if (null != block) {
                condition = condFactory.createCondition(key);
                condNested = entryNesting;
                if (null == condition)
                    badPolicy("Condition type", key);
            } else
                logger.debug("startObjectEntry() no match");

            return true;
        }
    };

    public S3BucketPolicy parse(String policy, String bucketName) throws ParseException, PermissionDeniedException {
        bucketPolicy = new S3BucketPolicy();
        bucketPolicy.setBucketName(bucketName);
        jparser.parse(policy, myHandler);
        return bucketPolicy;
    }

    /**
     * From Amazon on S3 Policies:
     * "Each policy must cover only a single bucket and resources within that bucket (when writing a
     * policy, don't include statements that refer to other buckets or resources in other buckets)"
     *
     * @param resourcePath
     */
    private void verifySameBucket(String resourcePath) throws PermissionDeniedException {
        String testBucketName = resourcePath;
        String bucketName = bucketPolicy.getBucketName();

        // -> extract just the bucket name
        int offset = testBucketName.indexOf("/");
        if (-1 != offset)
            testBucketName = testBucketName.substring(0, offset);

        if (!testBucketName.equals(bucketName))
            throw new PermissionDeniedException("The S3 Bucket Policy must only refer to the single bucket: \"" + bucketName +
                "\", but it referres to the following resource: \"" + resourcePath + "\"");
    }

    public static void badPolicy(String place, String badValue) throws ParseException {
        String toUser = new String("S3 Bucket Policy " + place + " of: \"" + badValue + "\" is unknown");
        throw new ParseException(ParseException.ERROR_UNEXPECTED_TOKEN, toUser);
    }
}
