package com.planet_ink.coffee_mud.core;

/**
 * Encodes and decodes to and from Base64 notation.
 *
 *
 * Change Log:
 * <ul>
 *  <li>v2.0.2 - Now specifies UTF-8 encoding in places where the code fails on systems
 *   with other encodings (like EBCDIC).</li>
 *  <li>v2.0.1 - Fixed an error when decoding a single byte, that is, when the
 *   encoded data was a single byte.</li>
 *  <li>v2.0 - I got rid of methods that used booleans to set options.
 *   Now everything is more consolidated and cleaner. The code now detects
 *   when data that's being decoded is gzip-compressed and will decompress it
 *   automatically. Generally things are cleaner. You'll probably have to
 *   change some method calls that you were making to support the new
 *   options format (<tt>int</tt>s that you "OR" together).</li>
 *  <li>v1.5.1 - Fixed bug when decompressing and decoding to a
 *   byte[] using <tt>decode( String s, boolean gzipCompressed )</tt>.
 *   Added the ability to "suspend" encoding in the Output Stream so
 *   you can turn on and off the encoding if you need to embed base64
 *   data in an otherwise "normal" stream (like an XML file).</li>
 *  <li>v1.5 - Output stream pases on flush() command but doesn't do anything itself.
 *  	This helps when using GZIP streams.
 *  	Added the ability to GZip-compress objects before encoding them.</li>
 *  <li>v1.4 - Added helper methods to read/write files.</li>
 *  <li>v1.3.6 - Fixed OutputStream.flush() so that 'position' is reset.</li>
 *  <li>v1.3.5 - Added flag to turn on and off line breaks. Fixed bug in input stream
 *  	where last buffer being read, if not completely full, was not returned.</li>
 *  <li>v1.3.4 - Fixed when "improperly padded stream" error was thrown at the wrong time.</li>
 *  <li>v1.3.3 - Fixed I/O streams which were totally messed up.</li>
 * </ul>
 *
 *
 * I am placing this code in the Public Domain. Do with it as you will.
 * This software comes with no guarantees or warranties but with
 * plenty of well-wishing instead!
 * Please visit <a href="http://iharder.net/xmlizable">http://iharder.net/base64</a>
 * periodically to check for updates or to contribute improvements.
 *
 * @author Robert Harder
 * @author rob@iharder.net
 * @version 2.0
 */
public class B64Encoder
{
	private B64Encoder()
	{
	}
	/* Base 64 Encoding stuff */
	private static byte[] ALPHABET;

	public static final int		NO_OPTIONS			= 0;
	public static final int		ENCODE				= 1;
	public static final int		DECODE				= 0;
	public static final int		GZIP				= 2;
	public static final int		DONT_BREAK_LINES	= 8;
	public static final int		MAX_LINE_LENGTH		= 76;
	public static final byte	EQUALS_SIGN			= (byte) '=';
	public static final byte	NEW_LINE			= (byte) '\n';
	public static final String	PREFERRED_ENCODING	= "UTF-8";

