/*
 * 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 org.apache.jackrabbit.oak.spi.security.authorization.permission;

import org.apache.jackrabbit.guava.common.collect.ImmutableSet;
import org.apache.jackrabbit.guava.common.collect.Sets;
import org.apache.jackrabbit.api.JackrabbitSession;
import org.apache.jackrabbit.oak.plugins.tree.TreeLocation;
import org.apache.jackrabbit.oak.spi.namespace.NamespaceConstants;
import org.apache.jackrabbit.oak.spi.nodetype.NodeTypeConstants;
import org.apache.jackrabbit.oak.spi.security.privilege.PrivilegeConstants;
import org.apache.jackrabbit.oak.spi.version.VersionConstants;
import org.apache.jackrabbit.util.Text;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import javax.jcr.Session;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * Provides constants for permissions used in the OAK access evaluation as well
 * as permission related utility methods.
 */
public final class Permissions {

    private Permissions() {
    }

    public static final long NO_PERMISSION = 0;

    /**
     * @since OAK 1.0
     */
    public static final long READ_NODE = 1;

    /**
     * @since OAK 1.0
     */
    public static final long READ_PROPERTY = READ_NODE << 1;

    /**
     * @since OAK 1.0
     */
    public static final long ADD_PROPERTY = READ_PROPERTY << 1;

    /**
     * @since OAK 1.0
     */
    public static final long MODIFY_PROPERTY = ADD_PROPERTY << 1;

    public static final long REMOVE_PROPERTY = MODIFY_PROPERTY << 1;

    public static final long ADD_NODE = REMOVE_PROPERTY << 1;

    public static final long REMOVE_NODE = ADD_NODE << 1;

    public static final long READ_ACCESS_CONTROL = REMOVE_NODE << 1;

    public static final long MODIFY_ACCESS_CONTROL = READ_ACCESS_CONTROL << 1;

    public static final long NODE_TYPE_MANAGEMENT = MODIFY_ACCESS_CONTROL << 1;

    public static final long VERSION_MANAGEMENT = NODE_TYPE_MANAGEMENT << 1;

    public static final long LOCK_MANAGEMENT = VERSION_MANAGEMENT << 1;

    public static final long LIFECYCLE_MANAGEMENT = LOCK_MANAGEMENT << 1;

    public static final long RETENTION_MANAGEMENT = LIFECYCLE_MANAGEMENT << 1;

    public static final long MODIFY_CHILD_NODE_COLLECTION = RETENTION_MANAGEMENT << 1;

    public static final long NODE_TYPE_DEFINITION_MANAGEMENT = MODIFY_CHILD_NODE_COLLECTION << 1;

    public static final long NAMESPACE_MANAGEMENT = NODE_TYPE_DEFINITION_MANAGEMENT << 1;

    public static final long WORKSPACE_MANAGEMENT = NAMESPACE_MANAGEMENT << 1;

    public static final long PRIVILEGE_MANAGEMENT = WORKSPACE_MANAGEMENT << 1;

    /**
     * @since OAK 1.0
     */
    public static final long USER_MANAGEMENT = PRIVILEGE_MANAGEMENT << 1;
    /**
     * @since OAK 1.0
     */
    public static final long INDEX_DEFINITION_MANAGEMENT = USER_MANAGEMENT << 1;

    public static final long READ = READ_NODE | READ_PROPERTY;

    /**
     * @since OAK 1.0
     */
    public static final long REMOVE = REMOVE_NODE | REMOVE_PROPERTY;

    public static final long SET_PROPERTY = ADD_PROPERTY | MODIFY_PROPERTY | REMOVE_PROPERTY;

    public static final long WRITE = ADD_NODE | REMOVE_NODE | SET_PROPERTY;

    public static final long ALL = (READ
            | SET_PROPERTY
            | ADD_NODE | REMOVE_NODE
            | READ_ACCESS_CONTROL | MODIFY_ACCESS_CONTROL
            | NODE_TYPE_MANAGEMENT
            | VERSION_MANAGEMENT
            | LOCK_MANAGEMENT
            | LIFECYCLE_MANAGEMENT
            | RETENTION_MANAGEMENT
            | MODIFY_CHILD_NODE_COLLECTION
            | NODE_TYPE_DEFINITION_MANAGEMENT
            | NAMESPACE_MANAGEMENT
            | WORKSPACE_MANAGEMENT
            | PRIVILEGE_MANAGEMENT
            | USER_MANAGEMENT
            | INDEX_DEFINITION_MANAGEMENT
    );

