/*
 * @(#)RFC2047Encoder.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.mime;

import com.jscape.inet.util.Base64;
import com.jscape.inet.util.Codec;
import com.jscape.inet.util.CodecFactory;
import com.jscape.inet.util.QuotedPrintable;

import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import java.util.Vector;
import java.util.regex.Pattern;


/**
 * Implements methods for encoding and decoding non-ASCII header data in a
 * <code>MimeMessage</code>.  Encoding and decoding is performed according to
 * standards defined in RFC 2047.
 */
@SuppressWarnings("unchecked")
public class RFC2047Encoder {

    /**
     * Encodes data according to RFC 2047.
     *
     * @param data              the data to encode
     * @param characterEncoding the character encoding to use
     * @param offset            the number of characters to offset when calculating maximum line length of 72 characters
     *                          if MIME header folding is used.
     *
     * @return encoded data
     *
     * @throws UnsupportedEncodingException if character encoding is invalid
     */
    public static String encode(String data, String characterEncoding,
                                int offset)
            throws UnsupportedEncodingException {
        if (offset >= 75) {
            throw new UnsupportedEncodingException("According with RFC 2047 encoded-words can not be greater than 75 characters");
        }
        String encoded = getEncodedValue(data, characterEncoding);

        int maxLen = 75 - offset;
        boolean ascii = false;

        if (characterEncoding.equalsIgnoreCase("ascii") || characterEncoding.equalsIgnoreCase("us-ascii")) {
            ascii = true;
        }

        // check if encoded length is too long
        if (encoded.length() >= maxLen) {
            StringBuffer buffer = new StringBuffer();
            char[] array = data.toCharArray();
            int start = 0;

            outer:
            while (true) {
                int i = 1;
                inner:
                for (; i <= (array.length - start); ++i) {
                    String tmp = new String(array, start, i);
                    encoded = getEncodedValue(tmp, characterEncoding);

                    // maximum met .. back off 1 character
                    if (encoded.length() >= maxLen) {
                        --i;
                        tmp = new String(array, start, i);
                        encoded = getEncodedValue(tmp, characterEncoding);
                        start = start + i;
                        buffer.append(encoded);

                        // break up encoding if non ascii
                        if (!ascii) {
                            buffer.append(" ");
                        }

                        break inner;
                    } else if ((start + i + 1) == array.length) {
                        tmp = new String(array, start, i + 1);
                        encoded = getEncodedValue(tmp, characterEncoding);
                        buffer.append(encoded);

                        break outer;
                    }
                }
            }

            encoded = buffer.toString();
        }

        return encoded;
    }

    /**
     * Gets encoded value of data.
     *
     * @param data              the data to encode
     * @param characterEncoding the character set to use
     *
     * @return the encoded value
     *
     * @throws UnsupportedEncodingException if an error occurs while encoding data
     */
    private static String getEncodedValue(String data, String characterEncoding)
            throws UnsupportedEncodingException {
        StringBuilder buffer = new StringBuilder();

        if ((characterEncoding != null) && !characterEncoding.equalsIgnoreCase("us-ascii") && !characterEncoding.equalsIgnoreCase("ascii")) {
            Base64 base64 = new Base64();
            buffer.append("=?");
            buffer.append(characterEncoding);
            buffer.append("?B?");

            char[] encoded = base64.encode(data.getBytes(Charset.forName(characterEncoding)));
            buffer.append(new String(encoded));
            buffer.append("?=");
        } else {
            buffer.append(data);
        }

        return buffer.toString();
    }

    /**
     * Get decoded value.
     *
     * @param value
     *
     * @return decoded value
     */
    public static String getDecodedValue(String value) {
        String ret = value.replaceAll("\\?=\\s+=\\?", "\\?==\\?");
        Vector startIndexes = getStartIndexes(value);
        Vector endIndexes = getEndIndexes(value);

        int lastSelectedEndIndex = 0;
        Pattern patternB = Pattern.compile("=\\?.*(\\?B\\?){1}.*\\?=", Pattern.CASE_INSENSITIVE);
        Pattern patternQ = Pattern.compile("=\\?.*(\\?Q\\?){1}.*\\?=", Pattern.CASE_INSENSITIVE);

        for (int i = 0; i < startIndexes.size(); i++) {
            int startIndex = ((Integer) startIndexes.get(i)).intValue();
            for (int j = 0; j < endIndexes.size(); j++) {
                int endIndex = ((Integer) endIndexes.get(j)).intValue();

                if (endIndex < startIndex) {
                    continue;
                }

                int length = endIndex - startIndex;
                if (length < 7) {
                    continue;
                }

                String subString = value.substring(startIndex, endIndex + 2);

                if (startIndex < lastSelectedEndIndex) {
                    continue;
                }

                if (patternB.matcher(subString).matches() || patternQ.matcher(subString).matches()) {
                    lastSelectedEndIndex = endIndex;
                    ret = ret.replace(subString, RFC2047Encoder.getDecodedWord(subString));
                }
            }
        }

        return ret;
    }

