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

import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.activemq.artemis.api.core.SimpleString;

/**
 * Utility class that provides methods for parsing URI's
 *
 * This class can be used to split composite URI's into their component parts and is used to extract any
 * URI options from each URI in order to set specific properties on Beans.
 *
 * (copied from activemq 5)
 */
public class URISupport {

   /**
    * A composite URI can be split into one or more CompositeData object which each represent the
    * individual URIs that comprise the composite one.
    */
   public static class CompositeData {

      private String host;
      private String scheme;
      private String path;
      private URI[] components;
      private Map<String, String> parameters;
      private String fragment;

      public URI[] getComponents() {
         return components;
      }

      public String getFragment() {
         return fragment;
      }

      public Map<String, String> getParameters() {
         return parameters;
      }

      public String getScheme() {
         return scheme;
      }

      public String getPath() {
         return path;
      }

      public String getHost() {
         return host;
      }

      public URI toURI() throws URISyntaxException {
         StringBuilder sb = new StringBuilder();
         if (scheme != null) {
            sb.append(scheme);
            sb.append(':');
         }

         if (host != null && host.length() != 0) {
            sb.append(host);
         } else {
            sb.append('(');
            for (int i = 0; i < components.length; i++) {
               if (i != 0) {
                  sb.append(',');
               }
               sb.append(components[i].toString());
            }
            sb.append(')');
         }

         if (path != null) {
            sb.append('/');
            sb.append(path);
         }
         appendParameters(sb, parameters);
         if (fragment != null) {
            sb.append('#');
            sb.append(fragment);
         }
         return new URI(sb.toString());
      }
   }

   public static StringBuilder appendParameters(StringBuilder sb, Map<String, String> parameters) throws URISyntaxException {
      if (!parameters.isEmpty()) {
         sb.append('?');
         sb.append(createQueryString(parameters));
      }
      return sb;
   }

   /**
    * Give a URI break off any URI options and store them in a Key / Value Mapping.
    *
    * @param uri The URI whose query should be extracted and processed.
    * @return A Mapping of the URI options.
    * @throws java.net.URISyntaxException
    */
   public static Map<String, String> parseQuery(String uri) throws URISyntaxException {
      try {
         uri = uri.substring(uri.lastIndexOf("?") + 1); // get only the relevant part of the query
         Map<String, String> rc = new HashMap<>();
         if (uri != null && !uri.isEmpty()) {
            parseParameters(rc, uri.split("[&;]"));
         }
         return rc;
      } catch (UnsupportedEncodingException e) {
         throw (URISyntaxException) new URISyntaxException(e.toString(), "Invalid encoding").initCause(e);
      }
   }

   public static boolean containsQuery(String uri) {
      return uri.contains("?");
   }

   public static boolean containsQuery(SimpleString uri) {
      return uri.contains('?');
   }

   private static void parseParameters(Map<String, String> rc,
                                       String[] parameters) throws UnsupportedEncodingException {
      for (String parameter : parameters) {
         int p = parameter.indexOf("=");
         if (p >= 0) {
            String name = URLDecoder.decode(parameter.substring(0, p), "UTF-8");
            String value = URLDecoder.decode(parameter.substring(p + 1), "UTF-8");
            rc.put(name, value);
         } else {
            rc.put(parameter, null);
         }
      }
   }

   /**
    * Given a URI parse and extract any URI query options and return them as a Key / Value mapping.
    *
    * This method differs from the {@link parseQuery} method in that it handles composite URI types and
    * will extract the URI options from the outermost composite URI.
    *
    * @param uri The URI whose query should be extracted and processed.
    * @return A Mapping of the URI options.
    * @throws java.net.URISyntaxException
    */
   public static Map<String, String> parseParameters(URI uri) throws URISyntaxException {
      if (!isCompositeURI(uri)) {
         return uri.getQuery() == null ? emptyMap() : parseQuery(stripPrefix(uri.getQuery(), "?"));
      } else {
         CompositeData data = URISupport.parseComposite(uri);
         Map<String, String> parameters = new HashMap<>();
         parameters.putAll(data.getParameters());
         if (parameters.isEmpty()) {
            parameters = emptyMap();
         }

         return parameters;
      }
   }

