/*
 * Copyright (c) OSGi Alliance (2004, 2017). All Rights Reserved.
 * 
 * Licensed 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.osgi.service.dmt;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.TreeMap;

/**
 * {@code Acl} is an immutable class representing structured access to DMT ACLs.
 * Under OMA DM the ACLs are defined as strings with an internal syntax.
 * <p>
 * The methods of this class taking a principal as parameter accept remote
 * server IDs (as passed to {@link DmtAdmin#getSession(String, String, int)
 * DmtAdmin.getSession}), as well as &quot; {@code *} &quot; indicating any
 * principal.
 * <p>
 * The syntax for valid remote server IDs:
 * <p>
 * &lt;<i>server-identifier</i>&gt; ::= All printable characters except
 * {@code '='}, {@code '&'}, {@code '*'}, {@code '+'} or white-space characters.
 * 
 * @author $Id: c58d6f55ec211830489505001946db4b73c9bfdd $
 */
public final class Acl {

	// ----- Public constants -----//

	/**
	 * Principals holding this permission can issue GET command on the node
	 * having this ACL.
	 */
	public static final int			GET					= 1;

	/**
	 * Principals holding this permission can issue ADD commands on the node
	 * having this ACL.
	 */
	public static final int			ADD					= 2;

	/**
	 * Principals holding this permission can issue REPLACE commands on the node
	 * having this ACL.
	 */
	public static final int			REPLACE				= 4;

	/**
	 * Principals holding this permission can issue DELETE commands on the node
	 * having this ACL.
	 */
	public static final int			DELETE				= 8;

	/**
	 * Principals holding this permission can issue EXEC commands on the node
	 * having this ACL.
	 */
	public static final int			EXEC				= 16;

	/**
	 * Principals holding this permission can issue any command on the node
	 * having this ACL. This permission is the logical OR of {@link #ADD},
	 * {@link #DELETE}, {@link #EXEC}, {@link #GET} and {@link #REPLACE}
	 * permissions.
	 */
	public static final int			ALL_PERMISSION		= ADD | DELETE | EXEC | GET | REPLACE;

	// ----- Private constants -----//

	private static final int[]		PERMISSION_CODES	= new int[] {ADD, DELETE, EXEC, GET, REPLACE};

	private static final String[]	PERMISSION_NAMES	= new String[] {"Add", "Delete", "Exec", "Get", "Replace"};

	private static final String		ALL_PRINCIPALS		= "*";

	// ----- Private fields -----//

	// the implementation takes advantage of this being a sorted map
	private final TreeMap<String, Integer>	principalPermissions;

	private final int				globalPermissions;

	// ----- Public constructors -----//

	/**
	 * Create an instance of the ACL from its canonical string representation.
	 * 
	 * @param acl The string representation of the ACL as defined in OMA DM. If
	 *        {@code null} or empty then it represents an empty list of
	 *        principals with no permissions.
	 * @throws IllegalArgumentException if acl is not a valid OMA DM ACL string
	 */
	public Acl(String acl) {
		if (acl == null || acl.equals("")) { // empty permission set
			principalPermissions = new TreeMap<>();
			globalPermissions = 0;
			return;
		}

		TreeMap<String, Integer> tempPrincipalPermissions = new TreeMap<>();
		int tempGlobalPermissions = 0;

		String[] aclEntries = split(acl, '&', -1);
		for (int i = 0; i < aclEntries.length; i++) {
			if (aclEntries[i].length() == 0)
				throw new IllegalArgumentException("Invalid ACL string: empty ACL entry.");

			String[] entryParts = split(aclEntries[i], '=', 2);
			if (entryParts.length == 1)
				throw new IllegalArgumentException("Invalid ACL string: no '=' in ACL entry.");
			if (entryParts[1].length() == 0)
				throw new IllegalArgumentException("Invalid ACL string: no server identifiers in ACL entry.");

			int command = parseCommand(entryParts[0]);
			String[] serverIds = split(entryParts[1], '+', -1);
			for (int j = 0; j < serverIds.length; j++) {
				if (serverIds[j].length() == 0)
					throw new IllegalArgumentException("Invalid ACL string: empty server identifier.");

				if (serverIds[j].equals(ALL_PRINCIPALS))
					tempGlobalPermissions |= command;
				else {
					checkServerId(serverIds[j], "Invalid ACL string: " + "server ID contains illegal character");
					Integer n = tempPrincipalPermissions.get(serverIds[j]);
					int oldPermission = (n != null) ? n.intValue() : 0;
					tempPrincipalPermissions.put(serverIds[j], Integer.valueOf(oldPermission | command));
				}
			}
		}

		principalPermissions = tempPrincipalPermissions;
		globalPermissions = tempGlobalPermissions;
	}