    private static final Set<Long> NON_AGGREGATES = ImmutableSet.of(
            READ_NODE,
            READ_PROPERTY,
            ADD_PROPERTY,
            MODIFY_PROPERTY,
            REMOVE_PROPERTY,
            ADD_NODE,
            REMOVE_NODE,
            MODIFY_CHILD_NODE_COLLECTION,
            READ_ACCESS_CONTROL,
            MODIFY_ACCESS_CONTROL,
            NODE_TYPE_MANAGEMENT,
            VERSION_MANAGEMENT,
            LOCK_MANAGEMENT,
            LIFECYCLE_MANAGEMENT,
            RETENTION_MANAGEMENT,
            NODE_TYPE_DEFINITION_MANAGEMENT,
            NAMESPACE_MANAGEMENT,
            WORKSPACE_MANAGEMENT,
            PRIVILEGE_MANAGEMENT,
            USER_MANAGEMENT,
            INDEX_DEFINITION_MANAGEMENT);

    public static final Map<Long, String> PERMISSION_NAMES = new LinkedHashMap<>();
    static {
        PERMISSION_NAMES.put(ALL, "ALL");
        PERMISSION_NAMES.put(READ, "READ");
        PERMISSION_NAMES.put(READ_NODE, "READ_NODE");
        PERMISSION_NAMES.put(READ_PROPERTY, "READ_PROPERTY");
        PERMISSION_NAMES.put(SET_PROPERTY, "SET_PROPERTY");
        PERMISSION_NAMES.put(ADD_PROPERTY, "ADD_PROPERTY");
        PERMISSION_NAMES.put(MODIFY_PROPERTY, "MODIFY_PROPERTY");
        PERMISSION_NAMES.put(REMOVE_PROPERTY, "REMOVE_PROPERTY");
        PERMISSION_NAMES.put(ADD_NODE, "ADD_NODE");
        PERMISSION_NAMES.put(REMOVE_NODE, "REMOVE_NODE");
        PERMISSION_NAMES.put(REMOVE, "REMOVE");
        PERMISSION_NAMES.put(WRITE, "WRITE");
        PERMISSION_NAMES.put(MODIFY_CHILD_NODE_COLLECTION, "MODIFY_CHILD_NODE_COLLECTION");
        PERMISSION_NAMES.put(READ_ACCESS_CONTROL, "READ_ACCESS_CONTROL");
        PERMISSION_NAMES.put(MODIFY_ACCESS_CONTROL, "MODIFY_ACCESS_CONTROL");
        PERMISSION_NAMES.put(NODE_TYPE_MANAGEMENT, "NODE_TYPE_MANAGEMENT");
        PERMISSION_NAMES.put(VERSION_MANAGEMENT, "VERSION_MANAGEMENT");
        PERMISSION_NAMES.put(LOCK_MANAGEMENT, "LOCK_MANAGEMENT");
        PERMISSION_NAMES.put(LIFECYCLE_MANAGEMENT, "LIFECYCLE_MANAGEMENT");
        PERMISSION_NAMES.put(RETENTION_MANAGEMENT, "RETENTION_MANAGEMENT");
        PERMISSION_NAMES.put(NODE_TYPE_DEFINITION_MANAGEMENT, "NODE_TYPE_DEFINITION_MANAGEMENT");
        PERMISSION_NAMES.put(NAMESPACE_MANAGEMENT, "NAMESPACE_MANAGEMENT");
        PERMISSION_NAMES.put(WORKSPACE_MANAGEMENT, "WORKSPACE_MANAGEMENT");
        PERMISSION_NAMES.put(PRIVILEGE_MANAGEMENT, "PRIVILEGE_MANAGEMENT");
        PERMISSION_NAMES.put(USER_MANAGEMENT, "USER_MANAGEMENT");
        PERMISSION_NAMES.put(INDEX_DEFINITION_MANAGEMENT, "INDEX_DEFINITION_MANAGEMENT");
    }

