/*
 * 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.ode.utils;

import org.apache.commons.httpclient.URIException;
import org.apache.commons.httpclient.util.URIUtil;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * A partial implementation of URI Template expansion
 * as specified by the <a href="http://bitworking.org/projects/URI-Templates/spec/draft-gregorio-uritemplate-03.html">URI template specification</a>.
 * <p/><strong>Limitations</strong>
 * <br/>The only operation implemented so far is <a href="http://bitworking.org/projects/URI-Templates/spec/draft-gregorio-uritemplate-03.html#var">Var substitution</a>. If an expansion template for another operation (join, neg, opt, etc) is found,
 * an {@link UnsupportedOperationException} is thrown.
 * <p/>
 * <p/>
 * <p/><strong>Escaping Considerations</strong>
 * <br/>Replacement and default values are escaped. All characters except unreserved (as defined by <a href="http://tools.ietf.org/html/rfc2396#appendix-A">rfc2396</a>) are escaped.
 * <br/> unreserved    = alphanum | mark
 * <br/> mark          = "-" | "_" | "." | "!" | "~" | "*" | "'" |  "(" | ")"
 * <p/>
 * <a href="http://tools.ietf.org/html/rfc2396">Rfc2396</a> is used to be compliant with {@linkplain java.net.URI java.net.URI}.
 * <p/>
 * <p/><strong>Examples:</strong>
 * <br/>
 * Given the following template variable names and values:
 * <ul>
 * <li>foo = tag</li>
 * <li>bar = java</li>
 * <li>name = null</li>
 * <li>date = 2008/05/09</li>
 * </ul>
 * <p/>The following URI Templates will be expanded as shown:
 * <br/>http://example.com/{foo}/{bar}.{format=xml}
 * <br/>http://example.com/tag/java.xml
 * <br/>
 * <br/>http://example.com/tag/java.{format}
 * <br/>http://example.com/tag/java.
 * <br/>
 * <br/>http://example.com/{foo}/{name}
 * <br/>http://example.com/tag/
 * <br/>
 * <br/>http://example.com/{foo}/{name=james}
 * <br/>http://example.com/tag/james
 * <br/>
 * <br/>http://example.org/{date}
 * <br/>http://example.org/2008%2F05%2F09
 * <br/>
 * <br/>http://example.org/{-join|&|foo,bar,xyzzy,baz}/{date}
 * <br/>--> UnsupportedOperationException
 *
 * @author <a href="mailto:midon@intalio.com">Alexis Midon</a>
 * @see #varSubstitution(String, Object[], java.util.Map)
 */

public class URITemplate {

    private static final Log log = LogFactory.getLog(URITemplate.class);


    public static final String EXPANSION_REGEX = "\\{[^\\}]+\\}";
    // compiled pattern of the regex
    private static final Pattern PATTERN = Pattern.compile(EXPANSION_REGEX);

    /**
     * Implements the function describes in <a href="http://bitworking.org/projects/URI-Templates/spec/draft-gregorio-uritemplate-03.html#appendix_a">the spec</a>
     *
     * @param expansion, an expansion template (with the surrounding braces)
     * @return an array of object containing the operation name, the operation argument, a map of <var, default value (null if none)>
     */
    public static Object[] parseExpansion(String expansion) {
        // remove surrounding braces if any
        if (expansion.matches(EXPANSION_REGEX)) {
            expansion = expansion.substring(1, expansion.length() - 1);
        }
        String[] r;
        if (expansion.contains("|")) {
            // (op, arg, vars)
            r = expansion.split("\\|", -1);
            // remove the leading '-' of the operation
            r[0] = r[0].substring(1);
        } else {
            r = new String[]{null, null, expansion};
        }

        // parse the vars
        Map vars = new HashMap();
        String[] var = r[2].split(",");
        for (String s : var) {
            if (s.contains("=")) {
                String[] a = s.split("=");
                vars.put(a[0], a[1]);
            } else {
                vars.put(s, null);
            }
        }
        // op, arg, vars
        return new Object[]{r[0], r[1], vars};
    }

