/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */


/* 
 *
 * Author : 
 *  Bertrand Guiheneuf <bertrand@helixcode.com>
 *
 * Copyright 1999, 2000 HelixCode (http://www.helixcode.com) .
 *
 * This program is free software; you can redistribute it and/or 
 * modify it under the terms of the GNU 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
 * USA
 */
#include <config.h>
#include "camel-stream-b64.h"

#define BSIZE 512



static CamelStreamClass *parent_class = NULL;

static guchar char_to_six_bits [256] = {
	128, 128, 128, 128, 128, 128, 128, 128, /*   0 .. 7   */
	128, 128, 128, 128, 128, 128, 128, 128, /*   8 .. 15  */
	128, 128, 128, 128, 128, 128, 128, 128, /*  16 .. 23  */
	128, 128, 128, 128, 128, 128, 128, 128, /*  24 .. 31  */
	128, 128, 128, 128, 128, 128, 128, 128, /*  32 .. 39  */
	128, 128, 128,  62, 128, 128, 128,  63, /*  40 .. 47  */
	 52,  53,  54,  55,  56,  57,  58,  59, /*  48 .. 55  */
	 60,  61, 128, 128, 128,  64, 128, 128, /*  56 .. 63  */
	128,   0,   1,   2,   3,   4,   5,   6, /*  64 .. 71  */
	  7,   8,   9,  10,  11,  12,  13,  14, /*  72 .. 79  */
	 15,  16,  17,  18,  19,  20,  21,  22, /*  80 .. 87  */
	 23,  24,  25, 128, 128, 128, 128, 128, /*  88 .. 95  */
	128,  26,  27,  28,  29,  30,  31,  32, /*  96 .. 103 */
	 33,  34,  35,  36,  37,  38,  39,  40, /* 104 .. 111 */
	 41,  42,  43,  44,  45,  46,  47,  48, /* 112 .. 119 */
	 49,  50,  51, 128, 128, 128, 128, 128, /* 120 .. 127 */
	128, 128, 128, 128, 128, 128, 128, 128, /* 128 .. 135 */
	128, 128, 128, 128, 128, 128, 128, 128, /* 136 .. 143 */
	128, 128, 128, 128, 128, 128, 128, 128, /* 144 .. 151 */
	128, 128, 128, 128, 128, 128, 128, 128, /* 152 .. 159 */
	128, 128, 128, 128, 128, 128, 128, 128, /* 160 .. 167 */
	128, 128, 128, 128, 128, 128, 128, 128, /* 168 .. 175 */
	128, 128, 128, 128, 128, 128, 128, 128, /* 176 .. 183 */
	128, 128, 128, 128, 128, 128, 128, 128, /* 184 .. 191 */
	128, 128, 128, 128, 128, 128, 128, 128, /* 192 .. 199 */
	128, 128, 128, 128, 128, 128, 128, 128, /* 200 .. 207 */
	128, 128, 128, 128, 128, 128, 128, 128, /* 208 .. 215 */
	128, 128, 128, 128, 128, 128, 128, 128, /* 216 .. 223 */
	128, 128, 128, 128, 128, 128, 128, 128, /* 224 .. 231 */
	128, 128, 128, 128, 128, 128, 128, 128, /* 232 .. 239 */
	128, 128, 128, 128, 128, 128, 128, 128, /* 240 .. 247 */
	128, 128, 128, 128, 128, 128, 128, 128  /* 248 .. 255 */
};


static gchar six_bits_to_char[65] = 
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";

/* Returns the class for a CamelStreamB64 */
#define CSB64_CLASS(so) CAMEL_STREAM_B64_CLASS (GTK_OBJECT(so)->klass)

static void           my_init_with_input_stream         (CamelStreamB64 *stream_b64, 
							 CamelStream *input_stream);

static gint           my_read                           (CamelStream *stream, 
							 gchar *buffer, 
							 gint n);

static void           my_reset                          (CamelStream *stream);

static gint           my_read_decode                    (CamelStream *stream, 
							 gchar *buffer, 
							 gint n);