	public static final byte[] _NATIVE_ALPHABET = /* May be something funny like EBCDIC */
	{
		(byte)'A', (byte)'B', (byte)'C', (byte)'D', (byte)'E', (byte)'F', (byte)'G',
		(byte)'H', (byte)'I', (byte)'J', (byte)'K', (byte)'L', (byte)'M', (byte)'N',
		(byte)'O', (byte)'P', (byte)'Q', (byte)'R', (byte)'S', (byte)'T', (byte)'U',
		(byte)'V', (byte)'W', (byte)'X', (byte)'Y', (byte)'Z',
		(byte)'a', (byte)'b', (byte)'c', (byte)'d', (byte)'e', (byte)'f', (byte)'g',
		(byte)'h', (byte)'i', (byte)'j', (byte)'k', (byte)'l', (byte)'m', (byte)'n',
		(byte)'o', (byte)'p', (byte)'q', (byte)'r', (byte)'s', (byte)'t', (byte)'u',
		(byte)'v', (byte)'w', (byte)'x', (byte)'y', (byte)'z',
		(byte)'0', (byte)'1', (byte)'2', (byte)'3', (byte)'4', (byte)'5',
		(byte)'6', (byte)'7', (byte)'8', (byte)'9', (byte)'+', (byte)'/'
	};
	public static final byte[] DECODABET =
	{
		-9,-9,-9,-9,-9,-9,-9,-9,-9, 				// Decimal  0 -  8
		-5,-5,  									// Whitespace: Tab and Linefeed
		-9,-9,  									// Decimal 11 - 12
		-5, 										// Whitespace: Carriage Return
		-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, 	// Decimal 14 - 26
		-9,-9,-9,-9,-9, 							// Decimal 27 - 31
		-5, 										// Whitespace: Space
		-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,  			// Decimal 33 - 42
		62, 										// Plus sign at decimal 43
		-9,-9,-9,   								// Decimal 44 - 46
		63, 										// Slash at decimal 47
		52,53,54,55,56,57,58,59,60,61,  			// Numbers zero through nine
		-9,-9,-9,   								// Decimal 58 - 60
		-1, 										// Equals sign at decimal 61
		-9,-9,-9,   								   // Decimal 62 - 64
		0,1,2,3,4,5,6,7,8,9,10,11,12,13,			// Letters 'A' through 'N'
		14,15,16,17,18,19,20,21,22,23,24,25,		// Letters 'O' through 'Z'
		-9,-9,-9,-9,-9,-9,  						// Decimal 91 - 96
		26,27,28,29,30,31,32,33,34,35,36,37,38, 	// Letters 'a' through 'm'
		39,40,41,42,43,44,45,46,47,48,49,50,51, 	// Letters 'n' through 'z'
		-9,-9,-9,-9 								// Decimal 123 - 126
		/*,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 127 - 139
		-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, 	// Decimal 140 - 152
		-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, 	// Decimal 153 - 165
		-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, 	// Decimal 166 - 178
		-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, 	// Decimal 179 - 191
		-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, 	// Decimal 192 - 204
		-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, 	// Decimal 205 - 217
		-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, 	// Decimal 218 - 230
		-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, 	// Decimal 231 - 243
		-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 		// Decimal 244 - 255 */
	};
	public static final byte WHITE_SPACE_ENC = -5; // Indicates white space in encoding
	public static final byte EQUALS_SIGN_ENC = -1; // Indicates equals sign in encoding

	protected static byte[] encode3to4( final byte[] b4, final byte[] threeBytes, final int numSigBytes )
	{
		encode3to4( threeBytes, 0, numSigBytes, b4, 0 );
		return b4;
	}

	static
	{
		byte[] __bytes;
		try
		{
			__bytes = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".getBytes( PREFERRED_ENCODING );
		}   // end try
		catch (final java.io.UnsupportedEncodingException use)
		{
			__bytes = _NATIVE_ALPHABET; // Fall back to native encoding
		}   // end catch
		if(ALPHABET==null)
			ALPHABET = __bytes;
	}

	protected static byte[] encode3to4(final byte[] source, final int srcOffset, final int numSigBytes,
									   final byte[] destination, final int destOffset )
	{
		final int inBuff =   ( numSigBytes > 0 ? ((source[ srcOffset	 ] << 24) >>>  8) : 0 )
					 | ( numSigBytes > 1 ? ((source[ srcOffset + 1 ] << 24) >>> 16) : 0 )
					 | ( numSigBytes > 2 ? ((source[ srcOffset + 2 ] << 24) >>> 24) : 0 );

		switch( numSigBytes )
		{
			case 3:
				destination[ destOffset 	] = ALPHABET[ (inBuff >>> 18)   	 ];
				destination[ destOffset + 1 ] = ALPHABET[ (inBuff >>> 12) & 0x3f ];
				destination[ destOffset + 2 ] = ALPHABET[ (inBuff >>>  6) & 0x3f ];
				destination[ destOffset + 3 ] = ALPHABET[ (inBuff   	) & 0x3f ];
				return destination;

			case 2:
				destination[ destOffset 	] = ALPHABET[ (inBuff >>> 18)   	 ];
				destination[ destOffset + 1 ] = ALPHABET[ (inBuff >>> 12) & 0x3f ];
				destination[ destOffset + 2 ] = ALPHABET[ (inBuff >>>  6) & 0x3f ];
				destination[ destOffset + 3 ] = EQUALS_SIGN;
				return destination;

			case 1:
				destination[ destOffset 	] = ALPHABET[ (inBuff >>> 18)   	 ];
				destination[ destOffset + 1 ] = ALPHABET[ (inBuff >>> 12) & 0x3f ];
				destination[ destOffset + 2 ] = EQUALS_SIGN;
				destination[ destOffset + 3 ] = EQUALS_SIGN;
				return destination;

			default:
				return destination;
		}
	}

