/**
 * 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.activemq.shiro.authz;

import org.apache.shiro.authz.Permission;
import org.apache.shiro.authz.permission.WildcardPermission;

import java.lang.reflect.Method;
import java.util.List;
import java.util.Set;

/**
 * @since 5.10.0
 */
public class ActiveMQWildcardPermission extends WildcardPermission {

    private final boolean caseSensitive;

    public ActiveMQWildcardPermission(String wildcardString) {
        this(wildcardString, true);
    }

    public ActiveMQWildcardPermission(String wildcardString, boolean caseSensitive) {
        super(wildcardString, caseSensitive);
        this.caseSensitive = caseSensitive;
    }

    @Override
    public boolean implies(Permission p) {
        // By default only supports comparisons with other WildcardPermissions
        if (!(p instanceof WildcardPermission)) {
            return false;
        }

        WildcardPermission wp = (WildcardPermission) p;

        List<Set<String>> otherParts = getParts(wp);

        int i = 0;
        for (Set<String> otherPart : otherParts) {
            // If this permission has less parts than the other permission, everything after the number of parts contained
            // in this permission is automatically implied, so return true
            if (getParts().size() - 1 < i) {
                return true;
            } else {
                Set<String> thisPart = getParts().get(i);

                for (String token : thisPart) {
                    if (token.equals(WILDCARD_TOKEN)) {
                        continue;
                    }
                    for (String otherToken : otherPart) {
                        if (!caseSensitive) {
                            otherToken = otherToken.toLowerCase();
                        }
                        if (!matches(token, otherToken)) {
                            return false;
                        }
                    }
                }
                i++;
            }
        }

        // If this permission has more parts than the other parts, only imply it if all of the other parts are wildcards
        for (; i < getParts().size(); i++) {
            Set<String> part = getParts().get(i);
            if (!part.contains(WILDCARD_TOKEN)) {
                return false;
            }
        }

        return true;
    }

    /**
     * Tests whether or not a string matches against a pattern.
     * The pattern may contain two special characters:<br>
     * '*' means zero or more characters<br>
     * '?' means one and only one character
     *
     * @param pattern pattern to match against.
     *                Must not be <code>null</code>.
     * @param value   string which must be matched against the pattern.
     *                Must not be <code>null</code>.
     * @return <code>true</code> if the string matches against the
     *         pattern, or <code>false</code> otherwise.
     */
    protected boolean matches(String pattern, String value) {

        char[] patArr = pattern.toCharArray();
        char[] valArr = value.toCharArray();
        int patIndex = 0;
        int patEndIndex = patArr.length - 1;
        int valIndex = 0;
        int valEndIndex = valArr.length - 1;
        char ch;

        boolean patternContainsStar = false;
        for (char patternChar : patArr) {
            if (patternChar == '*') {
                patternContainsStar = true;
                break;
            }
        }

        if (!patternContainsStar) {
            // No '*'s, so we make a shortcut
            if (patEndIndex != valEndIndex) {
                return false; // Pattern and string do not have the same size
            }
            for (int i = 0; i <= patEndIndex; i++) {
                ch = patArr[i];
                if (ch != '?') {
                    if (ch != valArr[i]) {
                        return false;// Character mismatch
                    }
                }
            }
            return true; // String matches against pattern
        }


        // Process characters before first star
        while ((ch = patArr[patIndex]) != '*' && valIndex <= valEndIndex) {
            if (ch != '?') {
                if (ch != valArr[valIndex]) {
                    return false;// Character mismatch
                }
            }
            patIndex++;
            valIndex++;
        }
        if (valIndex > valEndIndex) {
            // All characters in the value are used. Check if only '*'s remain
            // in the pattern. If so, we succeeded. Otherwise failure.
            for (int i = patIndex; i <= patEndIndex; i++) {
                if (patArr[i] != '*') {
                    return false;
                }
            }
            return true;
        }

        // Process characters after last star
        while ((ch = patArr[patEndIndex]) != '*' && valIndex <= valEndIndex) {
            if (ch != '?') {
                if (ch != valArr[valEndIndex]) {
                    return false;// Character mismatch
                }
            }
            patEndIndex--;
            valEndIndex--;
        }
        if (valIndex > valEndIndex) {
            // All characters in the value are used. Check if only '*'s remain
            // in the pattern. If so, we succeeded. Otherwise failure.
            for (int i = patIndex; i <= patEndIndex; i++) {
                if (patArr[i] != '*') {
                    return false;
                }
            }
            return true;
        }

        // process pattern between stars. patIndex and patEndIndex always point to a '*'.
        while (patIndex != patEndIndex && valIndex <= valEndIndex) {
            int innerPatternIndex = -1;
            for (int i = patIndex + 1; i <= patEndIndex; i++) {
                if (patArr[i] == '*') {
                    innerPatternIndex = i;
                    break;
                }
            }
            if (innerPatternIndex == patIndex + 1) {
                // Two stars next to each other, skip the first one.
                patIndex++;
                continue;
            }
            // Find the pattern between patIndex & innerPatternIndex in the value between
            // valIndex and valEndIndex
            int innerPatternLength = (innerPatternIndex - patIndex - 1);
            int innerValueLength = (valEndIndex - valIndex + 1);
            int foundIndex = -1;
            innerValueLoop:
            for (int i = 0; i <= innerValueLength - innerPatternLength; i++) {
                for (int j = 0; j < innerPatternLength; j++) {
                    ch = patArr[patIndex + j + 1];
                    if (ch != '?') {
                        if (ch != valArr[valIndex + i + j]) {
                            continue innerValueLoop;
                        }
                    }
                }

                foundIndex = valIndex + i;
                break;
            }

            if (foundIndex == -1) {
                return false;
            }

            patIndex = innerPatternIndex;
            valIndex = foundIndex + innerPatternLength;
        }

        // All characters in the string are used. Check if only '*'s are left
        // in the pattern. If so, we succeeded. Otherwise failure.
        for (int i = patIndex; i <= patEndIndex; i++) {
            if (patArr[i] != '*') {
                return false;
            }
        }

        return true;
    }

    protected List<Set<String>> getParts(WildcardPermission wp) {
        if (wp instanceof ActiveMQWildcardPermission) {
            return ((ActiveMQWildcardPermission) wp).getParts();
        } else {
            return getPartsByReflection(wp);
        }
    }

    protected List<Set<String>> getPartsByReflection(WildcardPermission wp) {
        try {
            return doGetPartsByReflection(wp);
        } catch (Exception e) {
            String msg = "Unable to obtain WildcardPermission instance's 'parts' value.";
            throw new IllegalStateException(msg, e);
        }
    }

    @SuppressWarnings("unchecked")
    protected List<Set<String>> doGetPartsByReflection(WildcardPermission wp) throws Exception {
        Method getParts = WildcardPermission.class.getDeclaredMethod("getParts");
        getParts.setAccessible(true);
        return (List<Set<String>>) getParts.invoke(wp);
    }

    @Override
    public String toString() {
        StringBuilder buffer = new StringBuilder();
        for (Set<String> part : getParts()) {
            if (buffer.length() > 0) {
                buffer.append(":");
            }
            boolean first = true;
            for (String token : part) {
                if (!first) {
                    buffer.append(",");
                }
                buffer.append(token);
                first = false;
            }
        }
        return buffer.toString();
    }
}
