/*
 *  Copyright 2011 Tor-Einar Jarnbjo
 *
 *  Licensed 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 de.jarnbjo.jsnappy;

import java.util.Arrays;

/**
 * This class provide utility methods for decompressing 
 * data blocks using the Snappy algorithm.
 * 
 * @author Tor-Einar Jarnbjo
 * @since 1.0
 */
public class SnappyDecompressor {

	private SnappyDecompressor() {		
	}
	
	/**
	 * Equivalent to <code>decompress(in, 0, in.length, null)</code>.
	 * @param in compressed data block
	 * @return decompressed data block
	 */
	public static Buffer decompress(byte[] in) {
		return decompress(in, 0, in.length, null);
	}

	/**
	 * Equivalent to <code>decompress(in, 0, in.length, out)</code>.
	 * @param in compressed data block
	 * @param out Buffer for decompressed data block
	 * @return reference to <code>out</code>
	 */
	public static Buffer decompress(byte[] in, Buffer out) {
		return decompress(in, 0, in.length, out);
	}

	/**
	 * Equivalent to <code>decompress(in, offset, length, null)</code>.
	 * @param in byte array containing the compressed data block
	 * @param offset offset in <code>in<code>, on which decoding is started
	 * @param length length of compressed data block 
	 * @return decompressed data block
	 */
	public static Buffer decompress(byte[] in, int offset, int length) {
		return decompress(in, offset, length, null);
	}

	/**
	 * Equivalent to <code>decompress(in.getData(), 0, in.getLength(), null)</code>.
	 * @param in compressed data block
	 * @return decompressed data block
	 */
	public static Buffer decompress(Buffer in) {
		return decompress(in.getData(), 0, in.getLength());
	}

	/**
	 * Equivalent to <code>decompress(in.getData(), 0, in.getLength(), out)</code>.
	 * @param in compressed data block
	 * @param out Buffer for decompressed data block
	 * @return reference to <code>out</code>
	 */
	public static Buffer decompress(Buffer in, Buffer out) {
		return decompress(in.getData(), 0, in.getLength(), out);
	}

	/**
	 * Decompress the data contained in <code>in</code> from <code>offset</code>
	 * and <code>length</code> bytes. If an output buffer is provided, the buffer
	 * is reused for the decompressed data. If the buffer is too small, its capacity
	 * is expanded to fit the result. If a <code>null</code> argument is passed,
	 * a new buffer is allocated.
	 * @param in
	 * @param offset
	 * @param length
	 * @param out
	 * @throws FormatViolationException if the input data is invalid
	 * @return
	 */
	public static Buffer decompress(byte[] in, int offset, int length, Buffer out) throws FormatViolationException {

		int i=0, l, o, c;
		int sourceIndex = offset, targetIndex = 0;
		int targetLength = 0;

		i = 0;
		do {
			targetLength += (in[sourceIndex] & 0x7f) << (i++ * 7);
		} while ((in[sourceIndex++] & 0x80) == 0x80);

		if (out == null) {
			out = new Buffer(targetLength);
		}
		else {
			out.ensureCapacity(targetLength);
		}

		out.setLength(targetLength);
		byte[] outBuffer = out.getData();

		while(sourceIndex < offset + length) {
			
			if(targetIndex >= targetLength) {
				throw new FormatViolationException("Superfluous input data encountered on offset " + sourceIndex, sourceIndex);
			}
			
			switch(in[sourceIndex] & 3) {
			case 0:
				l = (in[sourceIndex++] >> 2) & 0x3f;
				switch(l) {
				case 60:
					l = in[sourceIndex++] & 0xff;
					l++;
					break;
				case 61:
					l = in[sourceIndex++] & 0xff;
					l |= (in[sourceIndex++] & 0xff) << 8;
					l++;
					break;
				case 62:
					l = in[sourceIndex++] & 0xff;
					l |= (in[sourceIndex++] & 0xff) << 8;
					l |= (in[sourceIndex++] & 0xff) << 16;
					l++;
					break;
				case 63:
					l = in[sourceIndex++] & 0xff;
					l |= (in[sourceIndex++] & 0xff) << 8;
					l |= (in[sourceIndex++] & 0xff) << 16;
					l |= (in[sourceIndex++] & 0xff) << 24;
					l++;
					break;
				default:
					l++;
					break;
				}
				System.arraycopy(in, sourceIndex, outBuffer, targetIndex, l);
				sourceIndex += l;
				targetIndex += l;
				break;
			case 1:
				l = 4 + ((in[sourceIndex] >> 2) & 7);
				o = (in[sourceIndex++] & 0xe0) << 3;
				o |= in[sourceIndex++] & 0xff;
				if(l < o) {
					System.arraycopy(outBuffer, targetIndex - o, outBuffer, targetIndex, l);
					targetIndex += l;
				}
				else {
					if(o == 1) {
						Arrays.fill(outBuffer, targetIndex, targetIndex + l, outBuffer[targetIndex-1]);
						targetIndex += l;
					}
					else {
						while(l > 0) {
							c = l > o ? o : l;
							System.arraycopy(outBuffer, targetIndex - o, outBuffer, targetIndex, c);
							targetIndex += c;
							l -= c;
						}
					}
				}
				break;
			case 2:
				l = ((in[sourceIndex++] >> 2) & 0x3f) + 1;
				o = in[sourceIndex++] & 0xff;
				o |= (in[sourceIndex++] & 0xff) << 8;
				if(l < o) {
					System.arraycopy(outBuffer, targetIndex - o, outBuffer, targetIndex, l);
					targetIndex += l;
				}
				else {
					while(l > 0) {
						c = l > o ? o : l;
						System.arraycopy(outBuffer, targetIndex - o, outBuffer, targetIndex, c);
						targetIndex += c;
						l -= c;
					}
				}
				break;
			case 3:
				l = ((in[sourceIndex++] >> 2) & 0x3f) + 1;
				o = in[sourceIndex++] & 0xff;
				o |= (in[sourceIndex++] & 0xff) << 8;
				o |= (in[sourceIndex++] & 0xff) << 16;
				o |= (in[sourceIndex++] & 0xff) << 24;
				if(l < o) {
					System.arraycopy(outBuffer, targetIndex - o, outBuffer, targetIndex, l);
					targetIndex += l;
				}
				else {
					if(o == 1) {
						Arrays.fill(outBuffer, targetIndex, targetIndex + l, outBuffer[targetIndex-1]);
						targetIndex += l;
					}
					else {
						while(l > 0) {
							c = l > o ? o : l;
							System.arraycopy(outBuffer, targetIndex - o, outBuffer, targetIndex, c);
							targetIndex += c;
							l -= c;
						}
					}
				}
				break;
			}
		}

		return out;
	}

}