    private static final Map<String, Long> PERMISSION_LOOKUP = new LinkedHashMap<>();
    static {
        PERMISSION_LOOKUP.put("ALL", ALL);
        PERMISSION_LOOKUP.put("READ", READ);
        PERMISSION_LOOKUP.put("READ_NODE", READ_NODE);
        PERMISSION_LOOKUP.put("READ_PROPERTY", READ_PROPERTY);
        PERMISSION_LOOKUP.put("SET_PROPERTY", SET_PROPERTY);
        PERMISSION_LOOKUP.put("ADD_PROPERTY", ADD_PROPERTY);
        PERMISSION_LOOKUP.put("MODIFY_PROPERTY", MODIFY_PROPERTY);
        PERMISSION_LOOKUP.put("REMOVE_PROPERTY", REMOVE_PROPERTY);
        PERMISSION_LOOKUP.put("ADD_NODE", ADD_NODE);
        PERMISSION_LOOKUP.put("REMOVE_NODE", REMOVE_NODE);
        PERMISSION_LOOKUP.put("REMOVE", REMOVE);
        PERMISSION_LOOKUP.put("WRITE", WRITE);
        PERMISSION_LOOKUP.put("MODIFY_CHILD_NODE_COLLECTION", MODIFY_CHILD_NODE_COLLECTION);
        PERMISSION_LOOKUP.put("READ_ACCESS_CONTROL", READ_ACCESS_CONTROL);
        PERMISSION_LOOKUP.put("MODIFY_ACCESS_CONTROL", MODIFY_ACCESS_CONTROL);
        PERMISSION_LOOKUP.put("NODE_TYPE_MANAGEMENT", NODE_TYPE_MANAGEMENT);
        PERMISSION_LOOKUP.put("VERSION_MANAGEMENT", VERSION_MANAGEMENT);
        PERMISSION_LOOKUP.put("LOCK_MANAGEMENT", LOCK_MANAGEMENT);
        PERMISSION_LOOKUP.put("LIFECYCLE_MANAGEMENT", LIFECYCLE_MANAGEMENT);
        PERMISSION_LOOKUP.put("RETENTION_MANAGEMENT", RETENTION_MANAGEMENT);
        PERMISSION_LOOKUP.put("NODE_TYPE_DEFINITION_MANAGEMENT", NODE_TYPE_DEFINITION_MANAGEMENT);
        PERMISSION_LOOKUP.put("NAMESPACE_MANAGEMENT", NAMESPACE_MANAGEMENT);
        PERMISSION_LOOKUP.put("WORKSPACE_MANAGEMENT", WORKSPACE_MANAGEMENT);
        PERMISSION_LOOKUP.put("PRIVILEGE_MANAGEMENT", PRIVILEGE_MANAGEMENT);
        PERMISSION_LOOKUP.put("USER_MANAGEMENT", USER_MANAGEMENT);
        PERMISSION_LOOKUP.put("INDEX_DEFINITION_MANAGEMENT", INDEX_DEFINITION_MANAGEMENT);
    }

    private static final Set<String> WRITE_ACTIONS = ImmutableSet.of(
            Session.ACTION_REMOVE,
            Session.ACTION_ADD_NODE,
            Session.ACTION_SET_PROPERTY,
            JackrabbitSession.ACTION_REMOVE_NODE,
            JackrabbitSession.ACTION_ADD_PROPERTY,
            JackrabbitSession.ACTION_MODIFY_PROPERTY,
            JackrabbitSession.ACTION_REMOVE_PROPERTY
    );

    private static final Map<String, Long> ACTIONS_MAP = new LinkedHashMap<>();
    static {
        ACTIONS_MAP.put(Session.ACTION_ADD_NODE, ADD_NODE);
        ACTIONS_MAP.put(JackrabbitSession.ACTION_ADD_PROPERTY, ADD_PROPERTY);
        ACTIONS_MAP.put(JackrabbitSession.ACTION_MODIFY_PROPERTY, MODIFY_PROPERTY);
        ACTIONS_MAP.put(JackrabbitSession.ACTION_REMOVE_PROPERTY, REMOVE_PROPERTY);
        ACTIONS_MAP.put(JackrabbitSession.ACTION_REMOVE_NODE, REMOVE_NODE);
        ACTIONS_MAP.put(JackrabbitSession.ACTION_NODE_TYPE_MANAGEMENT, NODE_TYPE_MANAGEMENT);
        ACTIONS_MAP.put(JackrabbitSession.ACTION_LOCKING, LOCK_MANAGEMENT);
        ACTIONS_MAP.put(JackrabbitSession.ACTION_VERSIONING, VERSION_MANAGEMENT);
        ACTIONS_MAP.put(JackrabbitSession.ACTION_READ_ACCESS_CONTROL, READ_ACCESS_CONTROL);
        ACTIONS_MAP.put(JackrabbitSession.ACTION_MODIFY_ACCESS_CONTROL, MODIFY_ACCESS_CONTROL);
        ACTIONS_MAP.put(JackrabbitSession.ACTION_USER_MANAGEMENT, USER_MANAGEMENT);
    }

