/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright (c) 2010-2011 Oracle and/or its affiliates. All rights reserved.
 *
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common Development
 * and Distribution License("CDDL") (collectively, the "License").  You
 * may not use this file except in compliance with the License.  You can
 * obtain a copy of the License at
 * http://glassfish.java.net/public/CDDL+GPL_1_1.html
 * or packager/legal/LICENSE.txt.  See the License for the specific
 * language governing permissions and limitations under the License.
 *
 * When distributing the software, include this License Header Notice in each
 * file and include the License file at packager/legal/LICENSE.txt.
 *
 * GPL Classpath Exception:
 * Oracle designates this particular file as subject to the "Classpath"
 * exception as provided by Oracle in the GPL Version 2 section of the License
 * file that accompanied this code.
 *
 * Modifications:
 * If applicable, add the following below the License Header, with the fields
 * enclosed by brackets [] replaced by your own identifying information:
 * "Portions Copyright [year] [name of copyright owner]"
 *
 * Contributor(s):
 * If you wish your version of this file to be governed by only the CDDL or
 * only the GPL Version 2, indicate your decision by adding "[Contributor]
 * elects to include this software in this distribution under the [CDDL or GPL
 * Version 2] license."  If you don't indicate a single choice of license, a
 * recipient has the option to distribute your version of this file under
 * either the CDDL, the GPL Version 2 or to extend the choice of license to
 * its licensees as provided above.  However, if you add GPL Version 2 code
 * and therefore, elected the GPL Version 2 license, then the option applies
 * only if the new code is made subject to such option by the copyright
 * holder.
 */

package com.sun.jersey.api.uri;

import java.util.List;
import java.util.Map;
import java.util.regex.MatchResult;
import java.util.regex.Matcher;
import java.util.regex.Pattern;


/**
 * A URI pattern for matching a URI against a regular expression
 * and returning capturing group values for any capturing groups present in
 * the expression.
 *
 * @author Paul.Sandoz@Sun.Com
 */
public class UriPattern {
    /**
     * The empty URI pattern that matches the null or empty URI path
     */
    public static final UriPattern EMPTY = new UriPattern();
    
    /**
     * The regular expression for matching URIs 
     * and obtaining capturing group values.
     */
    private final String regex;
    
    /**
     * The compiled regular expression of {@link #regex}
     */
    private final Pattern regexPattern;
    
    private final int[] groupIndexes;
    
    /**
     *  Construct an empty pattern.
     */
    protected UriPattern() {
        this.regex = "";
        this.regexPattern = null;
        this.groupIndexes = null;
    }

    /**
     * Construct a new URI pattern.
     * 
     * @param regex the regular expression. If the expression is null or an
     *        empty string then the pattern will only match a null or empty
     *        URI path.
     * @throws java.util.regex.PatternSyntaxException if the
     *         regular expression could not be compiled
     */
    public UriPattern(String regex) {
        this(regex, UriTemplateParser.EMPTY_INT_ARRAY);
    }

    /**
     * Construct a new URI pattern.
     * 
     * @param regex the regular expression. If the expression is null or an
     *        empty string then the pattern will only match a null or empty
     *        URI path.
     * @param groupIndexes the array of group indexes to capturing groups.
     * @throws java.util.regex.PatternSyntaxException if the
     *         regular expression could not be compiled
     */
    public UriPattern(String regex, int[] groupIndexes) {
        this(compile(regex), groupIndexes);
    }
    
    private static Pattern compile(String regex) {
        return (regex == null || regex.length() == 0) ? null : Pattern.compile(regex);
    }

    /**
     * Construct a new URI pattern.
     * 
     * @param regexPattern  the regular expression pattern
     * @throws IllegalArgumentException if the regexPattern is null.
     */
    public UriPattern(Pattern regexPattern) {
        this(regexPattern, UriTemplateParser.EMPTY_INT_ARRAY);
    }
    
    /**
     * Construct a new URI pattern.
     * 
     * @param regexPattern the regular expression pattern
     * @param groupIndexes the array of group indexes to capturing groups.
     * @throws IllegalArgumentException if the regexPattern is null.
     */
    public UriPattern(Pattern regexPattern, int[] groupIndexes) {
        if (regexPattern == null)
            throw new IllegalArgumentException();
        
        this.regex = regexPattern.toString();
        this.regexPattern = regexPattern;
        this.groupIndexes = groupIndexes;
    }

    /**
     * Get the regular expression.
     * 
     * @return the regular expression.
     */
    public final String getRegex() {
        return regex;
    }

    /**
     * Get the group indexes.
     *
     * @return the group indexes.
     */
    public final int[] getGroupIndexes() {
        return groupIndexes;
    }
    
    private static final class EmptyStringMatchResult implements MatchResult {
        public int start() {
            return 0;
        }

        public int start(int group) {
            if (group != 0)
                throw new IndexOutOfBoundsException();
            return start();
        }

        public int end() {
            return 0;
        }

        public int end(int group) {
            if (group != 0)
                throw new IndexOutOfBoundsException();
            return end();
        }

        public String group() {
            return "";
        }

        public String group(int group) {
            if (group != 0)
                throw new IndexOutOfBoundsException();
            return group();
        }

        public int groupCount() {
            return 0;
        }        
    }

    private static final EmptyStringMatchResult EMPTY_STRING_MATCH_RESULT = new EmptyStringMatchResult();
    