    /**
     * Simply build a map from nameValuePairs and pass it to {@link #expand(String, java.util.Map)}
     *
     * @param nameValuePairs an array containing of name, value, name, value, and so on.  Null values are allowed.
     * @see # expand (String, java.util.Map)
     */
    public static String expand(String uriTemplate, String... nameValuePairs) throws URIException, UnsupportedOperationException {
        return expand(uriTemplate, toMap(nameValuePairs));
    }

    /**
     * A partial implementation of URI Template expansion
     * as specified by the <a href="http://bitworking.org/projects/URI-Templates/spec/draft-gregorio-uritemplate-03.html">URI template specification</a>.
     * <p/>
     * The only operation implemented as of today is "Var Substitution". If an expansion template for another operation (join, neg, opt, etc) is found,
     * an {@link UnsupportedOperationException} will be thrown.
     * <p/>
     * See {@link #varSubstitution(String, Object[], java.util.Map)}
     *
     * @param uriTemplate    the URI template
     * @param nameValuePairs a Map of &lt;name, value&gt;. Null values are allowed.
     * @return a copy of uri template in which substitutions have been made (if possible)
     * @throws URIException                  if the default protocol charset is not supported
     * @throws UnsupportedOperationException if the operation is not supported. Currently only var substitution is supported.
     * @see #varSubstitution(String, Object[], java.util.Map)
     */
    public static String expand(String uriTemplate, Map<String, String> nameValuePairs) throws URIException, UnsupportedOperationException {
        return expand(uriTemplate, nameValuePairs, false);
    }

    /**
     * Same as {@link #expand(String, java.util.Map)} but preserve an expansion template if the corresponding variable
     * is not defined in the {@code nameValuePairs} map (i.e. map.contains(var)==false).
     * <br/>Meaning that a template may be returned.
     * <br/> If a default value exists for the undefined value, it will be used to replace the expansion pattern.
     * <p/>
     * <strong>Beware that this behavior deviates from the URI Template specification.</strong>
     * <p/>
     * For instance:
     * <br/>Given the following template variable names and values:
     * <ul>
     * <li>bar = java</li>
     * <li>foo undefined
     * </ul>
     * <p/>The following expansion templates will be expanded as shown if {@code preserveUndefinedVar} is true:
     * <br/>http://example.com/{bar}
     * <br/>http://example.com/java
     * <br/>
     * <br/>{foo=a_default_value}
     * <br/>a_default_value
     * <br/>
     * <br/>http://example.com/{bar}/{foo}
     * <br/>http://example.com/java/{foo}
     *
     * @see #expand(String, java.util.Map)
     */
    public static String expandLazily(String uriTemplate, Map<String, String> nameValuePairs) throws URIException, UnsupportedOperationException {
        return expand(uriTemplate, nameValuePairs, true);
    }

    /**
     * @see #expandLazily(String, java.util.Map)
     */
    public static String expandLazily(String uriTemplate, String... nameValuePairs) throws URIException {
        return expandLazily(uriTemplate, toMap(nameValuePairs));
    }


    /**
     * @see #varSubstitution(String, Object[], java.util.Map, boolean)
     * @see #expandLazily(String, String[])
     */
    private static String expand(String uriTemplate, Map<String, String> nameValuePairs, boolean preserveUndefinedVar) throws URIException, UnsupportedOperationException {
        Matcher m = PATTERN.matcher(uriTemplate);
        // Strings are immutable in java
        // so let's use a buffer, and append all substrings between 2 matches and the replacement value for each match 
        StringBuilder sb = new StringBuilder(uriTemplate.length());
        int prevEnd = 0;
        while (m.find()) {
            // append the string between two matches
            sb.append(uriTemplate.substring(prevEnd, m.start()));
            prevEnd = m.end();

            // expansion pattern with braces
            String expansionPattern = uriTemplate.substring(m.start(), m.end());
            Object[] expansionInfo = parseExpansion(expansionPattern);
            String operationName = (String) expansionInfo[0];
            // here we have to know which operation apply
            if (operationName != null) {
                final String msg = "Operation not supported [" + operationName + "]. This expansion pattern [" + expansionPattern + "] is not valid.";
                if (log.isWarnEnabled()) log.warn(msg);
                throw new UnsupportedOperationException(msg);
            } else {
                // here we care only for var substitution, i.e expansion patterns with no operation name
                sb.append(varSubstitution(expansionPattern, expansionInfo, nameValuePairs, preserveUndefinedVar));
            }

        }
        if (sb.length() == 0) {
            // return the template itself if no match (String are immutable in java, no need to clone the template)
            return uriTemplate;
        } else {
            // don't forget the remaining part
            sb.append(uriTemplate.substring(prevEnd, uriTemplate.length()));
            return sb.toString();
        }
    }