	public static String B64encodeObject( final java.io.Serializable serializableObject )
	{
		return B64encodeObject( serializableObject, NO_OPTIONS );
	}

	public static String B64encodeObject( final java.io.Serializable serializableObject, final int options )
	{
		final java.io.ByteArrayOutputStream  baos  = new java.io.ByteArrayOutputStream();
		java.io.OutputStream		   b64os = null;
		java.io.ObjectOutputStream     oos   = null;
		java.util.zip.GZIPOutputStream gzos  = null;

		final int gzip		   = (options & GZIP);
		final int dontBreakLines = (options & DONT_BREAK_LINES);

		try
		{
			b64os = new B64OutputStream( baos, ENCODE | dontBreakLines );

			if( gzip == GZIP )
			{
				gzos = new java.util.zip.GZIPOutputStream( b64os );
				oos  = new java.io.ObjectOutputStream( gzos );
			}
			else
				oos   = new java.io.ObjectOutputStream( b64os );

			oos.writeObject( serializableObject );
		}
		catch( final java.io.IOException e )
		{
			e.printStackTrace();
			return null;
		}
		finally
		{
			try
			{
				if (oos != null)
					oos.close();
			}
			catch (final Exception e)
			{
			}
			try
			{
				if (gzos != null)
					gzos.close();
			}
			catch (final Exception e)
			{
			}
			try
			{
				if (b64os != null)
					b64os.close();
			}
			catch (final Exception e)
			{
			}
			try
			{
				baos.close();
			}
			catch (final Exception e)
			{
			}
		}

		try
		{
			return new String( baos.toByteArray(), PREFERRED_ENCODING );
		}
		catch (final java.io.UnsupportedEncodingException uue)
		{
			return new String( baos.toByteArray() );
		}
	}

	public static String B64encodeBytes( final byte[] source )
	{
		return B64encodeBytes( source, 0, source.length, NO_OPTIONS );
	}   // end encodeBytes

	public static String B64encodeBytes( final byte[] source, final int options )
	{
		return B64encodeBytes( source, 0, source.length, options );
	}   // end encodeBytes

	public static String B64encodeBytes( final byte[] source, final int off, final int len )
	{
		return B64encodeBytes( source, off, len, NO_OPTIONS );
	}   // end encodeBytes

