/*
 * @(#)QuotedPrintable.java
 *
 * Copyright (c) 2001-2002 JScape
 * 1147 S. 53rd Pl., Mesa, Arizona, 85206, U.S.A.
 * All rights reserved.
 *
 * This software is the confidential and proprietary information of
 * JScape. ("Confidential Information").  You shall not disclose such
 * Confidential Information and shall use it only in accordance with
 * the terms of the license agreement you entered into with JScape.
 */

package com.jscape.inet.util;


import java.util.BitSet;


/**
 * Implements methods for encoding and decoding using quoted-printable algorithm.
 */
public class QuotedPrintable
        implements Codec {
    private static BitSet unsafe;

    static {
        // EBCDIC unsafe characters to be quoted in quoted-printable
        // See first NOTE in section 6.7 of rfc-2045
        unsafe = new BitSet(256);
        unsafe.set('!');
        unsafe.set('"');
        unsafe.set('#');
        unsafe.set('$');
        unsafe.set('@');
        unsafe.set('[');
        unsafe.set('\\');
        unsafe.set(']');
        unsafe.set('^');
        unsafe.set('`');
        unsafe.set('{');
        unsafe.set('|');
        unsafe.set('}');
        unsafe.set('~');
    }

    /**
     * Encodes data using quoted-printable algorithm.
     *
     * @param contents the data to encode.
     */
    public char[] encode(byte[] contents) {
        char[] map = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' };
        char[] nl = "\r\n".toCharArray();
        char[] res = null;

        if (contents.length == 1) {
            res = new char[4];
        } else {
            res = new char[(int) (contents.length * 4.0)];
        }

        char ch;
        int cnt = 0;
        int didx = 1;
        int last = 0;
        int slen = contents.length;

        for (int sidx = 0; sidx < slen; sidx++) {
            ch = (char) contents[sidx];
            if (ch == nl[0] && match(contents, sidx, nl)) {         // Rule #4
                if (res[didx - 1] == ' ') {            // Rule #3
                    res[didx - 1] = '=';
                    res[didx++] = '2';
                    res[didx++] = '0';
                } else if (res[didx - 1] == '\t') {        // Rule #3
                    res[didx - 1] = '=';
                    res[didx++] = '0';
                    res[didx++] = '9';
                }
                res[didx++] = '\r';
                res[didx++] = '\n';
                sidx += nl.length - 1;
                cnt = didx;
            } else if (ch > 126 || (ch < 32 && ch != '\t') || ch == '=' || unsafe.get((int) ch)) { // Rule #1, #2
                res[didx++] = '=';
                res[didx++] = map[(ch & 0xf0) >>> 4];
                res[didx++] = map[ch & 0x0f];
            } else {                        // Rule #1
                res[didx++] = ch;
            }
            if (didx > cnt + 70) {                // Rule #5
                res[didx++] = '=';
                res[didx++] = '\r';
                res[didx++] = '\n';
                cnt = didx;
            }
            if (didx > res.length - 5) {
                // grow array
                char[] tmp = new char[res.length + 500];

                System.arraycopy(res, 0, tmp, 0, didx);
                res = tmp;
            }
        }
        didx--;
        char[] ret = new char[didx];

        if (ret.length > 0) {
            System.arraycopy(res, 1, ret, 0, didx);
        }
        return ret;
    }

    /**
     * Decodes data encoded using quoted-printable algorithm.
     *
     * @throws Exception if an error occurs while decoding data.
     */
    public byte[] decode(char[] contents)
            throws Exception {

        byte[] res = new byte[(int) (contents.length * 1.1)];
        char[] nl = System.getProperty("line.separator", "\n").toCharArray();
        int last = 0;
        int didx = 0;
        int slen = contents.length;

        for (int sidx = 0; sidx < slen; ) {
            char ch = contents[sidx++];

            if (ch == '=') {
                if (sidx >= slen - 1) {
                    break;
                }

                if (contents[sidx] == '\n' || contents[sidx] == '\r') { // Rule #5
                    sidx++;
                    if (contents[sidx - 1] == '\r' && contents[sidx] == '\n') {
                        sidx++;
                    }
                } else {                    // Rule #1
                    char repl;
                    int hi = Character.digit(contents[sidx], 16),
                            lo = Character.digit(contents[sidx + 1], 16);

                    if ((hi | lo) < 0) {
                        // accept and continue
                        repl = contents[sidx];
                        sidx++;
                    } else {
                        repl = (char) (hi << 4 | lo);
                        sidx += 2;
                    }
                    res[didx++] = (byte) repl;
                }
                last = didx;
            } else if (ch == '\n' || ch == '\r') {    // Rule #4
                if (ch == '\r' && sidx < slen && contents[sidx] == '\n') {
                    sidx++;
                }
                for (int idx = 0; idx < nl.length; idx++) {
                    res[last++] = (byte) nl[idx];
                }
                didx = last;
            } else {                    // Rule #1, #2
                res[didx++] = (byte) ch;
                if (ch != ' ' && ch != '\t') {        // Rule #3
                    last = didx;
                }
            }
            if (didx > res.length - nl.length - 2) {
                // grow array
                byte[] tmp = new byte[res.length + 500];

                System.arraycopy(res, 0, tmp, 0, didx);
                res = tmp;
            }
        }
        byte[] tmp = new byte[didx];

        System.arraycopy(res, 0, tmp, 0, didx);
        return tmp;
    }

    private static boolean match(byte[] str, int start, char[] arr) {
        if (str.length < start + arr.length) {
            return false;
        }

        for (int idx = 1; idx < arr.length; idx++) {
            if (str[start + idx] != arr[idx]) {
                return false;
            }
        }
        return true;
    }
}