	/**
	 * Creates an instance with a specified list of principals and the
	 * permissions they hold. The two arrays run in parallel, that is
	 * {@code principals[i]} will hold {@code permissions[i]} in the ACL.
	 * <p>
	 * A principal name may not appear multiple times in the 'principals'
	 * argument. If the &quot;*&quot; principal appears in the array, the
	 * corresponding permissions will be granted to all principals (regardless
	 * of whether they appear in the array or not).
	 * 
	 * @param principals The array of principals
	 * @param permissions The array of permissions
	 * @throws IllegalArgumentException if the length of the two arrays are not
	 *         the same, if any array element is invalid, or if a principal
	 *         appears multiple times in the {@code principals} array
	 */
	public Acl(String[] principals, int[] permissions) {
		if (principals.length != permissions.length)
			throw new IllegalArgumentException("The lengths of the principal and permission arrays are not the same.");

		TreeMap<String, Integer> tempPrincipalPermissions = new TreeMap<>();
		int tempGlobalPermissions = 0;

		for (int i = 0; i < principals.length; i++) {
			// allow one * in 'principals' array, remove after loop
			if (!ALL_PRINCIPALS.equals(principals[i]))
				checkPrincipal(principals[i]);
			checkPermissions(permissions[i]);

			Integer permInt = Integer.valueOf(permissions[i]);
			Object old = tempPrincipalPermissions.put(principals[i], permInt);
			if (old != null)
				throw new IllegalArgumentException("Principal '" + principals[i] + "' appears multiple times in the principal array.");
		}

		// set the global permissions if there was a * in the array
		Object globalPermObj = tempPrincipalPermissions.remove(ALL_PRINCIPALS);
		if (globalPermObj != null)
			tempGlobalPermissions = ((Integer) globalPermObj).intValue();

		principalPermissions = tempPrincipalPermissions;
		globalPermissions = tempGlobalPermissions;
	}

	// ----- Private constructors -----//

	/**
	 * Creates an instance identical to the {@code base} ACL except for the
	 * permissions of the given {@code principal}, which are overwritten with
	 * the given {@code permissions}.
	 * <p>
	 * Assumes that the permissions parameter has been checked. All
	 * modifications of an {@code Acl} (add, delete, set) are done through this
	 * method.
	 * 
	 * @param base The ACL that provides all permissions except for permissions
	 *        of the given principal.
	 * @param principal The entity to which permission should be granted.
	 * @param permissions The set of permissions to be given. The parameter can
	 *        be a logical {@code or} of the permission constants defined in
	 *        this class.
	 */
	private Acl(Acl base, String principal, int permissions) {
		// make a shallow copy of the permission table, the keys (String) and
		// values (Integer) are immutable anyway
		@SuppressWarnings("unchecked")
		TreeMap<String, Integer> tempPrincipalPermissions = (TreeMap<String, Integer>) base.principalPermissions.clone();
		int tempGlobalPermissions = base.globalPermissions;

		int deletedGlobalPerm = tempGlobalPermissions & ~permissions;
		if (ALL_PRINCIPALS.equals(principal)) {
			deleteFromAll(tempPrincipalPermissions, deletedGlobalPerm);
			tempGlobalPermissions = permissions;
		} else {
			checkPrincipal(principal);

			if (deletedGlobalPerm != 0)
				throw new IllegalArgumentException("Cannot revoke globally set permissions (" + writeCommands(deletedGlobalPerm) + ") from a specific principal (" + principal + ").");

			setPrincipalPermission(tempPrincipalPermissions, principal, permissions);
		}

		principalPermissions = tempPrincipalPermissions;
		globalPermissions = tempGlobalPermissions;
	}

	// ----- Public methods -----//

	/**
	 * Checks whether the given object is equal to this {@code Acl} instance.
	 * Two {@code Acl} instances are equal if they allow the same set of
	 * permissions for the same set of principals.
	 * 
	 * @param obj the object to compare with this {@code Acl} instance
	 * @return {@code true} if the parameter represents the same ACL as this
	 *         instance
	 */
	@Override
	public boolean equals(Object obj) {
		if (obj == this)
			return true;

		if (!(obj instanceof Acl))
			return false;

		Acl other = (Acl) obj;

		if (globalPermissions != other.globalPermissions || principalPermissions.size() != other.principalPermissions.size())
			return false;

		// principalPermissions sets cannot be easily compared, because they are
		// not canonical: the global permissions may or may not be present for
		// each principal, without changing the meaning of the Acl object.

		// Compare canonical string representations, inefficient but simple.
		return toString().equals(other.toString());
	}

	/**
	 * Returns the hash code for this ACL instance. If two {@code Acl} instances
	 * are equal according to the {@link #equals(Object)} method, then calling
	 * this method on each of them must produce the same integer result.
	 * 
	 * @return hash code for this ACL
	 */
	@Override
	public int hashCode() {
		// Using the hash code of the canonical string representation, because
		// the principalPermissions set is not canonical (see above).
		return toString().hashCode();
	}