    /**
     * Returns names of the specified permissions.
     *
     * @param permissions The permissions for which the string representation
     * should be collected.
     * @return The names of the given permissions.
     */
    public static Set<String> getNames(long permissions) {
        if (PERMISSION_NAMES.containsKey(permissions)) {
            return ImmutableSet.of(PERMISSION_NAMES.get(permissions));
        } else {
            Set<String> names = new HashSet<>();
            for (Map.Entry<Long, String> entry : PERMISSION_NAMES.entrySet()) {
                long key = entry.getKey();
                if ((permissions & key) == key) {
                    names.add(entry.getValue());
                }
            }
            return names;
        }
    }

    /**
     * Returns the names of the specified permissions separated by ','.
     *
     * @param permissions The permissions for which the string representation
     * should be collected.
     * @return The names of the given permissions separated by ',' such
     * that i can be passed to {@link Session#hasPermission(String, String)}
     * and {@link Session#checkPermission(String, String)}.
     */
    public static String getString(long permissions) {
        if (PERMISSION_NAMES.containsKey(permissions)) {
            return PERMISSION_NAMES.get(permissions);
        } else {
            StringBuilder sb = new StringBuilder();
            for (Map.Entry<Long, String> entry : PERMISSION_NAMES.entrySet()) {
                long key = entry.getKey();
                if ((permissions & key) == key) {
                    if (sb.length() != 0) {
                        sb.append(',');
                    }
                    sb.append(entry.getValue());
                }
            }
            return sb.toString();
        }
    }

    public static boolean isRepositoryPermission(long permission) {
        return permission == NAMESPACE_MANAGEMENT ||
                permission == NODE_TYPE_DEFINITION_MANAGEMENT ||
                permission == PRIVILEGE_MANAGEMENT ||
                permission == WORKSPACE_MANAGEMENT;
    }

    public static boolean isAggregate(long permission) {
        return permission > NO_PERMISSION && !NON_AGGREGATES.contains(permission);
    }

    public static Iterable<Long> aggregates(final long permissions) {
        if (ALL == permissions) {
            return NON_AGGREGATES;
        } else {
            return NON_AGGREGATES.stream().filter(permission -> includes(permissions, permission)).collect(Collectors.toSet());
        }
    }

    public static boolean includes(long permissions, long permissionsToTest) {
        return (permissions & permissionsToTest) == permissionsToTest;
    }

    public static boolean respectParentPermissions(long permissions) {
        return Permissions.includes(permissions, Permissions.ADD_NODE) ||
                Permissions.includes(permissions, Permissions.REMOVE_NODE);
    }

    /**
     * Returns those bits from {@code permissions} that are not present in
     * the {@code otherPermissions}, i.e. subtracts the other permissions
     * from permissions.<br>
     * If the specified {@code otherPermissions} do not intersect with
     * {@code permissions},  {@code permissions} are returned.<br>
     * If {@code permissions} is included in {@code otherPermissions},
     * {@link #NO_PERMISSION} is returned.
     *
     * @param permissions The permissions from which to subtract {@code otherPermissions}.
     * @param otherPermissions The permissions to be subtracted.
     * @return the differences of the 2 permissions or {@link #NO_PERMISSION}.
     */
    public static long diff(long permissions, long otherPermissions) {
        return permissions & ~otherPermissions;
    }