   /**
    * Given a Key / Value mapping create and append a URI query value that represents the mapped entries, return the
    * newly updated URI that contains the value of the given URI and the appended query value.
    *
    * @param uri             The source URI that will have the Map entries appended as a URI query value.
    * @param queryParameters The Key / Value mapping that will be transformed into a URI query string.
    * @return A new URI value that combines the given URI and the constructed query string.
    * @throws java.net.URISyntaxException
    */
   public static URI applyParameters(URI uri, Map<String, String> queryParameters) throws URISyntaxException {
      return applyParameters(uri, queryParameters, "");
   }

   /**
    * Given a Key / Value mapping create and append a URI query value that represents the mapped entries, return the
    * newly updated URI that contains the value of the given URI and the appended query value.  Each entry in the query
    * string is prefixed by the supplied optionPrefix string.
    *
    * @param uri             The source URI that will have the Map entries appended as a URI query value.
    * @param queryParameters The Key / Value mapping that will be transformed into a URI query string.
    * @param optionPrefix    A string value that when not null or empty is used to prefix each query option key.
    * @return A new URI value that combines the given URI and the constructed query string.
    * @throws java.net.URISyntaxException
    */
   public static URI applyParameters(URI uri,
                                     Map<String, String> queryParameters,
                                     String optionPrefix) throws URISyntaxException {
      if (queryParameters != null && !queryParameters.isEmpty()) {
         StringBuilder newQuery = uri.getRawQuery() != null ? new StringBuilder(uri.getRawQuery()) : new StringBuilder();
         for (Map.Entry<String, String> param : queryParameters.entrySet()) {
            if (param.getKey().startsWith(optionPrefix)) {
               if (newQuery.length() != 0) {
                  newQuery.append('&');
               }
               final String key = param.getKey().substring(optionPrefix.length());
               newQuery.append(key).append('=').append(param.getValue());
            }
         }
         uri = createURIWithQuery(uri, newQuery.toString());
      }
      return uri;
   }

   private static Map<String, String> emptyMap() {
      return Collections.EMPTY_MAP;
   }

   /**
    * Removes any URI query from the given uri and return a new URI that does not contain the query portion.
    *
    * @param uri The URI whose query value is to be removed.
    * @return a new URI that does not contain a query value.
    * @throws java.net.URISyntaxException
    */
   public static URI removeQuery(URI uri) throws URISyntaxException {
      return createURIWithQuery(uri, null);
   }

   /**
    * Creates a URI with the given query, removing an previous query value from the given URI.
    *
    * @param uri   The source URI whose existing query is replaced with the newly supplied one.
    * @param query The new URI query string that should be appended to the given URI.
    * @return a new URI that is a combination of the original URI and the given query string.
    * @throws java.net.URISyntaxException
    */
   public static URI createURIWithQuery(URI uri, String query) throws URISyntaxException {
      String schemeSpecificPart = uri.getRawSchemeSpecificPart();
      // strip existing query if any
      int questionMark = schemeSpecificPart.lastIndexOf("?");
      // make sure question mark is not within parentheses
      if (questionMark < schemeSpecificPart.lastIndexOf(")")) {
         questionMark = -1;
      }
      if (questionMark > 0) {
         schemeSpecificPart = schemeSpecificPart.substring(0, questionMark);
      }
      if (query != null && query.length() > 0) {
         schemeSpecificPart += "?" + query;
      }
      return new URI(uri.getScheme(), schemeSpecificPart, uri.getFragment());
   }

   /**
    * Given a composite URI, parse the individual URI elements contained within that URI and return
    * a CompositeData instance that contains the parsed URI values.
    *
    * @param uri The target URI that should be parsed.
    * @return a new CompositeData instance representing the parsed composite URI.
    * @throws java.net.URISyntaxException
    */
   public static CompositeData parseComposite(URI uri) throws URISyntaxException {

      CompositeData rc = new CompositeData();
      rc.scheme = uri.getScheme();
      String ssp = stripPrefix(uri.getRawSchemeSpecificPart().trim(), "//").trim();

      parseComposite(uri, rc, ssp);

      rc.fragment = uri.getFragment();
      return rc;
   }

