/*
 * Copyright (c) 2011-2019 Contributors to the Eclipse Foundation
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
 * which is available at https://www.apache.org/licenses/LICENSE-2.0.
 *
 * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
 */

package io.vertx.core.net.impl;

import java.nio.charset.StandardCharsets;

/**
 * @author <a href="mailto:plopes@redhat.com">Paulo Lopes</a>
 */
public final class URIDecoder {

  private URIDecoder() {
    throw new RuntimeException("Static Class");
  }

  /**
   * Decodes a segment of an URI encoded by a browser.
   *
   * The string is expected to be encoded as per RFC 3986, Section 2. This is the encoding used by JavaScript functions
   * encodeURI and encodeURIComponent, but not escape. For example in this encoding, é (in Unicode U+00E9 or in
   * UTF-8 0xC3 0xA9) is encoded as %C3%A9 or %c3%a9.
   *
   * Plus signs '+' will be handled as spaces and encoded using the default JDK URLEncoder class.
   *
   * @param s string to decode
   *
   * @return decoded string
   */
  public static String decodeURIComponent(String s) {
    return decodeURIComponent(s, true);
  }

  /**
   * Decodes a segment of an URI encoded by a browser.
   *
   * The string is expected to be encoded as per RFC 3986, Section 2. This is the encoding used by JavaScript functions
   * encodeURI and encodeURIComponent, but not escape. For example in this encoding, é (in Unicode U+00E9 or in
   * UTF-8 0xC3 0xA9) is encoded as %C3%A9 or %c3%a9.
   *
   * @param s string to decode
   * @param plus weather or not to transform plus signs into spaces
   *
   * @return decoded string
   */
  public static String decodeURIComponent(String s, boolean plus) {
    if (s == null) {
      return null;
    }

    final int size = s.length();
    boolean modified = false;
    int i;
    for (i = 0; i < size; i++) {
      final char c = s.charAt(i);
      if (c == '%' || (plus && c == '+')) {
        modified = true;
        break;
      }
    }
    if (!modified) {
      return s;
    }
    final byte[] buf = s.getBytes(StandardCharsets.UTF_8);
    int pos = i;  // position in `buf'.
    for (; i < size; i++) {
      char c = s.charAt(i);
      if (c == '%') {
        if (i == size - 1) {
          throw new IllegalArgumentException("unterminated escape"
            + " sequence at end of string: " + s);
        }
        c = s.charAt(++i);
        if (c == '%') {
          buf[pos++] = '%';  // "%%" -> "%"
          break;
        }
        if (i >= size - 1) {
          throw new IllegalArgumentException("partial escape"
            + " sequence at end of string: " + s);
        }
        c = decodeHexNibble(c);
        final char c2 = decodeHexNibble(s.charAt(++i));
        if (c == Character.MAX_VALUE || c2 == Character.MAX_VALUE) {
          throw new IllegalArgumentException(
            "invalid escape sequence `%" + s.charAt(i - 1)
              + s.charAt(i) + "' at index " + (i - 2)
              + " of: " + s);
        }
        c = (char) (c * 16 + c2);
        // shouldn't check for plus since it would be a double decoding
        buf[pos++] = (byte) c;
      } else {
        buf[pos++] = (byte) (plus && c == '+' ? ' ' : c);
      }
    }
    return new String(buf, 0, pos, StandardCharsets.UTF_8);
  }

  /**
   * Helper to decode half of a hexadecimal number from a string.
   * @param c The ASCII character of the hexadecimal number to decode.
   * Must be in the range {@code [0-9a-fA-F]}.
   * @return The hexadecimal value represented in the ASCII character
   * given, or {@link Character#MAX_VALUE} if the character is invalid.
   */
  private static char decodeHexNibble(final char c) {
    if ('0' <= c && c <= '9') {
      return (char) (c - '0');
    } else if ('a' <= c && c <= 'f') {
      return (char) (c - 'a' + 10);
    } else if ('A' <= c && c <= 'F') {
      return (char) (c - 'A' + 10);
    } else {
      return Character.MAX_VALUE;
    }
  }
}