	public static String B64encodeBytes( final byte[] source, final int off, final int len, final int options )
	{
		final int dontBreakLines = ( options & DONT_BREAK_LINES );
		final int gzip		   = ( options & GZIP   );

		if( gzip == GZIP )
		{
			final java.io.ByteArrayOutputStream  baos  = new java.io.ByteArrayOutputStream();
			java.util.zip.GZIPOutputStream gzos  = null;
			B64OutputStream 		b64os = null;

			try
			{
				b64os = new B64OutputStream( baos, ENCODE | dontBreakLines );
				gzos  = new java.util.zip.GZIPOutputStream( b64os );

				gzos.write( source, off, len );
				gzos.close();
			}
			catch( final java.io.IOException e )
			{
				e.printStackTrace();
				return null;
			}
			finally
			{
				try
				{
					if (gzos != null)
						gzos.close();
				}
				catch (final Exception e)
				{
				}
				try
				{
					if (b64os != null)
						b64os.close();
				}
				catch (final Exception e)
				{
				}
				try
				{
					baos.close();
				}
				catch (final Exception e)
				{
				}
			}
			try
			{
				return new String( baos.toByteArray(), PREFERRED_ENCODING );
			}
			catch (final java.io.UnsupportedEncodingException uue)
			{
				return new String( baos.toByteArray() );
			}
		}
		final boolean breakLines = dontBreakLines == 0;

		final int    len43   = len * 4 / 3;
		final byte[] outBuff = new byte[   ( len43 )  					// Main 4:3
								   + ( (len % 3) > 0 ? 4 : 0 )  	// Account for padding
								   + (breakLines ? ( len43 / MAX_LINE_LENGTH ) : 0) ]; // New lines
		int d = 0;
		int e = 0;
		final int len2 = len - 2;
		int lineLength = 0;
		for( ; d < len2; d+=3, e+=4 )
		{
			encode3to4( source, d+off, 3, outBuff, e );

			lineLength += 4;
			if( breakLines && lineLength == MAX_LINE_LENGTH )
			{
				outBuff[e+4] = NEW_LINE;
				e++;
				lineLength = 0;
			}
		}

		if( d < len )
		{
			encode3to4( source, d+off, len - d, outBuff, e );
			e += 4;
		}
		try
		{
			return new String( outBuff, 0, e, PREFERRED_ENCODING );
		}
		catch (final java.io.UnsupportedEncodingException uue)
		{
			return new String( outBuff, 0, e );
		}

	}

	protected static int decode4to3( final byte[] source, final int srcOffset, final byte[] destination, final int destOffset )
	{
		if( source[ srcOffset + 2] == EQUALS_SIGN )
		{
			final int outBuff =   ( ( DECODABET[ source[ srcOffset	] ] & 0xFF ) << 18 )
						  | ( ( DECODABET[ source[ srcOffset + 1] ] & 0xFF ) << 12 );

			destination[ destOffset ] = (byte)( outBuff >>> 16 );
			return 1;
		}
		else
		if( source[ srcOffset + 3 ] == EQUALS_SIGN )
		{
			final int outBuff =   ( ( DECODABET[ source[ srcOffset	 ] ] & 0xFF ) << 18 )
						  | ( ( DECODABET[ source[ srcOffset + 1 ] ] & 0xFF ) << 12 )
						  | ( ( DECODABET[ source[ srcOffset + 2 ] ] & 0xFF ) <<  6 );

			destination[ destOffset 	] = (byte)( outBuff >>> 16 );
			destination[ destOffset + 1 ] = (byte)( outBuff >>>  8 );
			return 2;
		}
		else
		{
			try
			{
				final int outBuff =   ( ( DECODABET[ source[ srcOffset	 ] ] & 0xFF ) << 18 )
							  | ( ( DECODABET[ source[ srcOffset + 1 ] ] & 0xFF ) << 12 )
							  | ( ( DECODABET[ source[ srcOffset + 2 ] ] & 0xFF ) <<  6)
							  | ( ( DECODABET[ source[ srcOffset + 3 ] ] & 0xFF )      );

				destination[ destOffset 	] = (byte)( outBuff >> 16 );
				destination[ destOffset + 1 ] = (byte)( outBuff >>  8 );
				destination[ destOffset + 2 ] = (byte)( outBuff 	  );

				return 3;
			}
			catch( final Exception e)
			{
				Log.errOut("CMEncoder",e);
				return -1;
			}
		}
	}