    /**
     * Returns the permissions that correspond the given jcr actions such as
     * specified in {@link Session#hasPermission(String, String)}. Note that
     * in addition to the regular JCR actions ({@link Session#ACTION_READ},
     * {@link Session#ACTION_ADD_NODE}, {@link Session#ACTION_REMOVE} and
     * {@link Session#ACTION_SET_PROPERTY}) the string may also contain
     * the names of all permissions defined by this class.
     *
     * @param jcrActions A comma separated string of JCR actions and permission
     * names.
     * @param location The tree location for which the permissions should be
     * calculated.
     * @param isAccessControlContent Flag to mark the given location as access
     * control content.
     * @return The permissions.
     * @throws IllegalArgumentException If the string contains unknown actions
     * or permission names.
     */
    public static long getPermissions(@NotNull String jcrActions,
                                      @NotNull TreeLocation location,
                                      boolean isAccessControlContent) {
        Set<String> actions = Sets.newHashSet(Text.explode(jcrActions, ',', false));
        long permissions = NO_PERMISSION;
        // map read action respecting the 'isAccessControlContent' flag.
        if (actions.remove(Session.ACTION_READ)) {
            permissions |= mapReadAction(location, isAccessControlContent);
        }

        // map write actions respecting the 'isAccessControlContent' flag.
        if (!actions.isEmpty()) {
            if (isAccessControlContent) {
                if (actions.removeAll(WRITE_ACTIONS)) {
                    permissions |=  MODIFY_ACCESS_CONTROL;
                }
            } else {
                permissions |= mapWriteActions(actions, location);
            }
        }

        // map remaining actions and permission-names that have a simple 1:1
        // mapping between action and permission
        if (!actions.isEmpty()) {
            for (Map.Entry<String, Long> actionEntry : ACTIONS_MAP.entrySet()) {
                if (actions.remove(actionEntry.getKey())) {
                    permissions |= actionEntry.getValue();
                }
            }
            permissions |= getPermissions(actions);
        }

        // now the action set must be empty; otherwise it contained unsupported action(s)
        if (!actions.isEmpty()) {
            throw new IllegalArgumentException("Unknown actions: " + actions);
        }
        return permissions;
    }

    private static long mapReadAction(@NotNull TreeLocation location, boolean isAccessControlContent) {
        if (isAccessControlContent) {
            return READ_ACCESS_CONTROL;
        } else if (!location.exists()) {
            return READ;
        } else if (location.getProperty() != null) {
            return READ_PROPERTY;
        } else {
            return READ_NODE;
        }
    }

    private static long mapWriteActions(@NotNull Set<String> actions, @NotNull TreeLocation location) {
        // item is not access controlled -> cover actions that don't have
        // a 1:1 mapping to a given permission.
        long permissions = NO_PERMISSION;
        if (actions.remove(Session.ACTION_SET_PROPERTY)) {
            if (location.getProperty() == null) {
                permissions |= ADD_PROPERTY;
            } else {
                permissions |= MODIFY_PROPERTY;
            }
        }
        if (actions.remove(Session.ACTION_REMOVE)) {
            if (!location.exists()) {
                permissions |= REMOVE;
            } else if (location.getProperty() != null) {
                permissions |= REMOVE_PROPERTY;
            } else {
                permissions |= REMOVE_NODE;
            }
        }
        return permissions;
    }

    /**
     * Returns the permissions that correspond the given permission names.
     *
     * @param permissionNames A comma separated string of permission names.
     * @return The permissions.
     * @throws IllegalArgumentException If the string contains unknown actions
     * or permission names.
     */
    public static long getPermissions(@Nullable String permissionNames) {
        if (permissionNames == null || permissionNames.isEmpty()) {
            return NO_PERMISSION;
        } else {
            return getPermissions(Sets.newHashSet(Arrays.asList(permissionNames.split(","))));
        }
    }

    private static long getPermissions(@NotNull Set<String> permissionNames) {
        long permissions = NO_PERMISSION;
        Iterator<String> it = permissionNames.iterator();
        while (it.hasNext()) {
            String name = it.next();
            if (PERMISSION_LOOKUP.containsKey(name)) {
                permissions |= PERMISSION_LOOKUP.get(name);
                it.remove();
            }
        }
        return permissions;
    }

    public static long getPermission(@Nullable String path, long defaultPermission) {
        long permission;
        if (NamespaceConstants.NAMESPACES_PATH.equals(path)) {
            permission = Permissions.NAMESPACE_MANAGEMENT;
        } else if (NodeTypeConstants.NODE_TYPES_PATH.equals(path)) {
            permission = Permissions.NODE_TYPE_DEFINITION_MANAGEMENT;
        } else if (VersionConstants.SYSTEM_PATHS.contains(path)) {
            permission = Permissions.VERSION_MANAGEMENT;
        } else if (PrivilegeConstants.PRIVILEGES_PATH.equals(path)) {
            permission = Permissions.PRIVILEGE_MANAGEMENT;
        } else {
            // NOTE: workspace-mgt is missing (blocked by OAK-916)
            permission = defaultPermission;
        }
        return permission;
    }
}