    /**
     * Get end indexes.
     *
     * @param str
     *
     * @return
     */
    private static Vector getEndIndexes(String str) {
        String ending = "?=";
        Vector endIndexes = new Vector();
        int index = str.indexOf(ending);
        while (index < str.length() && index != -1) {
            endIndexes.add(new Integer(index));
            index += ending.length();
            index = str.indexOf(ending, index);
        }
        return endIndexes;
    }

    /**
     * Get starting indexes.
     *
     * @param str
     *
     * @return vector
     */
    private static Vector getStartIndexes(String str) {
        String starting = "=?";
        Vector startIndexes = new Vector();
        int index = str.indexOf(starting);
        while (index < str.length() && index != -1) {
            startIndexes.add(new Integer(index));
            index += starting.length();
            index = str.indexOf(starting, index);
        }
        return startIndexes;
    }

    public static String getDecodedWord(String word) {
        byte[] decoded = null;
        // find tokens
        int beginCharacterSet = word.indexOf("?") + 1;
        int endCharacterSet = word.indexOf("?", beginCharacterSet);
        int beginCodec = endCharacterSet + 1;
        int beginData = beginCodec + 2;
        int endData = word.indexOf("?=", beginData);

        // get character set
        String characterSet = word.substring(beginCharacterSet, endCharacterSet);
        // get codec string
        String codecStr = word.substring(beginCodec, beginCodec + 1);
        // get encoded data
        String data = word.substring(beginData, endData);
        // get codec
        Codec codec = CodecFactory.getCodec(codecStr);

        // if QuotedPrintable then replace _ with =20 per RFC 2047
        if (codec instanceof QuotedPrintable) {
            if (data.contains("_")) {
                char[] dataArray = data.toCharArray();
                StringBuilder buffer = new StringBuilder();

                for (int i = 0; i < dataArray.length; ++i) {
                    char c = dataArray[i];

                    if (c == '_') {
                        buffer.append("=20");
                    } else {
                        buffer.append(c);
                    }
                }

                data = buffer.toString();
            }
        }

        // decode word
        if (codec != null) {
            try {
                decoded = codec.decode(data.toCharArray());
            } catch (Exception e) {
                return word;
            }
        }

        try {
            return new String(decoded, Charset.forName(characterSet));
        } catch (Exception e) {
            return word;
        }
    }


    /**
     * Decodes data according to RFC 2047.
     *
     * @param value the data to decode
     *
     * @return the decoded data
     *
     * @throws UnsupportedEncodingException if unable to decode due to invalid
     *                                      character encoding
     */
    public static String decode(String value)
            throws UnsupportedEncodingException {


        byte[] decoded = null;
        int start = value.indexOf("=?");

        // set end of encoded word
        int end = value.indexOf("?=", start) + 2;
        //System.out.println("End : " + end);
        // System.out.println(value.substring(end, value.length()));

        while (true) {
            try {
                char cc = value.charAt(end);
                int tmp = end;

                if (!Character.isWhitespace(cc)) {
                    end = value.indexOf("?=", end);

                    if (end == -1) {
                        end = tmp;

                        break;
                    } else {
                        end = end + 2;
                    }
                } else {
                    break;
                }
            } catch (IndexOutOfBoundsException ie) {
                break;
            }
        }

        // retrieve encoded word
        String word = value.substring(start, end);

        // find tokens
        int beginCharacterSet = word.indexOf("?") + 1;
        int endCharacterSet = word.indexOf("?", beginCharacterSet);
        int beginCodec = endCharacterSet + 1;
        int beginData = beginCodec + 2;
        int endData = word.indexOf("?=", beginData);

        // get character set
        String characterSet = word.substring(beginCharacterSet, endCharacterSet);
        // get codec string
        String codecStr = word.substring(beginCodec, beginCodec + 1);
        // get encoded data
        String data = word.substring(beginData, endData);
        // get codec
        Codec codec = CodecFactory.getCodec(codecStr);

        // if QuotedPrintable then replace _ with =20 per RFC 2047
        if (codec instanceof QuotedPrintable) {
            if (data.indexOf("_") != -1) {
                char[] dataArray = data.toCharArray();
                StringBuffer buffer = new StringBuffer();

                for (int i = 0; i < dataArray.length; ++i) {
                    char c = dataArray[i];

                    if (c == '_') {
                        buffer.append("=20");
                    } else {
                        buffer.append(c);
                    }
                }

                data = buffer.toString();
            }
        }

        // decode word
        if (codec != null) {
            try {
                decoded = codec.decode(data.toCharArray());
            } catch (Exception e) {
                throw new UnsupportedEncodingException();
            }
        }

        return new String(decoded, Charset.forName(characterSet));
    }

    private RFC2047Encoder() {
        //empty
    }
}