	public static byte[] B64decode( final byte[] source, final int off, final int len )
	{
		final int    len34   = len * 3 / 4;
		final byte[] outBuff = new byte[ len34 ]; // Upper limit on size of output
		int    outBuffPosn = 0;

		final byte[] b4   	 = new byte[4];
		int    b4Posn    = 0;
		int    i		 = 0;
		byte   sbiCrop   = 0;
		byte   sbiDecode = 0;
		for( i = off; i < off+len; i++ )
		{
			sbiCrop = (byte)(source[i] & 0x7f); // Only the low seven bits
			sbiDecode = DECODABET[ sbiCrop ];

			if( sbiDecode >= WHITE_SPACE_ENC ) // White space, Equals sign or better
			{
				if( sbiDecode >= EQUALS_SIGN_ENC )
				{
					b4[ b4Posn++ ] = sbiCrop;
					if( b4Posn > 3 )
					{
						outBuffPosn += decode4to3( b4, 0, outBuff, outBuffPosn );
						b4Posn = 0;

						if( sbiCrop == EQUALS_SIGN )
							break;
					}
				}
			}
			else
			{
				System.err.println( "Bad Base64 input character at " + i + ": " + source[i] + "(decimal)" );
				return null;
			}
		}

		final byte[] out = new byte[ outBuffPosn ];
		System.arraycopy( outBuff, 0, out, 0, outBuffPosn );
		return out;
	}

	public static byte[] B64decode( final String s )
	{
		byte[] bytes;
		try
		{
			bytes = s.getBytes( PREFERRED_ENCODING );
		}
		catch( final java.io.UnsupportedEncodingException uee )
		{
			bytes = s.getBytes();
		}
		bytes = B64decode( bytes, 0, bytes.length );
		if( bytes != null && bytes.length >= 4 )
		{

			final int head = (bytes[0] & 0xff) | ((bytes[1] << 8) & 0xff00);
			if( java.util.zip.GZIPInputStream.GZIP_MAGIC == head )
			{
				java.io.ByteArrayInputStream  bais = null;
				java.util.zip.GZIPInputStream gzis = null;
				java.io.ByteArrayOutputStream baos = null;
				final byte[] buffer = new byte[2048];
				int    length = 0;

				try
				{
					baos = new java.io.ByteArrayOutputStream();
					bais = new java.io.ByteArrayInputStream( bytes );
					gzis = new java.util.zip.GZIPInputStream( bais );

					while( ( length = gzis.read( buffer ) ) >= 0 )
					{
						baos.write(buffer,0,length);
					}
					bytes = baos.toByteArray();

				}
				catch( final java.io.IOException e )
				{
				}
				finally
				{
					try
					{
						if (baos != null)
							baos.close();
					}
					catch (final Exception e)
					{
					}
					try
					{
						if (gzis != null)
							gzis.close();
					}
					catch (final Exception e)
					{
					}
					try
					{
						if (bais != null)
							bais.close();
					}
					catch (final Exception e)
					{
					}
				}
			}
		}

		return bytes;
	}

	public static Object B64decodeToObject( final String encodedObject )
	{
		final byte[] objBytes = B64decode( encodedObject );

		java.io.ByteArrayInputStream  bais = null;
		java.io.ObjectInputStream     ois  = null;
		Object obj = null;

		try
		{
			bais = new java.io.ByteArrayInputStream( objBytes );
			ois  = new java.io.ObjectInputStream( bais );

			obj = ois.readObject();
		}
		catch( final java.io.IOException e )
		{
			e.printStackTrace();
		}
		catch( final java.lang.ClassNotFoundException e )
		{
			e.printStackTrace();
		}
		finally
		{
			try
			{
				if (bais != null)
					bais.close();
			}
			catch (final Exception e)
			{
			}
			try
			{
				if (ois != null)
					ois.close();
			}
			catch (final Exception e)
			{
			}
		}

		return obj;
	}

	public static boolean B64encodeToFile( final byte[] dataToEncode, final String filename )
	{
		boolean success = false;
		B64OutputStream bos = null;
		try
		{
			bos = new B64OutputStream(
					  new java.io.FileOutputStream( filename ), ENCODE );
			bos.write( dataToEncode );
			success = true;
		}
		catch( final java.io.IOException e )
		{

			success = false;
		}
		finally
		{
			try
			{
				if (bos != null)
					bos.close();
			}
			catch (final Exception e)
			{
			}
		}

		return success;
	}

	public static boolean B64decodeToFile( final String dataToDecode, final String filename )
	{
		boolean success = false;
		B64OutputStream bos = null;
		try
		{
			bos = new B64OutputStream(
					  new java.io.FileOutputStream( filename ), DECODE );
			bos.write( dataToDecode.getBytes( PREFERRED_ENCODING ) );
			success = true;
		}
		catch( final java.io.IOException e )
		{
			success = false;
		}
		finally
		{
			try
			{
				if (bos != null)
					bos.close();
			}
			catch (final Exception e)
			{
			}
		}

		return success;
	}

