/*
 * Copyright (C) 2010 The Android Open Source Project
 *
 * Licensed 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 libcore.icu;

import java.util.regex.Pattern;

/**
 * Java version of Android's NativeIDN class, rewritten for J2ObjC because the
 * Android version uses native code that depends on ICU's uidna functions, which
 * aren't public on iOS.
 * <p>
 * This code is converted from the pseudo-code from the RFC 3492 specification
 * (https://www.ietf.org/rfc/rfc3492.txt).
 */
public final class NativeIDN {

  // Bootstring parameters for Punycode.
  private static final int BASE = 36;
  private static final int TMIN = 1;
  private static final int TMAX = 26;
  private static final int SKEW = 38;
  private static final int DAMP = 700;
  private static final int INITIAL_BIAS = 72;
  private static final int INITIAL_N = 128;
  private static final char DELIMITER = '-';

  // RFC 3490 separator characters.
  private static final Pattern SEPARATORS_REGEX = Pattern.compile("[.\u3002\uFF0E\uFF61]");

  private static final String PUNYCODE_PREFIX = "xn--";

  /**
   * Convert a Unicode string to IDN.
   */
  public static String toASCII(String s, int flags) {
    String[] parts = SEPARATORS_REGEX.split(s);
    for (int i = 0; i < parts.length; i++) {
      if (nonASCII(parts[i])) {
        parts[i] = PUNYCODE_PREFIX + encode(parts[i]);
      }
    }
    return String.join(".", parts);
  }

  /**
   * Returns true if a string contains any non-ASCII characters.
   */
  private static boolean nonASCII(String s) {
    for (int i = 0; i < s.length(); i++) {
      if (s.charAt(i) > 0x7F) {
          return true;
      }
    }
    return false;
  }

  /**
   * Encodes a Unicode string to Punycode ASCII.
   */
  private static String encode(String s) {
    int n = INITIAL_N;
    int delta = 0;
    int bias = INITIAL_BIAS;
    StringBuffer output = new StringBuffer();

    // Copy all basic code points to the output.
    int b = 0;
    for (int i = 0; i < s.length(); i++) {
      char c = s.charAt(i);
      if (basicCodePoint(c)) {
        output.append(c);
        b++;
      }
    }
    if (b > 0) {
      output.append(DELIMITER);
    }
    int h = b;
    while (h < s.length()) {
      int m = Integer.MAX_VALUE;
      for (int i = 0; i < s.length(); i++) {
        int c = s.charAt(i);
        if (c >= n && c < m) {
          m = c;
        }
      }
      if (m - n > (Integer.MAX_VALUE - delta) / (h + 1)) {
        // This should probably be a java.text.ParseException, but
        // IllegalArgumentException is what Android's IDN expects.
        throw new IllegalArgumentException("encoding overflow");
      }
      delta = delta + (m - n) * (h + 1);
      n = m;
      for (int j = 0; j < s.length(); j++) {
        int c = s.charAt(j);
        if (c < n) {
          delta++;
          if (0 == delta) {
            throw new IllegalArgumentException("encoding overflow");
          }
        }
        if (c == n) {
          int q = delta;
          for (int k = BASE;; k += BASE) {
            int t;
            if (k <= bias) {
              t = TMIN;
            } else if (k >= bias + TMAX) {
              t = TMAX;
            } else {
              t = k - bias;
            }
            if (q < t) {
              break;
            }
            output.append((char) encodeDigit(t + (q - t) % (BASE - t)));
            q = (q - t) / (BASE - t);
          }

          output.append((char) encodeDigit(q));
          bias = adapt(delta, h + 1, h == b);
          delta = 0;
          h++;
        }
      }
      delta++;
      n++;
    }
    return output.toString();
  }

  /**
   * Convert an IDN-formatted ASCII string to Unicode.
   */
  public static String toUnicode(String s, int flags) {
    String[] parts = SEPARATORS_REGEX.split(s);
    for (int i = 0; i < parts.length; i++) {
      if (parts[i].startsWith(PUNYCODE_PREFIX)) {
        parts[i] = decode(parts[i].substring(PUNYCODE_PREFIX.length()));
      }
    }
    return String.join(".", parts);

  }

  /**
   * Convert a Punycode/ASCII string to Unicode.
   */
  private static String decode(String s) {
    int n = INITIAL_N;
    int i = 0;
    int bias = INITIAL_BIAS;
    StringBuffer output = new StringBuffer();
    int d = s.lastIndexOf(DELIMITER);
    if (d > 0) {
      for (int j = 0; j < d; j++) {
        char c = s.charAt(j);
        if (!basicCodePoint(c)) {
          throw new IllegalArgumentException("bad input: " + c);
        }
        output.append(c);
      }
      d++;
    } else {
      d = 0;
    }
    while (d < s.length()) {
      int oldi = i;
      int w = 1;
      for (int k = BASE;; k += BASE) {
        if (d == s.length()) {
          throw new IllegalArgumentException("bad input: " + d);
        }
        int c = s.charAt(d++);
        int digit = decodeDigit(c);
        if (digit > (Integer.MAX_VALUE - i) / w) {
          throw new IllegalArgumentException("encoding overflow");
        }
        i = i + digit * w;
        int t;
        if (k <= bias) {
          t = TMIN;
        } else if (k >= bias + TMAX) {
          t = TMAX;
        } else {
          t = k - bias;
        }
        if (digit < t) {
          break;
        }
        w = w * (BASE - t);
      }
      bias = adapt(i - oldi, output.length() + 1, oldi == 0);
      if (i / (output.length() + 1) > Integer.MAX_VALUE - n) {
        throw new IllegalArgumentException("encoding overflow");
      }
      n = n + i / (output.length() + 1);
      i = i % (output.length() + 1);
      output.insert(i, (char) n);
      i++;
    }

    return output.toString();
  }

  private static int adapt(int delta, int numpoints, boolean firsttime) {
    if (firsttime) {
      delta = delta / DAMP;
    } else {
      delta = delta / 2;
    }
    delta = delta + (delta / numpoints);
    int k = 0;
    while (delta > ((BASE - TMIN) * TMAX) / 2) {
      delta = delta / (BASE - TMIN);
      k = k + BASE;
    }
    return k + (((BASE - TMIN + 1) * delta) / (delta + SKEW));
  }

  private static int decodeDigit(int c) {
    if (c >= 'A' && c <= 'Z') {
      return c - 'A';
    }
    if (c >= 'a' && c <= 'z') {
      return (c - 'a');
    }
    if (c >= '0' && c <= '9') {
      return c - '0' + 26;
    }
    throw new IllegalArgumentException("bad input: " + c);
  }

  private static char encodeDigit(int d) {
    if (d >= 0 && d <= 25) {
      return (char) (d + 'a');
    }
    if (d >= 26 && d <= 35) {
      return (char) (d - 26 + '0');
    }
    throw new IllegalArgumentException("bad input: " + d);
  }

  private static boolean basicCodePoint(int cp) {
    return cp < 0x80;
  }

  private NativeIDN() {
  }
}