	/**
	 * Create a new {@code Acl} instance from this {@code Acl} with the given
	 * permission added for the given principal. The already existing
	 * permissions of the principal are not affected.
	 * 
	 * @param principal The entity to which permissions should be granted, or
	 *        &quot;*&quot; to grant permissions to all principals.
	 * @param permissions The permissions to be given. The parameter can be a
	 *        logical {@code or} of more permission constants defined in this
	 *        class.
	 * @return a new {@code Acl} instance
	 * @throws IllegalArgumentException if {@code principal} is not a valid
	 *         principal name or if {@code permissions} is not a valid
	 *         combination of the permission constants defined in this class
	 */
	public synchronized Acl addPermission(String principal, int permissions) {
		checkPermissions(permissions);

		int oldPermissions = getPermissions(principal);
		return setPermission(principal, oldPermissions | permissions);
	}

	/**
	 * Create a new {@code Acl} instance from this {@code Acl} with the given
	 * permission revoked from the given principal. Other permissions of the
	 * principal are not affected.
	 * <p>
	 * Note, that it is not valid to revoke a permission from a specific
	 * principal if that permission is granted globally to all principals.
	 * 
	 * @param principal The entity from which permissions should be revoked, or
	 *        &quot;*&quot; to revoke permissions from all principals.
	 * @param permissions The permissions to be revoked. The parameter can be a
	 *        logical {@code or} of more permission constants defined in this
	 *        class.
	 * @return a new {@code Acl} instance
	 * @throws IllegalArgumentException if {@code principal} is not a valid
	 *         principal name, if {@code permissions} is not a valid combination
	 *         of the permission constants defined in this class, or if a
	 *         globally granted permission would have been revoked from a
	 *         specific principal
	 */
	public synchronized Acl deletePermission(String principal, int permissions) {
		checkPermissions(permissions);

		int oldPermissions = getPermissions(principal);
		return setPermission(principal, oldPermissions & ~permissions);
	}

	/**
	 * Get the permissions associated to a given principal.
	 * 
	 * @param principal The entity whose permissions to query, or &quot;*&quot;
	 *        to query the permissions that are granted globally, to all
	 *        principals
	 * @return The permissions of the given principal. The returned {@code int}
	 *         is a bitmask of the permission constants defined in this class
	 * @throws IllegalArgumentException if {@code principal} is not a valid
	 *         principal name
	 */
	public synchronized int getPermissions(String principal) {
		int permissions = 0;

		if (!(ALL_PRINCIPALS.equals(principal))) {
			checkPrincipal(principal);
			Object po = principalPermissions.get(principal);
			if (po != null)
				permissions = ((Integer) po).intValue();
		}

		return permissions | globalPermissions;
	}

	/**
	 * Check whether the given permissions are granted to a certain principal.
	 * The requested permissions are specified as a bitfield, for example
	 * {@code (Acl.ADD | Acl.DELETE | Acl.GET)}.
	 * 
	 * @param principal The entity to check, or &quot;*&quot; to check whether
	 *        the given permissions are granted to all principals globally
	 * @param permissions The permissions to check
	 * @return {@code true} if the principal holds all the given permissions
	 * @throws IllegalArgumentException if {@code principal} is not a valid
	 *         principal name or if {@code permissions} is not a valid
	 *         combination of the permission constants defined in this class
	 */
	public synchronized boolean isPermitted(String principal, int permissions) {
		checkPermissions(permissions);

		int hasPermissions = getPermissions(principal);
		return (permissions & hasPermissions) == permissions;
	}

	/**
	 * Create a new {@code Acl} instance from this {@code Acl} where all
	 * permissions for the given principal are overwritten with the given
	 * permissions.
	 * <p>
	 * Note, that when changing the permissions of a specific principal, it is
	 * not allowed to specify a set of permissions stricter than the global set
	 * of permissions (that apply to all principals).
	 * 
	 * @param principal The entity to which permissions should be granted, or
	 *        &quot;*&quot; to globally grant permissions to all principals.
	 * @param permissions The set of permissions to be given. The parameter is a
	 *        bitmask of the permission constants defined in this class.
	 * @return a new {@code Acl} instance
	 * @throws IllegalArgumentException if {@code principal} is not a valid
	 *         principal name, if {@code permissions} is not a valid combination
	 *         of the permission constants defined in this class, or if a
	 *         globally granted permission would have been revoked from a
	 *         specific principal
	 */
	public synchronized Acl setPermission(String principal, int permissions) {
		checkPermissions(permissions);

		Acl newPermission = new Acl(this, principal, permissions);
		return newPermission;
	}