	public static byte[] B64decodeFromFile( final String filename )
	{
		byte[] decodedData = null;
		B64InputStream bis = null;
		try
		{
			final java.io.File file = new java.io.File( filename );
			byte[] buffer = null;
			int length   = 0;
			int numBytes = 0;

			if( file.length() > Integer.MAX_VALUE )
			{
				System.err.println( "File is too big for this convenience method (" + file.length() + " bytes)." );
				return null;
			}
			buffer = new byte[ (int)file.length() ];

			bis = new B64InputStream(
					  new java.io.BufferedInputStream(
					  new java.io.FileInputStream( file ) ), DECODE );

			while( ( numBytes = bis.read( buffer, length, 4096 ) ) >= 0 )
				length += numBytes;

			decodedData = new byte[ length ];
			System.arraycopy( buffer, 0, decodedData, 0, length );

		}
		catch( final java.io.IOException e )
		{
			System.err.println( "Error decoding from file " + filename );
		}
		finally
		{
			try
			{
				if (bis != null)
					bis.close();
			}
			catch (final Exception e)
			{
			}
		}

		return decodedData;
	}

	public static String B64encodeFromFile( final String filename )
	{
		String encodedData = null;
		B64InputStream bis = null;
		try
		{
			final java.io.File file = new java.io.File( filename );
			final byte[] buffer = new byte[ (int)(file.length() * 1.4) ];
			int length   = 0;
			int numBytes = 0;
			bis = new B64InputStream(
					  new java.io.BufferedInputStream(
					  new java.io.FileInputStream( file ) ), ENCODE );
			while( ( numBytes = bis.read( buffer, length, 4096 ) ) >= 0 )
				length += numBytes;
			encodedData = new String( buffer, 0, length, PREFERRED_ENCODING );

		}
		catch( final java.io.IOException e )
		{
			System.err.println( "Error encoding from file " + filename );
		}
		finally
		{
			try
			{
				if (bis != null)
					bis.close();
			}
			catch (final Exception e)
			{
			}
		}

		return encodedData;
	}

	private static class B64InputStream extends java.io.FilterInputStream
	{
		private final boolean	encode;			// Encoding or decoding
		private int				position;		// Current position in the buffer
		private final byte[]	buffer;			// Small buffer holding
												// converted data
		private final int		bufferLength;	// Length of buffer (3 or 4)
		private int				numSigBytes;	// Number of meaningful bytes in the buffer
		private int				lineLength;
		private final boolean	breakLines;		// Break lines at less than 80characters

		public B64InputStream( final java.io.InputStream in, final int options )
		{
			super( in );
			this.breakLines   = (options & DONT_BREAK_LINES) != DONT_BREAK_LINES;
			this.encode 	  = (options & ENCODE) == ENCODE;
			this.bufferLength = encode ? 4 : 3;
			this.buffer   = new byte[ bufferLength ];
			this.position = -1;
			this.lineLength = 0;
		}

