/*
 *  Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you 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 org.apache.myfaces.trinidad.util;

import java.io.OutputStream;
import java.io.Writer;
import java.io.IOException;
import org.apache.myfaces.trinidad.logging.TrinidadLogger;

/**
 * An OutputStream that encodes data in a base64 representation.
 * It takes a Writer as its single argument to its constructor and all
 * bytes written to the stream are correspondingly converted into Base64
 * and written out to the provided writer.
 */
public class Base64OutputStream extends OutputStream
{
  public Base64OutputStream(Writer out)
  {
    _out = out;
    _numLeftoverBytes = 0;
    _leftoverBytes = new byte[2];
  }
  
  /**
   *  Takes a byte writes it out to the writer
   * 
   * @param b   a byte
   */
  @Override
  public void write(int b) throws IOException
  {
    _single[0] = (byte) b;
    this.write(_single, 0, 1);
  } 
  
  /**
   * Writes len bytes from the specified byte array starting at offset off 
   * to this output stream. The general contract for write(b, off, len) is 
   * that some of the bytes in the array b are written to the output stream 
   * in order; element b[off] is the first byte written and b[off+len-1] is 
   * the last byte written by this operation.
   * 
   * The write method of OutputStream calls the write method of one argument 
   * on each of the bytes to be written out. Subclasses are encouraged to 
   * override this method and provide a more efficient implementation.
   * 
   * If b is null, a NullPointerException is thrown.
   * 
   * If off is negative, or len is negative, or off+len is greater than the 
   * length of the array b, then an IndexOutOfBoundsException is thrown. 
   * 
   * 
   * @param b        the data
   * @param off      the start offset in the data
   * @param len      the number of bytes to read
   */
  @Override
  public void write(byte[] b, int off, int len) 
                                      throws IOException, NullPointerException
  {
    if (b==null) 
    {
      throw new NullPointerException(_LOG.getMessage(
        "BYTE_ARRAY_CANNOT_BE_NULL"));
    }
      
    if (off<0 || len<0 || off+len>b.length) 
    {
      throw new IndexOutOfBoundsException(_LOG.getMessage(
        "ACTUAL_LENGTH_OFFSET", new Object[]{b.length,off,len}));
    }
    
    int lengthToProcess = len;
    int index = off;
    
    // base case 1: if only processing one byte from byte array
    if (lengthToProcess==1) 
    {
      if (_numLeftoverBytes==0) 
      {
        // remember this byte for next call to write
        _numLeftoverBytes = 1;
        _leftoverBytes[0] = b[index];
      }
      else if (_numLeftoverBytes==1) 
      {
        // remember this byte for next call to write
        _numLeftoverBytes = 2;
        _leftoverBytes[1] = b[index];
      }
      else if (_numLeftoverBytes==2) 
      {
        // this one byte is enough to complete a triplet
        // so convert triplet into Base64
        _writeBase64(_leftoverBytes[0], _leftoverBytes[1], b[index]);
        _numLeftoverBytes=0;
      }
      return;
    }  //end if (lengthToProcess==1)
    
    // base case 2: if only processing two bytes from byte array
    if (lengthToProcess==2) 
    {
      if (_numLeftoverBytes==0) 
      {
        // not enough to process a triplet, so remember these two bytes 
        // for next call to write
        _numLeftoverBytes = 2;
        _leftoverBytes[0] = b[index];
        _leftoverBytes[1] = b[index+1];
      }
      else if (_numLeftoverBytes==1) 
      {
        // these two bytes form triplet combined with the leftover byte
        _writeBase64(_leftoverBytes[0], b[index], b[index+1]);
        _numLeftoverBytes = 0;
      }
      else if (_numLeftoverBytes==2) 
      {
        // two leftover bytes and one new byte form a triplet
        // the second new byte is remembered for next call to write
        _writeBase64(_leftoverBytes[0], _leftoverBytes[1], b[index]);
        _leftoverBytes[0] = b[index+1];
        _numLeftoverBytes = 1;
      }
      return;
    }  // end if (lengthToProcess==2)
    
    // case involving looping
    if (lengthToProcess>2) 
    {
      if (_numLeftoverBytes==1) 
      {
        _writeBase64(_leftoverBytes[0], b[index], b[index+1]);
        _numLeftoverBytes = 0;
        lengthToProcess -= 2;
        index += 2;
        // proceed with loop
      }      
      else if (_numLeftoverBytes==2) 
      {
        _writeBase64(_leftoverBytes[0], _leftoverBytes[1], b[index]);
        _numLeftoverBytes = 0;
        lengthToProcess -= 1;
        index += 1;
        // proceed with loop
      }

      _processArray(b, index, lengthToProcess);
    }

  } //end write(byte[], int off, int len)
  
  @Override
  public void flush() throws IOException 
  {
    _out.flush();
  }

  /**
   * Call this method to indicate end of data stream 
   * and to append any padding characters if necessary.  This method should be 
   * called only if there will be no subsequent calls to a write method.  
   * Subsequent calls to the write method will result in incorrect encoding.
   * 
   * @deprecated use the close() method instead.
   */
  @Deprecated
  public void finish() throws IOException
  {
    close();
  }
  