   /**
    * Examine a URI and determine if it is a Composite type or not.
    *
    * @param uri The URI that is to be examined.
    * @return true if the given URI is a Composite type.
    */
   public static boolean isCompositeURI(URI uri) {
      String ssp = stripPrefix(uri.getRawSchemeSpecificPart().trim(), "//").trim();

      if (ssp.indexOf('(') == 0 && checkParenthesis(ssp)) {
         return true;
      }
      return false;
   }

   /**
    * Given a string and a position in that string of an open parend, find the matching close parend.
    *
    * @param str   The string to be searched for a matching parend.
    * @param first The index in the string of the opening parend whose close value is to be searched.
    * @return the index in the string where the closing parend is located.
    * @throws java.net.URISyntaxException fi the string does not contain a matching parend.
    */
   public static int indexOfParenthesisMatch(String str, int first) throws URISyntaxException {
      int index = -1;

      if (first < 0 || first > str.length()) {
         throw new IllegalArgumentException("Invalid position for first parenthesis: " + first);
      }

      if (str.charAt(first) != '(') {
         throw new IllegalArgumentException("character at indicated position is not a parenthesis");
      }

      int depth = 1;
      char[] array = str.toCharArray();
      for (index = first + 1; index < array.length; ++index) {
         char current = array[index];
         if (current == '(') {
            depth++;
         } else if (current == ')') {
            if (--depth == 0) {
               break;
            }
         }
      }

      if (depth != 0) {
         throw new URISyntaxException(str, "URI did not contain a matching parenthesis.");
      }

      return index;
   }

   /**
    * Given a composite URI and a CompositeData instance and the scheme specific part extracted from the source URI,
    * parse the composite URI and populate the CompositeData object with the results.  The source URI is used only
    * for logging as the ssp should have already been extracted from it and passed here.
    *
    * @param uri The original source URI whose ssp is parsed into the composite data.
    * @param rc  The CompositeData instance that will be populated from the given ssp.
    * @param ssp The scheme specific part from the original string that is a composite or one or more URIs.
    * @throws java.net.URISyntaxException
    */
   private static void parseComposite(URI uri, CompositeData rc, String ssp) throws URISyntaxException {
      String componentString;
      String params;

      if (!checkParenthesis(ssp)) {
         throw new URISyntaxException(uri.toString(), "Not a matching number of '(' and ')' parenthesis");
      }

      int p;
      int initialParen = ssp.indexOf("(");
      if (initialParen == 0) {

         rc.host = ssp.substring(0, initialParen);
         p = rc.host.indexOf("/");

         if (p >= 0) {
            rc.path = rc.host.substring(p);
            rc.host = rc.host.substring(0, p);
         }

         p = indexOfParenthesisMatch(ssp, initialParen);
         componentString = ssp.substring(initialParen + 1, p);
         params = ssp.substring(p + 1).trim();

      } else {
         componentString = ssp;
         params = "";
      }

      String[] components = splitComponents(componentString);
      rc.components = new URI[components.length];
      for (int i = 0; i < components.length; i++) {
         rc.components[i] = new URI(components[i].trim());
      }

      p = params.indexOf("?");
      if (p >= 0) {
         if (p > 0) {
            rc.path = stripPrefix(params.substring(0, p), "/");
         }
         rc.parameters = parseQuery(params.substring(p + 1));
      } else {
         if (params.length() > 0) {
            rc.path = stripPrefix(params, "/");
         }
         rc.parameters = emptyMap();
      }
   }

   /**
    * Given the inner portion of a composite URI, split and return each inner URI as a string
    * element in a new String array.
    *
    * @param str The inner URI elements of a composite URI string.
    * @return an array containing each inner URI from the composite one.
    */
   private static String[] splitComponents(String str) {
      List<String> l = new ArrayList<>();

      int last = 0;
      int depth = 0;
      char[] chars = str.toCharArray();
      for (int i = 0; i < chars.length; i++) {
         switch (chars[i]) {
            case '(':
               depth++;
               break;
            case ')':
               depth--;
               break;
            case ',':
               if (depth == 0) {
                  String s = str.substring(last, i);
                  l.add(s);
                  last = i + 1;
               }
               break;
            default:
         }
      }

      String s = str.substring(last);
      if (s.length() != 0) {
         l.add(s);
      }

      String[] rc = new String[l.size()];
      l.toArray(rc);
      return rc;
   }

