/*
 * Copyright (C) 2012 Michael Brown <mbrown@fensystems.co.uk>.
 *
 * 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., 51 Franklin Street, Fifth Floor, Boston, MA
 * 02110-1301, USA.
 */

/**
 * @file
 *
 * Xpress Compression Algorithm (MS-XCA) decompression
 *
 */

#include "wimboot.h"
#include "huffman.h"
#include "xpress.h"

#pragma GCC diagnostic ignored "-Wcast-align"

/**
 * Decompress XCA-compressed data
 *
 * @v data		Compressed data
 * @v len		Length of compressed data
 * @v buf		Decompression buffer, or NULL
 * @ret out_len		Length of decompressed data, or negative error
 */
ssize_t xca_decompress ( const void *data, size_t len, void *buf ) {
	const void *src = data;
	const void *end = ( uint8_t * ) src + len;
	uint8_t *out = buf;
	size_t out_len = 0;
	size_t out_len_threshold = 0;
	const struct xca_huf_len *lengths;
	struct xca xca;
	uint32_t accum = 0;
	int extra_bits = 0;
	unsigned int huf;
	struct huffman_symbols *sym;
	unsigned int raw;
	unsigned int match_len;
	unsigned int match_offset_bits;
	unsigned int match_offset;
	const uint8_t *copy;
	int rc;

	/* Process data stream */
	while ( src < end ) {

		/* (Re)initialise decompressor if applicable */
		if ( out_len >= out_len_threshold ) {

			/* Construct symbol lengths */
			lengths = src;
			src = ( uint8_t * ) src + sizeof ( *lengths );
			if ( src > end ) {
				DBG ( "XCA too short to hold Huffman lengths table.\n");
				return -1;
			}
			for ( raw = 0 ; raw < XCA_CODES ; raw++ )
				xca.lengths[raw] = xca_huf_len ( lengths, raw );

			/* Construct Huffman alphabet */
			if ( ( rc = huffman_alphabet ( &xca.alphabet,
						       xca.lengths,
						       XCA_CODES ) ) != 0 )
				return rc;

			/* Initialise state */
			accum = XCA_GET16 ( src );
			accum <<= 16;
			accum |= XCA_GET16 ( src );
			extra_bits = 16;

			/* Determine next threshold */
			out_len_threshold = ( out_len + XCA_BLOCK_SIZE );
		}

		/* Determine symbol */
		huf = ( accum >> ( 32 - HUFFMAN_BITS ) );
		sym = huffman_sym ( &xca.alphabet, huf );
		raw = huffman_raw ( sym, huf );
		accum <<= huffman_len ( sym );
		extra_bits -= huffman_len ( sym );
		if ( extra_bits < 0 ) {
			accum |= ( XCA_GET16 ( src ) << ( -extra_bits ) );
			extra_bits += 16;
		}

		/* Process symbol */
		if ( raw < XCA_END_MARKER ) {

			/* Literal symbol - add to output stream */
			if ( buf )
				*(out++) = raw;
			out_len++;

		} else if ( ( raw == XCA_END_MARKER ) &&
			    ( (uint8_t *) src >= ( ( uint8_t * ) end - 1 ) ) ) {

			/* End marker symbol */
			return out_len;

		} else {

			/* LZ77 match symbol */
			raw -= XCA_END_MARKER;
			match_offset_bits = ( raw >> 4 );
			match_len = ( raw & 0x0f );
			if ( match_len == 0x0f ) {
				match_len = XCA_GET8 ( src );
				if ( match_len == 0xff ) {
					match_len = XCA_GET16 ( src );
				} else {
					match_len += 0x0f;
				}
			}
			match_len += 3;
			if ( match_offset_bits ) {
				match_offset =
					( ( accum >> ( 32 - match_offset_bits ))
					  + ( 1 << match_offset_bits ) );
			} else {
				match_offset = 1;
			}
			accum <<= match_offset_bits;
			extra_bits -= match_offset_bits;
			if ( extra_bits < 0 ) {
				accum |= ( XCA_GET16 ( src ) << (-extra_bits) );
				extra_bits += 16;
			}

			/* Copy data */
			out_len += match_len;
			if ( buf ) {
				copy = ( out - match_offset );
				while ( match_len-- )
					*(out++) = *(copy++);
			}
		}
	}

	return out_len;
}