static gint           my_read_encode                    (CamelStream *stream, 
							 gchar *buffer, 
							 gint n);
static gboolean       my_eos                            (CamelStream *stream);

static void
camel_stream_b64_class_init (CamelStreamB64Class *camel_stream_b64_class)
{
	CamelStreamClass *camel_stream_class = CAMEL_STREAM_CLASS (camel_stream_b64_class);
	
	
	parent_class = gtk_type_class (camel_stream_get_type ());
	
	/* virtual method definition */
	camel_stream_b64_class->init_with_input_stream = my_init_with_input_stream;
	
	
	/* virtual method overload */
	camel_stream_class->read     = my_read;
	camel_stream_class->eos      = my_eos; 
	camel_stream_class->reset    = my_reset; 
	
	/* signal definition */
	
}

GtkType
camel_stream_b64_get_type (void)
{
	static GtkType camel_stream_b64_type = 0;
	
	if (!camel_stream_b64_type)	{
		GtkTypeInfo camel_stream_b64_info =	
		{
			"CamelStreamB64",
			sizeof (CamelStreamB64),
			sizeof (CamelStreamB64Class),
			(GtkClassInitFunc) camel_stream_b64_class_init,
			(GtkObjectInitFunc) NULL,
				/* reserved_1 */ NULL,
				/* reserved_2 */ NULL,
			(GtkClassInitFunc) NULL,
		};
		
		camel_stream_b64_type = gtk_type_unique (camel_stream_get_type (), &camel_stream_b64_info);
	}
	
	return camel_stream_b64_type;
}


static void 
my_reemit_available_signal (CamelStream *parent_stream, gpointer user_data)
{
	gtk_signal_emit_by_name (GTK_OBJECT (user_data), "data_available");
}

static void           
my_init_with_input_stream (CamelStreamB64 *stream_b64, 
			   CamelStream *input_stream)
{
	g_assert (stream_b64);
	g_assert (input_stream);

	
	
	/* by default, the stream is in decode mode */	
	stream_b64->mode = CAMEL_STREAM_B64_DECODER;
	
	stream_b64->eos = FALSE;
	stream_b64->status.decode_status.keep = 0;
	stream_b64->status.decode_status.state = 0;
		
	stream_b64->input_stream = input_stream;
	
	gtk_object_ref (GTK_OBJECT (input_stream));
	
	/* 
	 *  connect to the parent stream "data_available"
	 *  stream so that we can reemit the signal on the 
	 *  seekable substream in case some data would 
	 *  be available for us 
	 */
	gtk_signal_connect (GTK_OBJECT (input_stream),
			    "data_available", 
			    my_reemit_available_signal,
			    stream_b64);
	
	
	/* bootstrapping signal */
	gtk_signal_emit_by_name (GTK_OBJECT (stream_b64), "data_available");
	
	
}



CamelStream *
camel_stream_b64_new_with_input_stream (CamelStream *input_stream)
{
	CamelStreamB64 *stream_b64;
	
	stream_b64 = gtk_type_new (camel_stream_b64_get_type ());
	CSB64_CLASS (stream_b64)->init_with_input_stream (stream_b64, input_stream);

	return CAMEL_STREAM (stream_b64);
}




void       
camel_stream_b64_set_mode (CamelStreamB64 *stream_b64,
			   CamelStreamB64Mode mode)
{
	g_assert (stream_b64);

	stream_b64->mode = mode;

	if (mode == CAMEL_STREAM_B64_DECODER) {
		stream_b64->status.decode_status.keep = 0;
		stream_b64->status.decode_status.state = 0;
	} else {
		stream_b64->status.encode_status.keep = 0;
		stream_b64->status.encode_status.state = 0;
		stream_b64->status.encode_status.end_state = 0;
	}

}




static gint 
my_read (CamelStream *stream, 
	 gchar *buffer, 
	 gint n)
{
	CamelStreamB64 *stream_b64 = CAMEL_STREAM_B64 (stream);
	
	g_assert (stream);
        
	if (stream_b64->mode == CAMEL_STREAM_B64_DECODER)
		return my_read_decode (stream, buffer, n);
	else 
		return my_read_encode (stream, buffer, n);
}