	/**
	 * Get the list of principals who have any kind of permissions on this node.
	 * The list only includes those principals that have been explicitly
	 * assigned permissions (so &quot;*&quot; is never returned), globally set
	 * permissions naturally apply to all other principals as well.
	 * 
	 * @return The array of principals having permissions on this node.
	 */
	public String[] getPrincipals() {
		return principalPermissions.keySet().toArray(new String[0]);
	}

	/**
	 * Give the canonical string representation of this ACL. The operations are
	 * in the following order: {Add, Delete, Exec, Get, Replace}, principal
	 * names are sorted alphabetically.
	 * 
	 * @return The string representation as defined in OMA DM.
	 */
	@Override
	public synchronized String toString() {
		String acl = null;
		for (int i = 0; i < PERMISSION_CODES.length; i++)
			acl = writeEntry(PERMISSION_CODES[i], acl);

		return (acl != null) ? acl : "";
	}

	// ----- Private utility methods -----//

	private String writeEntry(int command, String acl) {
		String aclEntry = null;

		if ((command & globalPermissions) > 0)
			aclEntry = ALL_PRINCIPALS;
		else {
			// TreeMap guarantees alphabetical ordering of keys during traversal
			Iterator<Entry<String, Integer>> i = principalPermissions.entrySet().iterator();
			while (i.hasNext()) {
				Map.Entry<String, Integer> entry = i.next();
				if ((command & entry.getValue().intValue()) > 0)
					aclEntry = appendEntry(aclEntry, '+', entry.getKey());
			}
		}

		if (aclEntry == null)
			return acl;

		return appendEntry(acl, '&', writeCommands(command) + '=' + aclEntry);
	}

	private static void deleteFromAll(TreeMap<String, Integer> principalPermissions, int perm) {
		Iterator<Entry<String, Integer>> i = principalPermissions.entrySet().iterator();
		while (i.hasNext()) {
			Map.Entry<String, Integer> entry = i.next();
			setPrincipalPermission(principalPermissions, entry.getKey(), entry.getValue().intValue() & ~perm);
		}
	}

	private static void setPrincipalPermission(TreeMap<String, Integer> principalPermissions, String principal, int perm) {
		if (perm == 0)
			principalPermissions.remove(principal);
		else
			principalPermissions.put(principal, Integer.valueOf(perm));
	}

	private static String writeCommands(int command) {
		String commandStr = null;
		for (int i = 0; i < PERMISSION_CODES.length; i++)
			if ((command & PERMISSION_CODES[i]) != 0)
				commandStr = appendEntry(commandStr, ',', PERMISSION_NAMES[i]);

		return (commandStr != null) ? commandStr : "";
	}

	private static String appendEntry(String base, char separator, String entry) {
		return (base != null) ? base + separator + entry : entry;
	}

	private static int parseCommand(String command) {
		int i = Arrays.asList(PERMISSION_NAMES).indexOf(command);
		if (i == -1)
			throw new IllegalArgumentException("Invalid ACL string: unknown command '" + command + "'.");

		return PERMISSION_CODES[i];
	}

	private static void checkPermissions(int perm) {
		if ((perm & ~ALL_PERMISSION) != 0)
			throw new IllegalArgumentException("Invalid ACL permission value: " + perm);
	}

	private static void checkPrincipal(String principal) {
		if (principal == null || principal.length() == 0)
			throw new IllegalArgumentException("Principal is null or empty.");

		checkServerId(principal, "Principal name contains illegal character");
	}

	private static void checkServerId(String serverId, String errorText) {
		char[] chars = serverId.toCharArray();
		for (int i = 0; i < chars.length; i++)
			if ("*=+&".indexOf(chars[i]) != -1 || Character.isWhitespace(chars[i]))
				throw new IllegalArgumentException(errorText + " '" + chars[i] + "'.");
	}

	private static String[] split(String input, char sep, int limit) {
		List<String> v = new ArrayList<>();
		boolean limited = (limit > 0);
		int applied = 0;
		int index = 0;
		StringBuilder part = new StringBuilder();

		while (index < input.length()) {
			char ch = input.charAt(index);
			if (ch != sep)
				part.append(ch);
			else {
				++applied;
				v.add(part.toString());
				part = new StringBuilder();
			}
			++index;
			if (limited && applied == limit - 1)
				break;
		}
		while (index < input.length()) {
			char ch = input.charAt(index);
			part.append(ch);
			++index;
		}
		v.add(part.toString());

		int last = v.size();
		if (0 == limit) {
			for (int j = v.size() - 1; j >= 0; --j) {
				String s = v.get(j);
				if ("".equals(s))
					--last;
				else
					break;
			}
		}

		String[] ret = new String[last];
		for (int i = 0; i < last; ++i)
			ret[i] = v.get(i);

		return ret;
	}
}
