/*
 * Copyright (c) 2007, intarsys consulting GmbH
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * - Redistributions of source code must retain the above copyright notice,
 *   this list of conditions and the following disclaimer.
 *
 * - Redistributions in binary form must reproduce the above copyright notice,
 *   this list of conditions and the following disclaimer in the documentation
 *   and/or other materials provided with the distribution.
 *
 * - Neither the name of intarsys nor the names of its contributors may be used
 *   to endorse or promote products derived from this software without specific
 *   prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */
package de.intarsys.tools.encoding;

/**
 * An encoder for the BASE 64 code.
 * 
 */
public class Base64 {
	final private static byte PAD = (byte) '=';

	final private static char[] base64Chars = { 'A', 'B', 'C', 'D', 'E', 'F',
			'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S',
			'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',
			'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's',
			't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5',
			'6', '7', '8', '9', '+', '/' };

	final private static int[] base64Positions = { -1, -1, -1, -1, -1, -1, -1,
			-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
			-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
			-1, -1, 62, -1, -1, -1, 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61,
			-1, -1, -1, -1, -1, -1, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11,
			12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1,
			-1, -1, -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39,
			40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1,
			-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
			-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
			-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
			-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 };

	/**
	 * Decodes Base64 data into octets
	 * 
	 * @param inputBytes
	 *            Byte array containing Base64 data
	 * 
	 * @return byte array containing decoded data.
	 */
	public static byte[] decode(byte[] inputBytes) {
		// this is probably not the most performant implementation,
		// but I find it slightly more readable than others I've seen
		// [MIT] .... sorry, but refactored version is x times faster (but ugly)
		if (inputBytes == null) {
			return null;
		}
		int newSize = 0;
		int len = inputBytes.length;
		for (int i = 0; i < len; i++) {
			byte octect = inputBytes[i];
			if (((octect == 0x20) || (octect == 0xd) || (octect == 0xa) || (octect == 0x9))) {
				continue;
			}
			if ((base64Positions[inputBytes[i]] != -1)) {
				if (newSize != i) {
					inputBytes[newSize] = inputBytes[i];
				}
				newSize++;
				continue;
			}
			if ((inputBytes[i] == PAD)) {
				// ignore error in case there is data after PAD
				break;
			}

			// data error
			return null;
		}
		if (newSize == 0) {
			return new byte[0];
		}
		if ((newSize % 4) == 1) {
			// invalid
			return null;
		}
		byte[] result = new byte[((newSize - 1) / 4 * 3) + ((newSize - 1) % 4)];
		int threeI = 0;
		int fourI = 0;
		for (int i = 0; threeI < result.length; i++, threeI += 3, fourI += 4) {
			int bits;
			boolean padded3;
			boolean padded4;

			padded3 = padded4 = false;
			bits = (base64Positions[inputBytes[fourI]] << 18)
					+ (base64Positions[inputBytes[fourI + 1]] << 12);
			if (newSize > (fourI + 2)) {
				bits += (base64Positions[inputBytes[fourI + 2]] << 6);
				if (newSize > (fourI + 3)) {
					bits += base64Positions[inputBytes[fourI + 3]];
				} else {
					padded4 = true;
				}
			} else {
				// 3rd and 4th result bytes are padded
				padded3 = padded4 = true;
			}
			result[threeI] = (byte) (bits >> 16);
			if (!padded3) {
				result[threeI + 1] = (byte) ((bits >> 8) & 0xff);
			}
			if (!padded4) {
				result[threeI + 2] = (byte) (bits & 0xff);
			}
		}
		return result;
	}