static gint 
my_read_decode (CamelStream *stream, 
		gchar *buffer, 
		gint n)
{
	CamelStreamB64 *stream_b64 = CAMEL_STREAM_B64 (stream);
	CamelStream64DecodeStatus *status;
	CamelStream    *input_stream;
	guchar six_bits_value;
	gint nb_read_in_input;
	guchar c;
	gint j = 0;
	
	g_assert (stream);
	input_stream = stream_b64->input_stream;
	
	g_assert (input_stream);
	status = &(stream_b64->status.decode_status);
	

	nb_read_in_input = camel_stream_read (input_stream, &c, 1);
	
	while ((nb_read_in_input >0 ) && (j<n)) {
		
		six_bits_value = char_to_six_bits[c];
		
		/* if we encounter an '=' we can assume the end of the stream 
		   has been found */
		if (six_bits_value == 64) {
			stream_b64->eos = TRUE;
			status->keep = 0;
			
			break;
		}
		
		/* test if we must ignore the character */
		if (six_bits_value != 128) {
			six_bits_value = six_bits_value & 0x3f;

			switch (status->state){
			case 0:
				status->keep =  six_bits_value << 2;
				
				break;
			case 1:
				buffer [j++] = status->keep | (six_bits_value >> 4);
				status->keep = (six_bits_value & 0xf) << 4;
				break;
			case 2:
				buffer [j++] = status->keep | (six_bits_value >> 2);
				status->keep = (six_bits_value & 0x3) << 6;
				break;
			case 3:
				buffer [j++] = status->keep | six_bits_value;
				status->keep = 0;
				break;
			}
			
			status->state = (status->state + 1) % 4;
			
		}
		
		if (j<n) nb_read_in_input = camel_stream_read (input_stream, &c, 1);
		
	}
	
	if ((nb_read_in_input == 0) && (camel_stream_eos (input_stream)))
		stream_b64->eos = TRUE;

	return j;
	
}


