/*
 * Copyright (c) 2012-2018 Red Hat, Inc.
 * This program and the accompanying materials are made
 * available under the terms of the Eclipse Public License 2.0
 * which is available at https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *   Red Hat, Inc. - initial API and implementation
 */
package org.eclipse.che.commons.lang;

import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.*;

/** A collection of utilities for encoding URLs. */
public class URLEncodedUtils {

  private static final String PARAMETER_SEPARATOR = "&";
  private static final String NAME_VALUE_SEPARATOR = "=";

  /**
   * Returns Map<String, Set<String>> as built from the URI's query portion. For example, a URI of
   * http://example.org/path/to/file?a=1&b=2&c=3&c=4 would return a Map three key is a name name of
   * parameter Set<String> is a values, a={1}, one for b={2} and two for c={3,4}.
   *
   * <p>
   *
   * <p>This is typically useful while parsing an HTTP PUT.
   *
   * @param uri uri to parse
   * @param encoding encoding to use while parsing the query.
   */
  public static Map<String, Set<String>> parse(final URI uri, final String encoding) {
    Map<String, Set<String>> result = Collections.emptyMap();
    final String query = uri.getRawQuery();
    if (query != null && query.length() > 0) {
      result = new HashMap<>();
      parse(result, new Scanner(query), encoding, true);
    }
    return result;
  }

  /**
   * Returns Map<String, Set<String>> as built from the URI's query portion. Parameters encoding
   * does not performed. For example, a URI of http://example.org/path/to/file?a=1&b=2&c=3&c=4 would
   * return a Map three key is a name name of parameter Set<String> is a values, a={1}, one for
   * b={2} and two for c={3,4}.
   *
   * <p>
   *
   * <p>This is typically useful while parsing an HTTP PUT.
   *
   * @param uri uri to parse
   */
  public static Map<String, Set<String>> parse(final URI uri) {
    return parse(uri, true);
  }

  /**
   * Returns Map<String, Set<String>> as built from the URI's query portion. Parameters encoding
   * does not performed. For example, a URI of http://example.org/path/to/file?a=1&b=2&c=3&c=4 would
   * return a Map three key is a name name of parameter Set<String> is a values, a={1}, one for
   * b={2} and two for c={3,4}.
   *
   * <p>
   *
   * <p>This is typically useful while parsing an HTTP PUT.
   *
   * @param uri uri to parse
   * @param decodeQueryParam decode query parameter or not.
   */
  public static Map<String, Set<String>> parse(final URI uri, boolean decodeQueryParam) {
    Map<String, Set<String>> result = Collections.emptyMap();
    final String query = uri.getRawQuery();
    if (query != null && query.length() > 0) {
      result = new HashMap<>();
      parse(result, new Scanner(query), null, decodeQueryParam);
    }
    return result;
  }

  /**
   * Adds all parameters within the Scanner to the list of <code>parameters</code>, as encoded by
   * <code>encoding</code>. If <code>encoding</code> is null encoding does not performed. For
   * example, a scanner containing the string <code>a=1&b=2&c=3</code> would add the Map<String,
   * Set<String>> a=1, b=2, and c=3 to the list of parameters.
   *
   * @param parameters List to add parameters to.
   * @param scanner Input that contains the parameters to parse.
   * @param encoding Encoding to use when decoding the parameters. If encoding is null encoding does
   *     not performed.
   */
  private static void parse(
      final Map<String, Set<String>> parameters,
      final Scanner scanner,
      final String encoding,
      boolean decodeQueryParam) {
    scanner.useDelimiter(PARAMETER_SEPARATOR);
    while (scanner.hasNext()) {
      final String[] nameValue = scanner.next().split(NAME_VALUE_SEPARATOR);
      if (nameValue.length == 0 || nameValue.length > 2)
        throw new IllegalArgumentException("bad parameter");

      final String name = decodeQueryParam ? decode(nameValue[0], encoding) : nameValue[0];
      String value = null;
      if (nameValue.length == 2)
        value = decodeQueryParam ? decode(nameValue[1], encoding) : nameValue[1];

      Set<String> values = parameters.get(name);
      if (values == null) {
        values = new LinkedHashSet<>();
        parameters.put(name, values);
      }
      if (value != null) {
        values.add(value);
      }
    }
  }

  /**
   * Returns a String that is suitable for use as an <code>application/x-www-form-urlencoded</code>
   * list of parameters in an HTTP PUT or HTTP POST.
   *
   * @param parameters The parameters to include.
   * @param encoding The encoding to use.
   */
  public static String format(final Map<String, Set<String>> parameters, final String encoding) {
    final StringBuilder result = new StringBuilder();

    for (Map.Entry<String, Set<String>> parameter : parameters.entrySet()) {
      final String encodedName = encode(parameter.getKey(), encoding);
      final Set<String> values = parameter.getValue();
      if (values == null || values.size() == 0) {
        result.append(encodedName);
        result.append(NAME_VALUE_SEPARATOR);
        result.append("");
      } else {
        for (String value : values) {
          final String encodedValue = value != null ? encode(value, encoding) : "";
          if (result.length() > 0) result.append(PARAMETER_SEPARATOR);
          result.append(encodedName);
          result.append(NAME_VALUE_SEPARATOR);
          result.append(encodedValue);
        }
      }
    }
    return result.toString();
  }

  private static String decode(final String content, final String encoding) {
    try {
      return URLDecoder.decode(content, encoding != null ? encoding : "UTF-8");
    } catch (UnsupportedEncodingException problem) {
      throw new IllegalArgumentException(problem);
    }
  }

  private static String encode(final String content, final String encoding) {
    try {
      return URLEncoder.encode(content, encoding != null ? encoding : "UTF-8");
    } catch (UnsupportedEncodingException problem) {
      throw new IllegalArgumentException(problem);
    }
  }
}
