/*
 * 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.karaf.management.tools;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.Dictionary;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.List;
import java.util.SortedMap;
import java.util.TreeMap;

public class ACLConfigurationParser {

    // note that the order of the enums is important. Needs to be from most specific to least specific.
    public enum Specificity {
        ARGUMENT_MATCH,
        SIGNATURE_MATCH,
        NAME_MATCH,
        WILDCARD_MATCH,
        NO_MATCH
    };

    /**
     * Returns the roles that can invoke the given operation. This is determined by matching the
     * operation details against configuration provided.<p/>
     *
     * The following configuration is supported. Keys are used to match an invocation against. The value can contain
     * a comma-separated list of roles. Spaces are ignored for the role values. Note that comments are allowed in the
     * value field after the hash {@code #} character:
     * <pre>
     *     {@code
     *     myMethod = role1, role2
     *     methodName(int)[/17/] = role1                    # regex match, assume it's surrounded by ^ and $
     *     methodName(int)[/[01]8/] = role2
     *     methodName(int)["19"] = role3                    # exact value match
     *     methodName(int) = role4                          # signature match
     *     methodName(java.lang.String, int) = role5        # signature match
     *     methodName =                                     # no roles can invoke this command
     *     method* = role6                                  # name prefix/wildcard match. The asterisk must appear at the end.
     *     }
     * </pre>
     *
     * The following algorithm is used to find matching roles:
     * <ol>
     *     <li>Find all regex and exact value matches. For all parameters these matches are found by calling {@code toString()}
     *         on the parameters passed in. If there are multiple matches in this category all the matching roles are collected.
     *         If any is found return these roles.
     *     </li>
     *     <li>Find a signature match. If found return the associated roles.</li>
     *     <li>Find a method name match. If found return the associated roles.</li>
     *     <li>Find a method name prefix/wildcard match. If more than one prefix match, the roles associated with the longest
     *         prefix is used. So for example, if there are rules for {@code get*} and {@code *} only the roles associated with
     *         {@code get*} are returned.
     *     </li>
     *     <li>If none of the above criteria match, this method returns {@code null}.</li>
     * </ol>
     *
     * @param methodName the method name to be invoked.
     * @param params the parameters provided for the invocation. May be {@code null} for cases there the parameters are not yet
     *               known. In this case the roles that can <em>potentially</em> invoke the method are returned, although based on
     *               parameter values the actual invocation may still be denied.
     * @param signature the signature of the method specified as an array of class name. For simple types, the simple type name
     *                  is used (e.g. "int").
     * @param config the configuration to check against.
     * @param addToRoles the list of roles (which may be empty) if a matching configuration iteam has been found.
     * @return the specificity
     */
    public static Specificity getRolesForInvocation(String methodName, Object[] params, String[] signature,
                                                    Dictionary<String, Object> config, List<String> addToRoles) {
        Dictionary<String, Object> properties = trimKeys(config);

        Specificity s = getRolesBasedOnSignature(methodName, params, signature, properties, addToRoles);
        if (s != Specificity.NO_MATCH) {
            return s;
        }

        s = getRolesBasedOnSignature(methodName, params, null, properties, addToRoles);
        if (s != Specificity.NO_MATCH) {
            return s;
        }

        List<String> roles = getMethodNameWildcardRoles(properties, methodName);
        if (roles != null) {
            addToRoles.addAll(roles);
            return Specificity.WILDCARD_MATCH;
        } else {
            return Specificity.NO_MATCH;
        }
    }

    private static Specificity getRolesBasedOnSignature(String methodName, Object[] params, String[] signature,
                                                        Dictionary<String, Object> properties, List<String> roles) {
        if (params != null) {
            boolean foundExactOrRegex = false;
            Object exactArgMatchRoles = properties.get(getExactArgSignature(methodName, signature, params));
            if (exactArgMatchRoles instanceof String) {
                roles.addAll(parseRoles((String) exactArgMatchRoles));
                foundExactOrRegex = true;
            }

            List<String> r = getRegexRoles(properties, methodName, signature, params);
            if (r != null) {
                foundExactOrRegex = true;
                roles.addAll(r);
            }

            if (foundExactOrRegex) {
                // since we have the actual parameters we can match them and if they do we won't look for any
                // more generic rules...
                return Specificity.ARGUMENT_MATCH;
            }
        } else {
            // this is used in the case where parameters aren't known yet and the system wants to find out
            // what roles in principle can invoke this method
            List<String> r = getExactArgOrRegexRoles(properties, methodName, signature);
            if (r != null) {
               roles.addAll(r);
            }
        }

        Object signatureRoles = properties.get(getSignature(methodName, signature));
        if (signatureRoles instanceof String) {
            roles.addAll(parseRoles((String) signatureRoles));
            return signature == null ? Specificity.NAME_MATCH : Specificity.SIGNATURE_MATCH;
        }

        return Specificity.NO_MATCH;
    }

    private static Dictionary<String, Object> trimKeys(Dictionary<String, Object> properties) {
        Dictionary<String, Object> d = new Hashtable<String, Object>();
        for (Enumeration<String> e = properties.keys(); e.hasMoreElements(); ) {
            String key = e.nextElement();
            Object value = properties.get(key);
            d.put(removeSpaces(key), value);
        }
        return d;
    }

    private static String removeSpaces(String key) {
        StringBuilder sb = new StringBuilder();
        char quoteChar = 0;
        for (int i = 0; i < key.length(); i++) {
            char c = key.charAt(i);

            if (quoteChar == 0 && c == ' ')
                continue;

            if (quoteChar == 0 && (c == '\"' || c == '/') && sb.length() > 0 &&
                    (sb.charAt(sb.length() - 1) == '[' || sb.charAt(sb.length() - 1) == ',')) {
                // we're in a quoted string
                quoteChar = c;
            } else if (quoteChar != 0 && c == quoteChar) {
                // look ahead to see if the next non-space is the closing bracket or a comma, which ends the quoted string
                for (int j = i + 1; j < key.length(); j++) {
                    if (key.charAt(j) == ' ')
                        continue;
                    if (key.charAt(j) == ']' || key.charAt(j) == ',')
                        quoteChar = 0;
                    break;
                }
            }
            sb.append(c);
        }
        return sb.toString();
    }

    public static List<String> parseRoles(String roleStr) {
        int hashIdx = roleStr.indexOf('#');
        if (hashIdx >= 0) {
            // you can put a comment at the end
            roleStr = roleStr.substring(0, hashIdx);
        }

        List<String> roles = new ArrayList<String>();
        for (String role : roleStr.split("[,]")) {
            String trimmed = role.trim();
            if (trimmed.length() > 0) {
                roles.add(trimmed);
            }
        }

        return roles;
    }

    private static Object getExactArgSignature(String methodName, String[] signature, Object[] params) {
        StringBuilder sb = new StringBuilder(getSignature(methodName, signature));
        sb.append('[');
        boolean first = true;
        for (Object param : params) {
            if (first)
                first = false;
            else
                sb.append(',');
            sb.append('"');
            if (param != null)
                sb.append(param.toString().trim());
            sb.append('"');
        }
        sb.append(']');
        return sb.toString();
    }

    private static String getSignature(String methodName, String[] signature) {
        StringBuilder sb = new StringBuilder(methodName);
        if (signature == null)
            return sb.toString();

        sb.append('(');
        boolean first = true;
        for (String s : signature) {
            if (first)
                first = false;
            else
                sb.append(',');
            sb.append(s);
        }
        sb.append(')');
        return sb.toString();
    }

    private static List<String> getRegexRoles(Dictionary<String, Object> properties, String methodName, String[] signature, Object[] params) {
        List<String> roles = new ArrayList<String>();
        boolean matchFound = false;
        String methodSig = getSignature(methodName, signature);
        String prefix = methodSig + "[/";
        for (Enumeration<String> e = properties.keys(); e.hasMoreElements(); ) {
            String key = e.nextElement().trim();
            if (key.startsWith(prefix) && key.endsWith("/]")) {
                List<String> regexArgs = getRegexDecl(key.substring(methodSig.length()));
                if (allParamsMatch(regexArgs, params)) {
                    matchFound = true;
                    Object roleStr = properties.get(key);
                    if (roleStr instanceof String) {
                        roles.addAll(parseRoles((String) roleStr));
                    }
                }
            }
        }
        return matchFound ? roles : null;
    }

    private static List<String> getExactArgOrRegexRoles(Dictionary<String, Object> properties, String methodName, String[] signature) {
        List<String> roles = new ArrayList<String>();
        boolean matchFound = false;
        String methodSig = getSignature(methodName, signature);
        String prefix = methodSig + "[";
        for (Enumeration<String> e = properties.keys(); e.hasMoreElements(); ) {
            String key = e.nextElement().trim();
            if (key.startsWith(prefix) && key.endsWith("]")) {
                matchFound = true;
                Object roleStr = properties.get(key);
                if (roleStr instanceof String) {
                    roles.addAll(parseRoles((String) roleStr));
                }
            }
        }
        return matchFound ? roles : null;
    }

    private static List<String> getMethodNameWildcardRoles(Dictionary<String, Object> properties, String methodName) {
        SortedMap<String, String> wildcardRules = new TreeMap<String, String>(new Comparator<String>() {
            public int compare(String s1, String s2) {
                // returns longer entries before shorter ones...
                return s2.length() - s1.length();
            }
        });
        for (Enumeration<String> e = properties.keys(); e.hasMoreElements(); ) {
            String key = e.nextElement();
            if (key.endsWith("*")) {
                String prefix = key.substring(0, key.length() - 1);
                if (methodName.startsWith(prefix)) {
                    wildcardRules.put(prefix, properties.get(key).toString());
                }
            }
            if (key.startsWith("*")) {
                String suffix = key.substring(1);
                if (methodName.endsWith(suffix)) {
                    wildcardRules.put(suffix, properties.get(key).toString());
                }
            }
            if (key.startsWith("*") && key.endsWith("*") && key.length() > 1) {
                String middle = key.substring(1, key.length() - 1);
                if (methodName.contains(middle)) {
                    wildcardRules.put(middle, properties.get(key).toString());
                }
            }
        }

        if (wildcardRules.size() != 0) {
            return parseRoles(wildcardRules.values().iterator().next());
        } else {
            return null;
        }
    }

    private static boolean allParamsMatch(List<String> regexArgs, Object[] params) {
        if (regexArgs.size() != params.length)
            return false;

        for (int i = 0; i < regexArgs.size(); i++) {
            if (params[i] == null)
                return false;
            if (!params[i].toString().trim().matches(regexArgs.get(i))) {
                return false;
            }
        }

        return true;
    }

    private static List<String> getRegexDecl(String key) {
        List<String> l = new ArrayList<String>();

        boolean inRegex = false;
        StringBuilder curRegex = new StringBuilder();
        for (int i = 0; i < key.length(); i++) {
            if (!inRegex) {
                if (key.length() > i + 1) {
                    String s = key.substring(i, i + 2);
                    if ("[/".equals(s) || ",/".equals(s)) {
                        inRegex = true;
                        i++;
                        continue;
                    }
                }
            } else {
                String s = key.substring(i, i + 2);
                if ("/]".equals(s) || "/,".equals(s)) {
                    l.add(curRegex.toString());
                    curRegex = new StringBuilder();
                    inRegex = false;
                    continue;
                }
                curRegex.append(key.charAt(i));
            }
        }
        return l;
    }
}
