package freenet.support;

import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;


/**
 * This class provides encoding of byte arrays into Base64-encoded strings,
 * and decoding the other way.
 *
 * <P>NOTE!  This is modified Base64 with slightly different characters than
 * usual, so it won't require escaping when used in URLs.
 *
 * <P>NOTE!  This class only does the padding that's normal in Base64
 * if the 'true' flag is given to the encode() method.  This is because
 * Base64 requires that the length of the encoded text be a multiple
 * of four characters, padded with '='.  Without the 'true' flag, we don't
 * add these '=' characters.
 *
 * @author Stephen Blackheath
 */
public class Base64
{
  static final Charset UTF8 = StandardCharsets.UTF_8;

  private static char[] base64Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789~-".toCharArray();

  private static char[] base64StandardAlphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".toCharArray();

  /**
   * A reverse lookup table to convert base64 letters back into the
   * a 6-bit sequence.
   */
  private static byte[] base64Reverse;
  private static byte[] base64StandardReverse;

   // Populate the base64Reverse lookup table from the base64Alphabet table.
  static {
    base64Reverse = new byte[128];
    base64StandardReverse = new byte[base64Reverse.length];

      // Set all entries to 0xFF, which means that that particular letter
      // is not a legal base64 letter.
    for (int i = 0; i < base64Reverse.length; i++) {
      base64Reverse[i] = (byte) 0xFF;
      base64StandardReverse[i] = (byte) 0xFF;
    }
    for (int i = 0; i < base64Alphabet.length; i++) {
      base64Reverse[base64Alphabet[i]] = (byte) i;
      base64StandardReverse[base64StandardAlphabet[i]] = (byte) i;
    }
  }

  /**
   * Encode to our shortened (non-standards-compliant) format.
   */
  public static String encode(byte[] in)
  {
    return encode(in, false);
  }

  /* FIXME: Figure out where this function is used and maybe remove it if its not
   * used. Its old javadoc which has been here for a while fools the user into believing
   * that the format is standard compliant */

  /**
   * Caller should specify equalsPad=true if they want a standards compliant padding,
   * but not standard compliant encoding.
   */
  public static String encode(byte[] in, boolean equalsPad) {
    return encode(in, equalsPad, base64Alphabet);
  }

  /**
   * Convenience method to encode a string, in our shortened format.
   *
   * Please use this to encode a string, rather than trying to encode the string
   * yourself using the 0-arg String.getBytes() which is not deterministic.
   */
  public static String encodeUTF8(String in) {
    return encodeUTF8(in, false);
  }

  /**
   * Convenience method to encode a string.
   *
   * Please use this to encode a string, rather than trying to encode the string
   * yourself using the 0-arg String.getBytes() which is not deterministic.
   */
  public static String encodeUTF8(String in, boolean equalsPad) {
    return encode(in.getBytes(UTF8), equalsPad, base64Alphabet);
  }
  
  /**
   * Convenience method to encode a string.
   *
   * Please use this to encode a string, rather than trying to encode the string
   * yourself using the 0-arg String.getBytes() which is not deterministic.
   */
  public static String encodeStandardUTF8(String in) {
	  return encodeStandard(in.getBytes(UTF8));
  }

  /**
   * Standard compliant encoding.
   */
  public static String encodeStandard(byte[] in) {
    return encode(in, true, base64StandardAlphabet);
  }

  /**
   * Caller should specify equalsPad=true if they want a standards compliant encoding.
   */
  private static String encode(byte[] in, boolean equalsPad, char[] alphabet)
  {
    char[] out = new char[((in.length+2)/3)*4];
    int rem = in.length%3;
    int o = 0;
    for (int i = 0; i < in.length;) {
      int val = (in[i++] & 0xFF) << 16;
      if (i < in.length)
        val |= (in[i++] & 0xFF) << 8;
      if (i < in.length)
        val |= (in[i++] & 0xFF);
      out[o++] = alphabet[(val>>18) & 0x3F];
      out[o++] = alphabet[(val>>12) & 0x3F];
      out[o++] = alphabet[(val>>6) & 0x3F];
      out[o++] = alphabet[val & 0x3F];
    }
    int outLen = out.length;
    switch (rem) {
      case 1: outLen -= 2; break;
      case 2: outLen -= 1; break;
    }
      // Pad with '=' signs up to a multiple of four if requested.
    if (equalsPad)
      while (outLen < out.length)
        out[outLen++] = '=';
    return new String(out, 0, outLen);
  }