    private final class GroupIndexMatchResult implements MatchResult {
        private final MatchResult r;
        
        GroupIndexMatchResult(MatchResult r) {
            this.r = r;
        }
        
        public int start() {
            return r.start();
        }

        public int start(int group) {
            if (group > groupCount())
                throw new IndexOutOfBoundsException();
            
            return (group > 0) ? r.start(groupIndexes[group - 1]) : r.start();
        }

        public int end() {
            return r.end();
        }

        public int end(int group) {
            if (group > groupCount())
                throw new IndexOutOfBoundsException();

            return (group > 0) ? r.end(groupIndexes[group - 1]) : r.end();
        }

        public String group() {
            return r.group();
        }

        public String group(int group) {
            if (group > groupCount())
                throw new IndexOutOfBoundsException();

            return (group > 0) ? r.group(groupIndexes[group - 1]) : r.group();
        }

        public int groupCount() {
            return groupIndexes.length - 1;
        }        
    }

    /**
     * Match a URI against the pattern.
     * 
     * @param uri the uri to match against the template.
     * @return the match result, otherwise null if no match occurs.
     */
    public final MatchResult match(CharSequence uri) {
        // Check for match against the empty pattern
        if (uri == null || uri.length() == 0)
            return (regexPattern == null) ? EMPTY_STRING_MATCH_RESULT : null;
        else if (regexPattern == null)
            return null;
        
        // Match the URI to the URI template regular expression
        Matcher m = regexPattern.matcher(uri);
        if (!m.matches())
            return null;

        return (groupIndexes.length > 0) ? new GroupIndexMatchResult(m) : m;
    }
    
    /**
     * Match a URI against the pattern.
     * <p>
     * If the URI matches against the pattern then the capturing group values
     * (if any) will be added to a list passed in as parameter.
     * 
     * @param uri the uri to match against the template.
     * @param groupValues the list to add the values of a pattern's 
     *        capturing groups if matching is successful. The values are added
     *        in the same order as the pattern's capturing groups. The list
     *        is cleared before values are added.
     * @return true if the URI matches the pattern, otherwise false.
     * @throws IllegalArgumentException if the uri or 
     *         capturingGroupValues is null.
     */
    public final boolean match(CharSequence uri, List<String> groupValues) {
        if (groupValues == null) 
            throw new IllegalArgumentException();

        // Check for match against the empty pattern
        if (uri == null || uri.length() == 0)
            return (regexPattern == null) ? true : false;
        else if (regexPattern == null)
            return false;
                
        // Match the URI to the URI template regular expression
        Matcher m = regexPattern.matcher(uri);
        if (!m.matches())
            return false;

        groupValues.clear();
        if (groupIndexes.length > 0) {
            for (int i = 0; i < groupIndexes.length - 1; i++) {
                groupValues.add(m.group(groupIndexes[i]));
            }
        } else {
            for (int i = 1; i <= m.groupCount(); i++) {
                groupValues.add(m.group(i));
            }
        }
        
        // TODO check for consistency of different capturing groups
        // that must have the same value
        
        return true;
    }
    
    /**
     * Match a URI against the pattern.
     * <p>
     * If the URI matches against the pattern then the capturing group values
     * (if any) will be added to a map passed in as parameter.
     * 
     * @param uri the uri to match against the template.
     * @param groupNames the list names associated with a pattern's 
     *        capturing groups. The names MUST be in the same order as the 
     *        pattern's capturing groups and the size MUST be equal to or
     *        less than the number of capturing groups.
     * @param groupValues the map to add the values of a pattern's 
     *        capturing groups if matching is successful. A values is put
     *        into the map using the group name associated with the 
     *        capturing group. The map is cleared before values are added.
     * @return true if the URI matches the pattern, otherwise false.
     * @throws IllegalArgumentException if the uri or 
     *         capturingGroupValues is null.
     */
    public final boolean match(CharSequence uri, 
            List<String> groupNames, Map<String, String> groupValues) {
        if (groupValues == null) 
            throw new IllegalArgumentException();

        // Check for match against the empty pattern
        if (uri == null || uri.length() == 0)
            return (regexPattern == null) ? true : false;
        else if (regexPattern == null)
            return false;
        
        // Match the URI to the URI template regular expression
        Matcher m = regexPattern.matcher(uri);
        if (!m.matches())
            return false;
        
        // Assign the matched group values to group names
        groupValues.clear();
        for (int i = 0; i < groupNames.size(); i++) {
            String name  = groupNames.get(i);
            String currentValue = m.group((groupIndexes.length > 0) ? groupIndexes[i] : i + 1);
            
            // Group names can have the same name occuring more than once, 
            // check that groups values are same.
            String previousValue = groupValues.get(name);
            if (previousValue != null && !previousValue.equals(currentValue))
                return false;
            
            groupValues.put(name, currentValue);
        }
        
        return true;
    }
    
    @Override
    public final int hashCode() {
        return regex.hashCode();
    }

    @Override
    public final boolean equals(Object obj) {
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        final UriPattern that = (UriPattern) obj;
        if (this.regex != that.regex && 
                (this.regex == null || !this.regex.equals(that.regex))) {
            return false;
        }
        return true;
    }
    
    @Override
    public final String toString() {
        return regex;
    }
}