    /**
     * An implementation of var substitution as defined by the
     * <a href="http://bitworking.org/projects/URI-Templates/spec/draft-gregorio-uritemplate-03.html#var">URI template specification</a>.
     * <p/>
     * If for a given variable, the variable is in the name/value map but the associated value is null. The variable will be replaced with an empty string or with the default value if any.
     *
     * @param expansionPattern an expansion pattern (not a uri template) e.g. "{foo}"
     * @param expansionInfo    the result of {@link #parseExpansion(String)} for the given expansion pattern
     * @param nameValuePairs   the Map<String, String> of names and associated values. May containt null values.
     * @return the expanded string, properly escaped.
     * @throws URIException if an encoding exception occured
     * @see org.apache.commons.httpclient.util.URIUtil#encodeWithinQuery(String)
     * @see java.net.URI
     */
    public static String varSubstitution(String expansionPattern, Object[] expansionInfo, Map<String, String> nameValuePairs) throws URIException {
        return varSubstitution(expansionPattern, expansionInfo, nameValuePairs, false);
    }

    /**
     * Same as {@link #varSubstitution(String, Object[], java.util.Map)} but the {@code preserveUndefinedVar} boolean
     * argument (if {@code true}) allows to preserve an expansion template if the corresponding variable is not defined in the {@code nameValuePairs} map (i.e. map.contains(var)==false).
     * <br/> If a default value exists for the undefined value, it will be used to replace the expansion pattern.
     * <p/>
     * <strong>Beware that this behavior deviates from the URI Template specification.</strong>
     * <p/>
     * For instance:
     * <br/>Given the following template variable names and values:
     * <ul>
     * <li>bar = java</li>
     * <li>foo undefined
     * </ul>
     * <p/>The following expansion templates will be expanded as shown if {@code preserveUndefinedVar} is true:
     * <br/>{bar}
     * <br/>java
     * <br/>
     * <br/>{foo=a_default_value}
     * <br/>a_default_value
     * <br/>
     * <br/>{foo}
     * <br/>{foo}
     */
    public static String varSubstitution(String expansionPattern, Object[] expansionInfo, Map<String, String> nameValuePairs, boolean preserveUndefinedVar) throws URIException {
        Map vars = (Map) expansionInfo[2];
        // only one var per pattern
        Map.Entry e = (Map.Entry) vars.entrySet().iterator().next();
        String var = (String) e.getKey();
        String defaultValue = (String) e.getValue();
        boolean hasDefaultValue = defaultValue != null;
        // this boolean indicates if the var is mentionned in the map, not that the associated value is not null.
        boolean varDefined = nameValuePairs.containsKey(var);
        String providedValue = nameValuePairs.get(var);
        String res;
        boolean escapingNeeded = true;
        if (varDefined) {
            if (providedValue == null && !hasDefaultValue) {
                res = "";
            } else {
                res = providedValue != null ? providedValue : defaultValue;
            }
        } else {
            // If the variable is undefined and no default value is given then substitute with the empty string,
            // except if preserveUndefinedVar is true

            if (hasDefaultValue) {
                res = defaultValue;
            } else {
                if (preserveUndefinedVar) {
                    res = expansionPattern;
                    escapingNeeded = false;
                } else {
                    res = "";
                }
            }
        }
        // We assume that the replacement value is for the query part of the URI.
        // Actually the query allows less character than the path part. $%&+,:@
        // (acording to RFC2396
        return escapingNeeded ? URIUtil.encodeWithinQuery(res) : res;
    }


    private static Map<String, String> toMap(String... nameValuePairs) {
        if (nameValuePairs.length % 2 != 0) {
            throw new IllegalArgumentException("An even number of elements is expected.");
        }
        Map<String, String> m = new HashMap<String, String>();
        for (int i = 0; i < nameValuePairs.length; i = i + 2) {
            m.put(nameValuePairs[i], nameValuePairs[i + 1]);
        }
        return m;
    }
}
