/* ====================================================================
 * 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;
    }
}