		@Override
		public int read() throws java.io.IOException
		{
			if( position < 0 )
			{
				if( encode )
				{
					final byte[] b3 = new byte[3];
					int numBinaryBytes = 0;
					for( int i = 0; i < 3; i++ )
					{
						try
						{
							final int b = in.read();

							if( b >= 0 )
							{
								b3[i] = (byte)b;
								numBinaryBytes++;
							}

						}
						catch( final java.io.IOException e )
						{
							if( i == 0 )
								throw e;

						}
					}

					if( numBinaryBytes > 0 )
					{
						encode3to4( b3, 0, numBinaryBytes, buffer, 0 );
						position = 0;
						numSigBytes = 4;
					}
					else
					{
						return -1;
					}
				}
				else
				{
					final byte[] b4 = new byte[4];
					int i = 0;
					for( i = 0; i < 4; i++ )
					{
						int b = 0;
						do{ b = in.read(); }
						while( b >= 0 && DECODABET[ b & 0x7f ] <= WHITE_SPACE_ENC )
							;

						if( b < 0 )
							break; // Reads a -1 if end of stream

						b4[i] = (byte)b;
					}

					if( i == 4 )
					{
						numSigBytes = decode4to3( b4, 0, buffer, 0 );
						position = 0;
					}
					else
					if( i == 0 )
					{
						return -1;
					}
					else
					{
						throw new java.io.IOException( "Improperly padded Base64 input." );
					}

				}
			}
			if( position >= 0 )
			{
				if( position >= numSigBytes )
					return -1;

				if( encode && breakLines && lineLength >= MAX_LINE_LENGTH )
				{
					lineLength = 0;
					return '\n';
				}
				lineLength++;
				final int b = buffer[ position++ ];
				if( position >= bufferLength )
					position = -1;
				return b & 0xFF;
			}
			throw new java.io.IOException( "Error in Base64 code reading stream." );
		}

		@Override
		public int read( final byte[] dest, final int off, final int len ) throws java.io.IOException
		{
			int i;
			int b;
			for( i = 0; i < len; i++ )
			{
				b = read();

				if( b >= 0 )
					dest[off + i] = (byte)b;
				else
				if( i == 0 )
					return -1;
				else
					break;
			}
			return i;
		}
	}

	private static class B64OutputStream extends java.io.FilterOutputStream
	{
		private final boolean	encode;
		private int				position;
		private byte[]			buffer;
		private final int		bufferLength;
		private int				lineLength;
		private final boolean	breakLines;
		private final byte[]	b4;				// Scratch used in a few places
		private final boolean	suspendEncoding;

		public B64OutputStream( final java.io.OutputStream out, final int options )
		{
			super( out );
			this.breakLines   = (options & DONT_BREAK_LINES) != DONT_BREAK_LINES;
			this.encode 	  = (options & ENCODE) == ENCODE;
			this.bufferLength = encode ? 3 : 4;
			this.buffer 	  = new byte[ bufferLength ];
			this.position     = 0;
			this.lineLength   = 0;
			this.suspendEncoding = false;
			this.b4 		  = new byte[4];
		}

		@Override
		public void write(final int theByte) throws java.io.IOException
		{
			if( suspendEncoding )
			{
				super.out.write( theByte );
				return;
			}
			if( encode )
			{
				buffer[ position++ ] = (byte)theByte;
				if( position >= bufferLength )
				{
					out.write( encode3to4( b4, buffer, bufferLength ) );

					lineLength += 4;
					if( breakLines && lineLength >= MAX_LINE_LENGTH )
					{
						out.write( NEW_LINE );
						lineLength = 0;
					}
					position = 0;
				}
			}
			else
			{
				if( DECODABET[ theByte & 0x7f ] > WHITE_SPACE_ENC )
				{
					buffer[ position++ ] = (byte)theByte;
					if( position >= bufferLength )
					{
						final int len = decode4to3( buffer, 0, b4, 0 );
						out.write( b4, 0, len );
						position = 0;
					}
				}
				else
				if( DECODABET[ theByte & 0x7f ] != WHITE_SPACE_ENC )
				{
					throw new java.io.IOException( "Invalid character in Base64 data." );
				}
			}
		}

		@Override
		public void write( final byte[] theBytes, final int off, final int len ) throws java.io.IOException
		{
			if( suspendEncoding )
			{
				super.out.write( theBytes, off, len );
				return;
			}
			for( int i = 0; i < len; i++ )
			{
				write( theBytes[ off + i ] );
			}
		}

		public void flushBase64() throws java.io.IOException
		{
			if( position > 0 )
			{
				if( encode )
				{
					out.write( encode3to4( b4, buffer, position ) );
					position = 0;
				}
				else
				{
					throw new java.io.IOException( "Base64 input not properly padded." );
				}
			}
		}

		@Override
		public void close() throws java.io.IOException
		{
			flushBase64();
			super.close();

			buffer = null;
			out    = null;
		}
	}
}