   /**
    * String the given prefix from the target string and return the result.
    *
    * @param value  The string that should be trimmed of the given prefix if present.
    * @param prefix The prefix to remove from the target string.
    * @return either the original string or a new string minus the supplied prefix if present.
    */
   public static String stripPrefix(String value, String prefix) {
      if (value.startsWith(prefix)) {
         return value.substring(prefix.length());
      }
      return value;
   }

   /**
    * Strip a URI of its scheme element.
    *
    * @param uri The URI whose scheme value should be stripped.
    * @return The stripped URI value.
    * @throws java.net.URISyntaxException
    */
   public static URI stripScheme(URI uri) throws URISyntaxException {
      return new URI(stripPrefix(uri.getSchemeSpecificPart().trim(), "//"));
   }

   /**
    * Given a key / value mapping, create and return a URI formatted query string that is valid and
    * can be appended to a URI. Query parameters in the string are sorted by key.
    *
    * @param options The Mapping that will create the new Query string.
    * @return a URI formatted query string.
    * @throws java.net.URISyntaxException
    */
   public static String createQueryString(Map<String, ? extends Object> options) throws URISyntaxException {
      try {
         if (options.size() > 0) {
            StringBuilder rc = new StringBuilder();
            boolean first = true;
            List<String> keys = new ArrayList<>();
            keys.addAll(options.keySet());
            Collections.sort(keys);
            for (String key : keys) {
               if (first) {
                  first = false;
               } else {
                  rc.append("&");
               }
               String value = (String) options.get(key);
               rc.append(URLEncoder.encode(key, "UTF-8"));
               rc.append("=");
               rc.append(URLEncoder.encode(value, "UTF-8"));
            }
            return rc.toString();
         } else {
            return "";
         }
      } catch (UnsupportedEncodingException e) {
         throw (URISyntaxException) new URISyntaxException(e.toString(), "Invalid encoding").initCause(e);
      }
   }

   /**
    * Creates a URI from the original URI and the remaining parameters.
    *
    * When the query options of a URI are applied to certain objects the used portion of the query options needs
    * to be removed and replaced with those that remain so that other parts of the code can attempt to apply the
    * remainder or give an error is unknown values were given.  This method is used to update a URI with those
    * remainder values.
    *
    * @param originalURI The URI whose current parameters are remove and replaced with the given remainder value.
    * @param params      The URI params that should be used to replace the current ones in the target.
    * @return a new URI that matches the original one but has its query options replaced with the given ones.
    * @throws java.net.URISyntaxException
    */
   public static URI createRemainingURI(URI originalURI, Map<String, String> params) throws URISyntaxException {
      String s = createQueryString(params);
      if (s.length() == 0) {
         s = null;
      }
      return createURIWithQuery(originalURI, s);
   }

   /**
    * Given a URI value create and return a new URI that matches the target one but with the scheme value
    * supplied to this method.
    *
    * @param bindAddr The URI whose scheme value should be altered.
    * @param scheme   The new scheme value to use for the returned URI.
    * @return a new URI that is a copy of the original except that its scheme matches the supplied one.
    * @throws java.net.URISyntaxException
    */
   public static URI changeScheme(URI bindAddr, String scheme) throws URISyntaxException {
      return new URI(scheme, bindAddr.getUserInfo(), bindAddr.getHost(), bindAddr.getPort(), bindAddr.getPath(), bindAddr.getQuery(), bindAddr.getFragment());
   }

   /**
    * Examine the supplied string and ensure that all parens appear as matching pairs.
    *
    * @param str The target string to examine.
    * @return true if the target string has valid paren pairings.
    */
   public static boolean checkParenthesis(String str) {
      boolean result = true;
      if (str != null) {
         int open = 0;
         int closed = 0;

         int i = 0;
         while ((i = str.indexOf('(', i)) >= 0) {
            i++;
            open++;
         }
         i = 0;
         while ((i = str.indexOf(')', i)) >= 0) {
            i++;
            closed++;
         }
         result = open == closed;
      }
      return result;
   }
}