static gint 
my_read_encode (CamelStream *stream, 
		gchar *buffer, 
		gint n)
{
	CamelStreamB64 *stream_b64 = CAMEL_STREAM_B64 (stream);
	CamelStream64EncodeStatus *status;
	CamelStream *input_stream;
	gint nb_read_in_input = 0;
	guchar c;
	gint j = 0;
	gboolean end_of_read = FALSE;
	
	g_assert (stream);
	input_stream = stream_b64->input_stream;
	
	g_assert (input_stream);

	/* I don't know why the caller would want to 
	   read a zero length buffer but ... */
	if (n == 0)
		return 0;


	status = &(stream_b64->status.encode_status);
	
	
	if (status->end_state == 0) { 
		/* we are not at the end of the input steam, 
		   process the data normally */
		
		while ((j<n) && !end_of_read) {
			
			/* check if we must break the encoded line */
			if (status->line_length == 76) {
				buffer [j++] = '\n';
				status->line_length = 0;
				break;
			}
			
			/* 
			 * because we encode four characters for 
			 * 3 bytes, the last char does not need any  
			 * read to write in the stream
			 */
			if (status->state == 3) {
				buffer [j++] = six_bits_to_char [status->keep];
				status->state = 0;
				status->keep = 0;
				status->line_length++;
				break;
			}
			
			/* 
			 * in all the other phases of the stream 
			 * writing, we need to read a byte from the
			 * input stream 
			 */
			nb_read_in_input = camel_stream_read (input_stream, &c, 1);
			
			if (nb_read_in_input > 0) {
				switch (status->state){

				case 0:				
					buffer [j++] = six_bits_to_char [(c >> 2) & 0x3f];
					status->keep = (c & 0x3 ) << 4;
					break;
					
				case 1:
					buffer [j++] = six_bits_to_char [status->keep | (c >> 4)];
					status->keep = (c & 0x0f ) << 2;
					break;
					
				case 2:
					buffer [j++] = six_bits_to_char [status->keep | (c >> 6)] ;
					status->keep = (c & 0x3f );
					break;
					
				}
				
				status->state = (status->state + 1) % 4;
				status->line_length++;
			} else 
				end_of_read = TRUE;
			
			
			if (camel_stream_eos (input_stream))
				status->end_state = 1;
			
		}
	}
	
	/* 
	 * now comes the real annoying part. Because some clients
	 * expect the b64 encoded sequence length to be multiple of 4,
	 * we must pad the end with '='. 
	 * This is trivial when we write to stream as much as we want
	 * but this is not the case when we are limited in the number
	 * of chars we can write to the output stream. The consequence
	 * of this is that we must keep the state of the writing
	 * so that we can resume the next time this routine is called. 
	 */

	if ( status->end_state != 0) { 
		
		/* 
		 * we are at the end of the input stream
		 * we must pad the output with '='.
		 */
		printf ("%d\n", status->end_state);
		while ((j<n) && (status->end_state != 6)) {
			
			if (status->end_state == 5) {

				status->end_state = 6;
				buffer [j++] = '\n';
				stream_b64->eos = TRUE;

			} else {
				
				switch (status->state) {
					
				/* 
				 * depending on state of the decoder, we need to 
				 * write different things. 
				 */
				case 0:	       
				/* 
				 * everyting has been written already and the
				 * output length is already a multiple of 3
				 * so that we have nothing to do.  
				 */
					status->end_state = 5;
					break;
					
				case 1:
				/* 
				 * we have something in keep  
				 * and two '=' we must write
				 */
					switch (status->end_state) {
					case 1:
						buffer [j++] = six_bits_to_char [status->keep] ;
						status->end_state++;
						break;
					case 2:
						buffer [j++] = '=';
						status->end_state++;
						break;
					case 3:
						buffer [j++] = '=';
						status->end_state = 5;
						break;
					}
					
					
					break;
					
					
				case 2:
				/* 
				 * we have something in keep  
				 * and one '=' we must write
				 */
					switch (status->end_state) {
					case 1:
						buffer [j++] = six_bits_to_char [status->keep];
						status->end_state++;
						break;
					case 2:
						buffer [j++] = '=';
						status->end_state = 5;
						break;
					}
					
					break;
					
				case 3:
				/* 
				 * we have something in keep we must write
				 */
					switch (status->end_state) {
					case 1:
						buffer [j++] = six_bits_to_char [status->keep];
						status->end_state++;
						break;
					case 2:
						buffer [j++] = '=';
						status->end_state = 5;
						break;
					}
					
					break;
				}
				
				
			}
		}
		
	}

	return j;
}
	
	
	
	
	
static gboolean
my_eos (CamelStream *stream)
{
	CamelStreamB64 *stream_b64 = CAMEL_STREAM_B64 (stream);
	
	g_assert (stream);
	g_assert (stream_b64->input_stream);
	
	return (stream_b64->eos);
}





static void 
my_reset (CamelStream *stream)
{
	CamelStreamB64 *stream_b64 = CAMEL_STREAM_B64 (stream);
	
	g_assert (stream);
	g_assert (stream_b64->input_stream);
	
	stream_b64->status.decode_status.keep = 0;
	stream_b64->status.decode_status.state = 0;
	
	stream_b64->eos = FALSE;

	camel_stream_reset (stream_b64->input_stream);
}



void
camel_stream_b64_write_to_stream (CamelStreamB64 *stream_b64, 
				  CamelStream *output_stream)
{
	gchar tmp_buf[4096];
	gint nb_read;
	gint nb_written;

	/* 
	 * default implementation that uses the input 
	 * stream and stream it in a blocking way
	 * to an output stream.
	 */
	g_assert (output_stream);
	g_assert (stream_b64);
	
	while (!camel_stream_eos (CAMEL_STREAM (stream_b64))) {
		nb_read = camel_stream_read (CAMEL_STREAM (stream_b64), tmp_buf, 4096);
		nb_written = 0;
		
		while (nb_written < nb_read) 
			nb_written += camel_stream_write (output_stream, tmp_buf + nb_written, nb_read - nb_written);
	}
	
}