  /**
   * Call this method to indicate end of data stream 
   * and to append any padding characters if necessary.  This method should be 
   * called only if there will be no subsequent calls to a write method.  
   * Subsequent calls to the write method will result in incorrect encoding.
   * 
   */
  @Override
  public void close() throws IOException
  {
    if (_numLeftoverBytes==1) 
    {
      // grab the one byte from the leftover array
      byte b1 = _leftoverBytes[0];
      
      // convert to two base 64 chars
      int c1, c2;
      c1 = (b1>>2)&0x3f;
      c2 = (b1<<4)&0x3f;
      
      char[] encodedChars = _fourChars;
      
      encodedChars[0] = _encode(c1);
      encodedChars[1] = _encode(c2);
      // append two padding characters
      encodedChars[2] = '=';
      encodedChars[3] = '=';
      
      _out.write(encodedChars);
    } 
    else if (_numLeftoverBytes==2)
    {
      // grab the two bytes from the leftovers array
      byte b1, b2;
      b1 = _leftoverBytes[0];
      b2 = _leftoverBytes[1];
      
      // convert the two bytes into three base64 chars
      int c1, c2, c3;
      c1 = (b1>>2)&0x3f;
      c2 = (b1<<4 | ((b2>>4)&0x0f))&0x3f;
      c3 = (b2<<2)&0x3f;
      
      char[] encodedChars = _fourChars;
      
      encodedChars[0] = _encode(c1);
      encodedChars[1] = _encode(c2);
      encodedChars[2] = _encode(c3);  
      //append one padding character
      encodedChars[3] = '=';
      
      _out.write(encodedChars);
    } 
    _out.close();
  }
  
  
  /**
   * Encodes three bytes in base64 representation and writes the corresponding 
   * four base64 characters to the output writer.
   * 
   * @param b1  the first byte
   * @param b2  the second byte
   * @param b3  the third byte
   */
  private void _writeBase64(byte b1, byte b2, byte b3) throws IOException
  {
    int c1, c2, c3, c4;
    char[] encodedChars = _fourChars;
    
    c1 = (b1>>2)&0x3f;          // b1.high6
    c2 = (b1<<4 | ((b2>>4)&0x0f) )&0x3f;  // b1.low2 + b2.high4
    c3 = (b2<<2 | ((b3>>6)&0x03) )&0x3f;  // b2.low4 + b3.high2
    c4 = b3&0x3f;               // b3.low6
    
    encodedChars[0] = _encode(c1);
    encodedChars[1] = _encode(c2);
    encodedChars[2] = _encode(c3);
    encodedChars[3] = _encode(c4);
    
    // write array of chars to writer
    _out.write(encodedChars);    
  }
  
  /**
   * Writes lengthToProcess number of bytes from byte array to writer 
   * in base64 beginning with startIndex. Assumes all leftover bytes from 
   * previous calls to write have been dealt with.  
   * 
   * @param b               the data
   * @param startIndex      the start offset in the data
   * @param lengthToProcess the number of bytes to read
   */
  private void _processArray(byte[] b, int startIndex, int lengthToProcess) 
                                                        throws IOException
  {
    int index = startIndex;
  
    // loop through remaining length of array
    while(lengthToProcess>0) {
      // base case: only one byte
      if (lengthToProcess==1) 
      {        
        // save this byte for next call to write
        _numLeftoverBytes = 1;
        _leftoverBytes[0] = b[index];
        return;
      }
      // base case: only two bytes
      else if (lengthToProcess==2) 
      {     
        // save these two bytes for next call to write
        _numLeftoverBytes = 2;
        _leftoverBytes[0] = b[index];
        _leftoverBytes[1] = b[index+1]; 
        return;
      } 
      else 
      {
        // encode three bytes (24 bits) from input array
        _writeBase64(b[index],b[index+1],b[index+2]);
        
        // finally, make some progress in loop
        lengthToProcess -= 3;
        index +=3;
       }
    } //end while
  }
  
  

  /**
   * Encodes a six-bit pattern into a base-64 character.
   * 
   * @param c an integer whose lower 6 bits contain the base64 representation
   *          all other bits should be zero
   */
  private static char _encode(int c) 
  {
    if (c < 26)
      return (char)('A' + c);
    if (c < 52)
      return (char)('a' + (c-26));
    if (c < 62)
      return (char)('0' + (c-52));
    if (c == 62)
      return '+';
    if (c == 63)
      return '/';
      
    throw new AssertionError("Invalid B64 character code:"+c);
  }

  
  /** stores leftover bytes from previous call to write method **/
  private final byte[]      _leftoverBytes;
  
  /** indicates the number of bytes that were leftover after the last triplet 
   * was formed in the last call to write method  **/
  private int         _numLeftoverBytes;
  
  // cached four-character array
  private final char[]      _fourChars = new char[4];
  // cached single-byte array
  private final byte[]      _single = new byte[1];

  // Writer that will receive all completed character output
  private final Writer      _out;  
  private static final TrinidadLogger _LOG = TrinidadLogger.createTrinidadLogger(
    Base64OutputStream.class);
}