  /**
   * Handles the standards-compliant padding (padded with '=' signs) as well as our
   * shortened form.
   * @throws IllegalBase64Exception
   */
  public static byte[] decode(String inStr) throws IllegalBase64Exception {
    return decode(inStr, base64Reverse);
  }

  /**
   * Convenience method to decode into a string, in our shortened format.
   *
   * Please use this to decode into a string, rather than trying to decode the
   * string yourself using new String(bytes[]) which is not deterministic.
   */
  public static String decodeUTF8(String inStr) throws IllegalBase64Exception {
    return new String(decode(inStr), UTF8);
  }

  /**
   * Handles the standards-compliant base64 encoding.
   */
  public static byte[] decodeStandard(String inStr) throws IllegalBase64Exception {
    return decode(inStr, base64StandardReverse);
  }

  /**
   * Handles the standards-compliant (padded with '=' signs) as well as our
   * shortened form.
   */
  private static byte[] decode(String inStr, byte[] reverseAlphabet)
    throws IllegalBase64Exception
  {
    try {
      char[] in = inStr.toCharArray();
      int inLength = in.length;

        // Strip trailing equals signs.
      while ((inLength > 0) && (in[inLength-1] == '='))
        inLength--;

      int blocks = inLength/4;
      int remainder = inLength & 3;
        // wholeInLen and wholeOutLen are the the length of the input and output
        // sequences respectively, not including any partial block at the end.
      int wholeInLen  = blocks*4;
      int wholeOutLen = blocks*3;
      int outLen = wholeOutLen;
      switch (remainder) {
        case 1: throw new IllegalBase64Exception("illegal Base64 length");
        case 2:  outLen = wholeOutLen+1; break;
        case 3:  outLen = wholeOutLen+2; break;
        default: outLen = wholeOutLen;
      }
      byte[] out = new byte[outLen];
      int o = 0;
      int i;
      for (i = 0; i < wholeInLen;) {
        int in1 = reverseAlphabet[in[i]];
        int in2 = reverseAlphabet[in[i+1]];
        int in3 = reverseAlphabet[in[i+2]];
        int in4 = reverseAlphabet[in[i+3]];
        int orValue = in1|in2|in3|in4;
        if ((orValue & 0x80) != 0)
          throw new IllegalBase64Exception("illegal Base64 character");
        int outVal = (in1 << 18) | (in2 << 12) | (in3 << 6) | in4;
        out[o] = (byte) (outVal>>16);
        out[o+1] = (byte) (outVal>>8);
        out[o+2] = (byte) outVal;
        i += 4;
        o += 3;
      }
      int orValue;
      switch (remainder) {
        case 2:
          {
            int in1 = reverseAlphabet[in[i]];
            int in2 = reverseAlphabet[in[i+1]];
            orValue = in1|in2;
            int outVal = (in1 << 18) | (in2 << 12);
            out[o] = (byte) (outVal>>16);
          }
          break;
        case 3:
          {
            int in1 = reverseAlphabet[in[i]];
            int in2 = reverseAlphabet[in[i+1]];
            int in3 = reverseAlphabet[in[i+2]];
            orValue = in1|in2|in3;
            int outVal = (in1 << 18) | (in2 << 12) | (in3 << 6);
            out[o] = (byte) (outVal>>16);
            out[o+1] = (byte) (outVal>>8);
          }
          break;
        default:
            // Keep compiler happy
          orValue = 0;
      }
      if ((orValue & 0x80) != 0)
        throw new IllegalBase64Exception("illegal Base64 character");
      return out;
    }
      // Illegal characters can cause an ArrayIndexOutOfBoundsException when
      // looking up reverseAlphabet.
    catch (ArrayIndexOutOfBoundsException e) {
      throw new IllegalBase64Exception("illegal Base64 character");
    }
  }
}