	/**
	 * Encodes hex octets into Base64
	 * 
	 * @param inputBytes
	 *            Array containing binaryData
	 * 
	 * @return Encoded Base64 array
	 */
	public static byte[] encode(byte[] inputBytes) {
		// this is probably not the most performant implementation,
		// but I find it slightly more readable than others I've seen
		byte[] result;
		int inputLength;

		if (inputBytes == null) {
			return null;
		}

		inputLength = inputBytes.length;
		result = new byte[(inputLength + 2) / 3 * 4];
		int i = 0;
		int iTimesFour = 0;
		int iTimesThree = 0;
		while (iTimesFour < result.length) {
			int bits;
			boolean pad3;
			boolean pad4;

			pad3 = pad4 = false;
			bits = (inputBytes[iTimesThree] << 16) & 0xff0000;
			if (inputLength > ((iTimesThree) + 1)) {
				bits |= ((inputBytes[(iTimesThree) + 1] << 8) & 0xff00);
				if (inputLength > ((iTimesThree) + 2)) {
					bits |= (inputBytes[(iTimesThree) + 2] & 0xff);
				} else {
					pad4 = true;
				}
			} else {
				// have to pad 3rd and 4th result byte
				pad3 = pad4 = true;
			}
			result[iTimesFour] = (byte) base64Chars[(bits >> 18) & 0x3f];
			result[(iTimesFour) + 1] = (byte) base64Chars[(bits >> 12) & 0x3f];
			result[(iTimesFour) + 2] = (byte) (pad3 ? PAD
					: base64Chars[(bits >> 6) & 0x3f]);
			result[(iTimesFour) + 3] = (byte) (pad4 ? PAD
					: base64Chars[(bits) & 0x3f]);
			i++;
			iTimesFour += 4;
			iTimesThree += 3;
		}
		return result;
	}

	/**
	 * returns length of decoded data given an array containing encoded data.
	 * WhiteSpace removing is done if data array not valid.
	 * 
	 * @param base64Data
	 * 
	 * @return a -1 would be return if not
	 */
	public static int getDecodedDataLength(byte[] base64Data) {
		if (base64Data == null) {
			return -1;
		}

		if (base64Data.length == 0) {
			return 0;
		}

		// byte[] normalizedBase64Data = removeWhiteSpace( base64Data );//Remove
		// any whiteSpace
		byte[] decodedData = null;

		if ((decodedData = decode(base64Data)) == null) { // decode could
			// return a null
			// byte array
			return -1;
		}

		return decodedData.length;
	}

	public static boolean isArrayByteBase64(byte[] arrayOctect) {
		return (getDecodedDataLength(arrayOctect) >= 0);
	}

	public static boolean isBase64(byte octect) {
		return (((octect == 0x20) || (octect == 0xd) || (octect == 0xa) || (octect == 0x9))
				|| (octect == PAD) || (base64Positions[octect] != -1));
	}

	public static boolean isBase64(String isValidString) {
		if (isValidString == null) {
			return false;
		}
		return (isArrayByteBase64(isValidString.getBytes()));
	}

	/**
	 * Remove whitespace from MIME containing encoded Base64 data. e.g. "
	 * sdffferererrereresfsdfsdfsdff\n\r iiiiiiiiierejrlkwjerklwjerwerwr==\n\r"
	 * 
	 * @param data
	 * 
	 * @return The input data without whitespace.
	 */
	public static byte[] removeWhiteSpace(byte[] data) {
		if (data == null) {
			return null;
		}

		int newSize = 0;
		int len = data.length;
		int i = 0;
		for (; i < len; i++) {
			byte octect = data[i];
			if (!((octect == 0x20) || (octect == 0xd) || (octect == 0xa) || (octect == 0x9))) {
				newSize++;
			}
		}

		if (newSize == len) {
			return data; // return input array since no whiteSpace
		}

		byte[] arrayWithoutSpaces = new byte[newSize]; // Allocate new array
		// without whiteSpace

		int j = 0;
		for (i = 0; i < len; i++) {
			byte octect = data[i];
			if (((octect == 0x20) || (octect == 0xd) || (octect == 0xa) || (octect == 0x9))) {
				continue;
			} else {
				arrayWithoutSpaces[j++] = data[i]; // copy non-WhiteSpace
			}
		}
		return arrayWithoutSpaces;
	}
}
