/* ====================================================================
 * 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.CharsetEncoder;
import java.nio.charset.CoderResult;

/**
 * <p>The CharsetEncoder used to encode both variants of the UTF-7 charset and the 
 * modified-UTF-7 charset.</p>
 * 
 * <p><strong>Please note this class does not behave strictly according to the specification in 
 * Sun Java VMs before 1.6.</strong> This is done to get around a bug in the implementation 
 * of {@link java.nio.charset.CharsetEncoder#encode(CharBuffer)}. Unfortunately, that method 
 * cannot be overridden.</p>
 *  
 * @see <a href="http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6221056">JDK bug 6221056</a>
 * 
 * @author Jaap Beetstra
 */
class UTF7StyleCharsetEncoder extends CharsetEncoder {
	private static final float AVG_BYTES_PER_CHAR = 1.5f;
	private static final float MAX_BYTES_PER_CHAR = 5.0f;
	private final UTF7StyleCharset cs;
	private final Base64Util base64;
	private final byte shift;
	private final byte unshift;
	private final boolean strict;
	private boolean base64mode;
	private int bitsToOutput;
	private int sextet;
	static boolean useUglyHackToForceCallToFlushInJava5;
	static {
		String version = System.getProperty("java.specification.version");
		String vendor = System.getProperty("java.vm.vendor");
		useUglyHackToForceCallToFlushInJava5 = "1.4".equals(version) || "1.5".equals(version);
		useUglyHackToForceCallToFlushInJava5 &= "Sun Microsystems Inc.".equals(vendor);
	}

	UTF7StyleCharsetEncoder(UTF7StyleCharset cs, Base64Util base64, boolean strict) {
		super(cs, AVG_BYTES_PER_CHAR, MAX_BYTES_PER_CHAR);
		this.cs = cs;
		this.base64 = base64;
		this.strict = strict;
		this.shift = cs.shift();
		this.unshift = cs.unshift();
	}

	/* (non-Javadoc)
	 * @see java.nio.charset.CharsetEncoder#implReset()
	 */
	protected void implReset() {
		base64mode = false;
		sextet = 0;
		bitsToOutput = 0;
	}

	/**
	 * {@inheritDoc}
	 * 
	 * <p>Note that this method might return <code>CoderResult.OVERFLOW</code> (as is 
	 * required by the specification) if insufficient space is available in the output 
	 * buffer. However, calling it again on JDKs before Java 6 triggers a bug in 
	 * {@link java.nio.charset.CharsetEncoder#flush(ByteBuffer)} causing it to throw an 
	 * IllegalStateException (the buggy method is <code>final</code>, thus cannot be 
	 * overridden).</p>
	 * 
	 * @see <a href="http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6227608">JDK bug 6227608</a>
	 * @param out The output byte buffer
	 * @return A coder-result object describing the reason for termination
	 */
	protected CoderResult implFlush(ByteBuffer out) {
		if (base64mode) {
			if (out.remaining() < 2)
				return CoderResult.OVERFLOW;
			if (bitsToOutput != 0)
				out.put(base64.getChar(sextet));
			out.put(unshift);
		}
		return CoderResult.UNDERFLOW;
	}

	/**
	 * {@inheritDoc}
	 * 
	 * <p>Note that this method might return <code>CoderResult.OVERFLOW</code>, even 
	 * though there is sufficient space available in the output buffer. This is done 
	 * to force the broken implementation of 
	 * {@link java.nio.charset.CharsetEncoder#encode(CharBuffer)} to call flush 
	 * (the buggy method is <code>final</code>, thus cannot be overridden).</p>
	 * <p>However, String.getBytes() fails if CoderResult.OVERFLOW is returned, since
	 * this assumes it always allocates sufficient bytes (maxBytesPerChar * nr_of_chars).
	 * Thus, as an extra check, the size of the input buffer is compared against the size
	 * of the output buffer.
	 * A static variable is used to indicate if a broken java version is used.</p>
	 * <p>It is not possible to directly write the last few bytes, since more bytes 
	 * might be waiting to be encoded then those available in the input buffer.</p> 
	 * 
	 * @see <a href="http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6221056">JDK bug 6221056</a>
	 * @param in The input character buffer
	 * @param out The output byte buffer
	 * @return A coder-result object describing the reason for termination
	 */
	protected CoderResult encodeLoop(CharBuffer in, ByteBuffer out) {
		while (in.hasRemaining()) {
			if (out.remaining() < 4)
				return CoderResult.OVERFLOW;
			char ch = in.get();
			if (cs.canEncodeDirectly(ch)) {
				unshift(out, ch);
				out.put((byte) ch);
			} else if (!base64mode && ch == shift) {
				out.put(shift);
				out.put(unshift);
			} else
				encodeBase64(ch, out);
		}
		/* <HACK type="ugly">
		 These lines are required to trick JDK 1.5 and earlier into flushing when using 
		 Charset.encode(String), Charset.encode(CharBuffer) or CharsetEncoder.encode(CharBuffer)
		 Without them, the last few bytes may be missing.
		 */
		if (base64mode && useUglyHackToForceCallToFlushInJava5
				&& out.limit() != MAX_BYTES_PER_CHAR * in.limit())
			return CoderResult.OVERFLOW;
		/* </HACK> */
		return CoderResult.UNDERFLOW;
	}

	/**
	 * <p>Writes the bytes necessary to leave <i>base 64 mode</i>. This might include an unshift 
	 * character.</p>
	 *  
	 * @param out
	 * @param ch
	 */
	private void unshift(ByteBuffer out, char ch) {
		if (!base64mode)
			return;
		if (bitsToOutput != 0)
			out.put(base64.getChar(sextet));
		if (base64.contains(ch) || ch == unshift || strict)
			out.put(unshift);
		base64mode = false;
		sextet = 0;
		bitsToOutput = 0;
	}

	/**
	 * <p>Writes the bytes necessary to encode a character in <i>base 64 mode</i>. All bytes
	 * which are fully determined will be written. The fields <code>bitsToOutput</code> and
	 * <code>sextet</code> are used to remember the bytes not yet fully determined.</p>
	 *  
	 * @param out
	 * @param ch
	 */
	private void encodeBase64(char ch, ByteBuffer out) {
		if (!base64mode)
			out.put(shift);
		base64mode = true;
		bitsToOutput += 16;
		while (bitsToOutput >= 6) {
			bitsToOutput -= 6;
			sextet += (ch >> bitsToOutput);
			sextet &= 0x3F;
			out.put(base64.getChar(sextet));
			sextet = 0;
		}
		sextet = (ch << (6 - bitsToOutput)) & 0x3F;
	}
}