/* ====================================================================
 * Copyright (c) 2006 J.T. Beetstra
 *
 * Permission is hereby granted, free of charge, to any person obtaining 
 * a copy of this software and associated documentation files (the 
 * "Software"), to deal in the Software without restriction, including 
 * without limitation the rights to use, copy, modify, merge, publish, 
 * distribute, sublicense, and/or sell copies of the Software, and to 
 * permit persons to whom the Software is furnished to do so, subject to 
 * the following conditions:
 *
 * The above copyright notice and this permission notice shall be 
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 
 * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 
 * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 
 * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 
 * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 * ====================================================================
 */
package com.beetstra.jutf7;

import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CoderResult;

/**
 * <p>The CharsetDecoder used to decode both variants of the UTF-7 charset and the 
 * modified-UTF-7 charset.</p>
 * 
 * @author Jaap Beetstra
 */
class UTF7StyleCharsetDecoder extends CharsetDecoder {
	private final Base64Util base64;
	private final byte shift;
	private final byte unshift;
	private final boolean strict;
	private boolean base64mode;
	private int bitsRead;
	private int tempChar;
	private boolean justShifted;
	private boolean justUnshifted;

	UTF7StyleCharsetDecoder(UTF7StyleCharset cs, Base64Util base64, boolean strict) {
		super(cs, 0.6f, 1.0f);
		this.base64 = base64;
		this.strict = strict;
		this.shift = cs.shift();
		this.unshift = cs.unshift();
	}

	/* (non-Javadoc)
	 * @see java.nio.charset.CharsetDecoder#decodeLoop(java.nio.ByteBuffer, java.nio.CharBuffer)
	 */
	protected CoderResult decodeLoop(ByteBuffer in, CharBuffer out) {
		while (in.hasRemaining()) {
			byte b = in.get();
			if (base64mode) {
				if (b == unshift) {
					if (base64bitsWaiting())
						return malformed(in);
					if (justShifted) {
						if (!out.hasRemaining())
							return overflow(in);
						out.put((char) shift);
					} else
						justUnshifted = true;
					setUnshifted();
				} else {
					if (!out.hasRemaining())
						return overflow(in);
					CoderResult result = handleBase64(in, out, b);
					if (result != null)
						return result;
				}
				justShifted = false;
			} else {
				if (b == shift) {
					base64mode = true;
					if (justUnshifted && strict)
						return malformed(in);
					justShifted = true;
					continue;
				}
				if (!out.hasRemaining())
					return overflow(in);
				out.put((char) b);
				justUnshifted = false;
			}
		}
		return CoderResult.UNDERFLOW;
	}

	private CoderResult overflow(ByteBuffer in) {
		in.position(in.position() - 1);
		return CoderResult.OVERFLOW;
	}

	/**
	 * <p>Decodes a byte in <i>base 64 mode</i>. Will directly write a character to the output 
	 * buffer if completed.</p>
	 * 
	 * @param in The input buffer
	 * @param out The output buffer
	 * @param lastRead Last byte read from the input buffer
	 * @return CoderResult.malformed if a non-base 64 character was encountered in strict 
	 *   mode, null otherwise
	 */
	private CoderResult handleBase64(ByteBuffer in, CharBuffer out, byte lastRead) {
		CoderResult result = null;
		int sextet = base64.getSextet(lastRead);
		if (sextet >= 0) {
			bitsRead += 6;
			if (bitsRead < 16) {
				tempChar += sextet << (16 - bitsRead);
			} else {
				bitsRead -= 16;
				tempChar += sextet >> (bitsRead);
				out.put((char) tempChar);
				tempChar = (sextet << (16 - bitsRead)) & 0xFFFF;
			}
		} else {
			if (strict)
				return malformed(in);
			out.put((char) lastRead);
			if (base64bitsWaiting())
				result = malformed(in);
			setUnshifted();
		}
		return result;
	}

	/* (non-Javadoc)
	 * @see java.nio.charset.CharsetDecoder#implFlush(java.nio.CharBuffer)
	 */
	protected CoderResult implFlush(CharBuffer out) {
		if ((base64mode && strict) || base64bitsWaiting())
			return CoderResult.malformedForLength(1);
		return CoderResult.UNDERFLOW;
	}

	/* (non-Javadoc)
	 * @see java.nio.charset.CharsetDecoder#implReset()
	 */
	protected void implReset() {
		setUnshifted();
		justUnshifted = false;
	}

	/**
	 * <p>Resets the input buffer position to just before the last byte read, and returns
	 * a result indicating to skip the last byte.</p>
	 * 
	 * @param in The input buffer
	 * @return CoderResult.malformedForLength(1);
	 */
	private CoderResult malformed(ByteBuffer in) {
		in.position(in.position() - 1);
		return CoderResult.malformedForLength(1);
	}

	/**
	 * @return True if there are base64 encoded characters waiting to be written
	 */
	private boolean base64bitsWaiting() {
		return tempChar != 0 || bitsRead >= 6;
	}

	/**
	 * <p>Updates internal state to reflect the decoder is no longer in <i>base 64 
	 * mode</i></p>
	 */
	private void setUnshifted() {
		base64mode = false;
		bitsRead = 0;
		tempChar = 0;
	}
}