/*
 * MimeInputStream.java - A Filter stream for MIME encoding
 *
 * Copyright (C) 2000,,2003 2002 Matt Albrecht
 * groboclown@users.sourceforge.net
 * http://groboutils.sourceforge.net
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */ 


package net.sourceforge.groboutils.util.io.v1;

import java.io.FilterInputStream;
import java.io.InputStream;
import java.io.IOException;

/**
 * java.io.FilterInputStream implementation for Mime base 64. Not incredibly
 * efficient, but it works and is small.
 *
 * All we need to implement are:
 *     read(int)
 *     read( byte b[], int off, int len )
 *     skip( long n ) - for translating the # of bytes to skip into mime bytes (4-to-3 ratio)
 *     available() - for the same reason as skip
 *
 * @author   Matt Albrecht <a href="mailto:groboclown@users.sourceforge.net">groboclown@users.sourceforge.net</a>
 * @since    0.9.0 Alpha (early 2000)
 * @version  $Date: 2003/02/10 22:52:45 $
 */
public class MimeInputStream extends FilterInputStream
{
    private int bits = 0, spare = 0;

    /**
     * Mime character set translation
     */
    private static final int[] charset = {
       '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','+','/' };
    private static final int UPPER_START = 0;
    private static final int LOWER_START = 26;
    private static final int NUM_START = LOWER_START+26;
    private static final int PLUS = NUM_START+10;
    private static final int SLASH = PLUS+1;
    private static final int pad = '=';

    /** Constructor! */
    public MimeInputStream( InputStream i )
    { super(i); }

    /**
     * Write the specified <code>byte</code>, performing mime encoding.
     *
     * <p>Override this method, since all other write methods call it.
     *
     * @exception  IOException  If an I/O error occurs
     */
    public int read() throws IOException
    {
       int s;
       int c, val;

       // find the mime value - fast way doesn't loop through the mime charset
       for (;;)
       {
          c = super.read();
          if (c < 0) return c; // EOF

          if (c >= 'A' && c <= 'Z')       { val = c - 'A' + UPPER_START; }
          else if (c >= 'a' && c <= 'z')  { val = c - 'a' + LOWER_START; }
          else if (c >= '0' && c <= '9')  { val = c - '0' + NUM_START; }
          else if (c == '+')              { val = PLUS; }
          else if (c == '/')              { val = SLASH; }
          else if (c == pad)
            { throw new IOException("end-of-mime character encountered"); }
          else // ignore out-of-bounds characters, per specs
            continue;
          switch (bits)
          {
             case 0: bits++;
                 spare = val << 2;
                 // didn't get a full byte - continue the read
               break;
             case 1: bits++;
                 s = spare | ((val >> 4) & 0x03);
                 spare = (val << 4) &  0xF0;
                 return s;
             case 2: bits++;
                 s = spare | ((val >> 2) & 0x0F);
                 spare = (val << 6) & 0xC0;
                 return s;
             case 3: bits = 0;
                 // val is already masked (&) with 0x3f
                 return spare | val;
          }
       }
    }

    /**
     * Reads up to <code>len</code> bytes of data from this input stream 
     * into an array of bytes. This method blocks until some input is 
     * available. 
     * <p>
     * This method performs <code>in.read(b, off, len)</code>, translates the 
     * mime characters, and returns the result.
     *
     * @param      b     the buffer into which the data is read.
     * @param      off   the start offset of the data.
     * @param      len   the maximum number of bytes read.
     * @return     the total number of bytes read into the buffer, or
     *             <code>-1</code> if there is no more data because the end of
     *             the stream has been reached.
     * @exception  IOException  if an I/O error occurs.
     * @see        java.io.FilterInputStream#in
    public int read(byte b[], int off, int len) throws IOException
    {
       // size checking
       if (b == null || b.length <= off || b.length <= off+len)
         throw new IllegalArgumentException();

       int s, i = off, j, sofar = 0;
       int c, val,
           ourlen;
       byte buffer[];

       // out-of-bounds values may throw us off the correct count
       while (sofar < len)
       {
          j = 0;
          ourlen = (len - sofar) * 4;
          if ((ourlen % 3) != 0) { ourlen /= 3; ourlen++; }
          else ourlen /= 3;

          buffer = new byte[ ourlen ];

          // translate the length to mime size
          in.read( buffer, 0, ourlen );

          // find the mime value - fast way doesn't loop through the mime charset
          for (; j < ourlen; j++)
          {
             c = buffer[j];
             if (c >= 'A' && c <= 'Z')       { val = c - 'A' + UPPER_START; }
             else if (c >= 'a' && c <= 'z')  { val = c - 'a' + LOWER_START; }
             else if (c >= '0' && c <= '9')  { val = c - '0' + NUM_START; }
             else if (c == '+')              { val = PLUS; }
             else if (c == '/')              { val = SLASH; }
             else if (c == pad)
             {
                // end of mime
                b[i] = (byte)spare;
                return sofar + j + 1;
             }
             else // ignore out-of-bounds characters, per specs
               continue;

             switch (bits)
             {
                case 0: bits++;
                    spare = val << 2;
                    // didn't get a full byte - continue the read
                  break;
                case 1: bits++;
                    b[i++] = (byte)(spare | ((val >> 4) & 0x03));
                    spare = (val << 4) &  0x03;
                  break;
                case 2: bits = 0;
                    // val is already masked (&) with 0x3f
                    b[i++] = (byte)(spare | val);
                  break;
             }
          } // end of for loop
          sofar += j;
       }
       return sofar+1;
    }
     */


    /**
     * Skips over and discards <code>n</code> bytes of data from the 
     * input stream. The <code>skip</code> method may, for a variety of 
     * reasons, end up skipping over some smaller number of bytes, 
     * possibly <code>0</code>. The actual number of bytes skipped is 
     * returned. 
     * <p>
     * This method performs <code>in.skip(n)</code>, followed by a quick
     * translation to mime size.
     *
     * @param      n   the number of bytes to be skipped.
     * @return     the actual number of bytes skipped.
     * @exception  IOException  if an I/O error occurs.
     */
    public long skip(long n) throws IOException
    {
       long p = n * 4;
       if ((p % 3) != 0) { p /= 3; p++; }
                    else { p /= 3; }
	 p = in.skip(p);
       p *= 3;
       if ((p & 0x03) != 0) { p >>= 2; p++; }
                       else { p >>= 2; }
       return p;
    }


    /**
     * Returns the number of bytes that can be read from this input 
     * stream without blocking. 
     * <p>
     * This method performs <code>in.available(n)</code>, does the mime-size
     * conversion, and returns the result.
     *
     * @return     the number of bytes that can be read from the input stream
     *             without blocking.
     * @exception  IOException  if an I/O error occurs.
     * @see        java.io.FilterInputStream#in
     */
    public int available() throws IOException
    {
	 int p = in.available() * 3;
       if ((p & 0x03) != 0) { p >>= 2; p++; }
                       else { p >>= 2; }
       return p;
    }
}
