/*
 * @(#)YEnc.java
 *
 * Copyright (c) 2003 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.StringTokenizer;
import java.util.Vector;
import java.util.zip.CRC32;

/**
 * Implements methods for encoding and decoding data using the YEnc specification.
 */
public class YEnc
        implements Codec {

    // the header for this YEncoded message
    private YEncHeader header;

    // the trailer for this YEncoded message
    private YEncTrailer trailer;

    // the part representing this YEncoded message
    private YEncPart part;

    // the maximium line length
    private int maxLineLength = YEncUtil.LINE_SIZE;


    /**
     * Creates a new YEnc instance.
     */
    public YEnc() {
    }

    /**
     * Creates a new YEnc instance.
     *
     * @param maxLineLength the maximum line length to use when encoding data, default is 128.
     */
    public YEnc(int maxLineLength) {
        this.maxLineLength = maxLineLength;
    }

    /**
     * Decodes data encoded in YEnc format.
     * Each part in a multi-part YEnc message must be decoded separately.
     *
     * @param values the encoded data to decode
     *
     * @return the decoded data
     *
     * @throws Exception if an error occurs while decoding data
     */
    public byte[] decode(char[] values)
            throws Exception {
        // reinitialize header, trailer and part data
        header = null;
        trailer = null;
        part = null;

        byte[] b = new byte[values.length];
        byte[] decodedValues = new byte[0];
        int written = 0;
        int index = 0;
        CRC32 crc = new CRC32();
        CRC32 pcrc = new CRC32();
        Vector lines = this.charArrayToVector(values);
        header = null;
        trailer = null;
        part = null;
        for (int i = 0; i < lines.size(); i++) {
            char[] line = (char[]) lines.get(i);

            // decodes header portion of YEnc encoded message
            if (YEncUtil.isHeader(line)) {
                header = new YEncHeader();
                header.parseHeader(line);
                decodedValues = new byte[(int) header.getSize()];
                continue;
            }

            // decodes trailer portion of YEnc encoded message
            if (YEncUtil.isTrailer(line)) {
                trailer = new YEncTrailer();
                trailer.parseTrailer(line);
                continue;
            }

            // decodes part line of YEnc encoded message
            if (YEncUtil.isPart(line)) {
                part = new YEncPart();
                part.parsePart(line);
                long actualSize = part.getEnd() - part.getBegin();
                decodedValues = new byte[(int) actualSize + 1];
                continue;
            }

            if (header != null && trailer == null) {
                for (int j = 0; j < line.length; j++) {
                    if (line[j] == '=') {
                        if (j == line.length - 1) {
                            throw new Exception("escape "
                                            + "character found but as last "
                                            + "character in a line");
                        }
                        line[++j] = (char) ((line[j] - 64) % 255);

                    }

                    // decode character
                    char c = YEncUtil.yDecode(line[j]);

                    // add decoded character to buffer
                    decodedValues[index++] = (byte) c;

                    // update CRC
                    crc.update(c);
                    pcrc.update(c);

                    // update characters written
                    written++;
                }
            }
        }

        // verify checksum values
        if (trailer != null) {

            // verify trailer CRC
            if (trailer.getCRC() != -1) {
                if (trailer.getCRC() != crc.getValue()) {
                    throw new Exception("CRC32 message attribute does not match expected value, message is corrupt.");
                }
            }

            // verify trailer PCRC
            if (trailer.getPCRC() != -1) {
                if (trailer.getPCRC() != pcrc.getValue()) {
                    throw new Exception("PCRC32 message attribute does not match expected value, message is corrupt.");
                }
            }
        }

        // check for missing header
        if (header == null) {
            throw new Exception("Invalid or missing YEnc header.");
        }

        // check for missing trailer
        if (trailer == null) {
            //throw new Exception("Invalid or missing YEnc trailer.");
        }

        return decodedValues;
    }

    /**
     * Encodes data in YEnc format.
     * The name attribute of YEnc header is set to "unknown".
     *
     * @param values the data to encode
     *
     * @return the data to encode
     */
    public char[] encode(byte[] values) {
        return encode(values, "unknown");
    }

    /**
     * Encodes data in YEnc format.
     *
     * @param values the data to encode
     * @param name   the value to be used in name attribute of YEnc header, typically the filename.
     *
     * @return the data to encode
     */
    public char[] encode(byte[] values, String name) {
        CRC32 crc, pcrc;
        crc = new CRC32();
        pcrc = new CRC32();
        StringBuffer sb = new StringBuffer();

        // initialize name attribute
        if (name == null || name.length() == 0) {
            name = "unknown";
        }

        sb.append(
                "=ybegin line="
                        + maxLineLength
                        + " size="
                        + values.length
                        + " name=" + name + "\r\n");
        char c;
        int linect = 0;
        for (int i = 0; i < values.length; i++) {
            crc.update(values[i]);
            c = YEncUtil.yEncode((char) values[i]);

            if (YEncUtil.yShouldEscape(c, linect, maxLineLength)) {
                sb.append("=");
                sb.append(YEncUtil.yEscape(c));
                linect++;
            } else {
                sb.append(c);
            }

            linect++;
            if (linect >= maxLineLength) {
                sb.append("\r\n");
                linect = 0;
            }
        }
        sb.append("\r\n");
        sb.append(
                "=yend size="
                        + values.length
                        + " crc32="
                        + Long.toHexString(crc.getValue()));
        char[] encodedValues = new char[sb.length()];
        sb.getChars(0, sb.length(), encodedValues, 0);

        return encodedValues;
    }

    /**
     * Converts array of characters to a Vector of lines.
     *
     * @param b the character array to convert
     *
     * @return a Vector of data
     */
    private Vector charArrayToVector(char[] b) {
        char[] line = new char[1024];
        Vector strings = new Vector();
        int lines = 0;
        int j = 0;
        for (int i = 0; i < b.length; i++) {
            if (b[i] == '\r') {
                continue;
            }
            if (b[i] == '\n') {
                char[] line2 = new char[j];
                for (int k = 0; k < j; k++) {
                    line2[k] = line[k];
                }
                lines++;

                strings.add(line2);
                j = 0;
                //line = new char[1024];

                continue;
            }
            if (j < 1024) {
                line[j++] = b[i];
            } else {

                strings.add(line);
                line = new char[1024];
                j = 0;
            }

        }
        return strings;
    }

    /**
     * Gets maximum line length used when encoding data.
     * Default line length is 128.
     *
     * @return the maximum line length
     */
    public int getMaxLineLength() {
        return maxLineLength;
    }

    /**
     * Sets maximum line length used when encoding data.
     * Default line length is 128.
     *
     * @param l the maximum line length;
     */
    public void setMaxLineLength(int l) {
        maxLineLength = l;
    }

    /**
     * Gets begin attribute of
     *
     * @return begin long value
     */
    public long getBegin() {
        return part.getBegin();
    }

    /**
     * @return end long value
     */
    public long getEnd() {
        return part.getEnd();
    }

    /**
     * @return line length
     */
    public long getLineLength() {
        return header.getLines();
    }

    /**
     * @return filename
     */
    public String getFileName() {
        return header.getFileName();
    }

    /**
     * @return size long value
     */
    public long getSize() {
        return header.getSize();
    }

}

/**
 * YEncHeader encapsulates yEnc header.
 */
class YEncHeader {
    /**
     * file size (element size= in yEnc header)
     */
    private Long size;
    /**
     * number of lines in the archive (element line= in yEnc header)
     */
    private Long lines;
    /**
     * total parts in the archive (element total= in yEnc header)
     */
    private Integer total;
    /**
     * part number in the archive (element part= in yEnc header)
     */
    private Integer part;
    /**
     * name of the file (element name= in yEnc header)
     */
    private String name;

    /**
     * Return the file name.
     */
    public String getFileName() {
        return name;
    }

    /**
     * Returns the part number
     */
    public int getPartNumber() {
        return part.intValue();
    }

    /**
     * Returns the total parts in the arcive.
     */
    public int getTotalParts() {
        return total.intValue();
    }

    /**
     * Returns line length.
     */
    public long getLines() {
        return lines.longValue();
    }

    /**
     * Returns file size.
     */
    public long getSize() {
        return size.longValue();
    }

    /**
     * This method parses the header and sets the correct values in the class variables.
     */
    public void parseHeader(char[] line)
            throws Exception {

        StringTokenizer st = new StringTokenizer(new String(line), " \t");
        while (st.hasMoreTokens()) {
            NVPair pair = YEncUtil.getProperty(st.nextToken());
            if (pair == null) {
                return;
            }

            if (pair.getName().equals("name")) {
                name = pair.getValue();
            }
            if (pair.getName().equals("size")) {
                size = YEncUtil.strToLong(pair.getValue(), pair.getName());
            }
            if (pair.getName().equals("line")) {
                lines = YEncUtil.strToLong(pair.getValue(), pair.getName());
            }
            if (pair.getName().equals("part")) {
                part = YEncUtil.strToInt(pair.getValue(), pair.getName());
            }
            if (pair.getName().equals("total")) {
                total = YEncUtil.strToInt(pair.getValue(), pair.getName());
            }

        }
    }

    public String toString() {
        return new String(
                "YEnc Header\r\nName="
                        + name
                        + "\r\nSize="
                        + size
                        + "\r\nTotal Lines="
                        + lines
                        + "\r\nTotal Parts="
                        + total
                        + "\r\nPart Number="
                        + part);
    }
}

/**
 * This class encapsulates the YEnc Trailer (=yend) .
 */
class YEncTrailer {
    // size attribute of trailer
    private Long size;

    // crc32 attribute of trailer
    private Long crc32;

    // pcrc32 attribute of trailer
    private Long pcrc32;

    /**
     * Gets size attribute of trailer
     *
     * @return the size value
     */
    public long getSize() {
        return size.longValue();
    }

    /**
     * Gets crc32 attribute of trailer.
     *
     * @return the crc32 value
     */
    public long getCRC() {
        if (crc32 != null) {
            return crc32.longValue();
        }
        return -1;
    }

    /**
     * Gets pcrc32 attribute of trailer
     *
     * @return the pcrc32 value
     */
    public long getPCRC() {
        if (pcrc32 != null) {
            return pcrc32.longValue();
        }
        return -1;
    }

    /**
     * Parses YEnc trailer
     *
     * @param line the trailer
     *
     * @throws Exception if an error occurs while parsing trailer
     */
    public void parseTrailer(char[] line)
            throws Exception {
        StringTokenizer st = new StringTokenizer(new String(line), " \t");
        while (st.hasMoreTokens()) {
            NVPair pair = YEncUtil.getProperty(st.nextToken());
            if (pair == null) {
                return;
            }

            if (pair.getName().equals("size")) {
                size = YEncUtil.strToLong(pair.getValue(), pair.getName());
            }
            if (pair.getName().equals("crc32")) {
                crc32 = YEncUtil.strToLong(pair.getValue(), 16, pair.getName());
            }
            if (pair.getName().equals("pcrc32")) {
                pcrc32 =
                        YEncUtil.strToLong(pair.getValue(), 16, pair.getName());
            }
        }
    }

    public String toString() {
        return new String(
                "YEnc Trailer\r\nSize="
                        + size
                        + "\r\nCRC32="
                        + crc32
                        + "\r\nPCRC32="
                        + pcrc32);
    }
}

/**
 * This class encapsulates the YEnc Trailer (=yend) .
 */
class YEncPart {

    // the starting index for part
    private Long begin;

    // the ending index for part
    private Long end;

    /**
     * Gets starting index for part.
     *
     * @return the starting index
     */
    public long getBegin() {
        return begin.longValue();
    }

    /**
     * Gets ending index for part.
     *
     * @return the ending index
     */
    public long getEnd() {
        return end.longValue();
    }

    /**
     * Parses YEnc part line obtaining begin and end elements
     *
     * @param line the line to parse
     *
     * @throws Exception if an error occurs parsing line
     */
    public void parsePart(char[] line)
            throws Exception {
        StringTokenizer st = new StringTokenizer(new String(line), " \t");
        while (st.hasMoreTokens()) {
            NVPair pair = YEncUtil.getProperty(st.nextToken());
            if (pair == null) {
                return;
            }

            // set start index
            if (pair.getName().equals("begin")) {
                begin = YEncUtil.strToLong(pair.getValue(), pair.getName());
            }

            // set end index
            if (pair.getName().equals("end")) {
                end = YEncUtil.strToLong(pair.getValue(), pair.getName());
            }
        }
    }


    public String toString() {
        return new String("YEnc Part\r\nBegin=" + begin + "\r\nEnd=" + end);
    }
}

/**
 * Class for representing a name/value pair
 */
class NVPair {

    // the name
    private String name;

    // the value
    private String value;

    /**
     * Creates a new NVPair instance.
     *
     * @param pname  the name
     * @param pvalue the value
     */
    NVPair(String pname, String pvalue) {
        name = pname;
        value = pvalue;
    }

    /**
     * Gets the name
     *
     * @return the name
     */
    public String getName() {
        return name;
    }

    /**
     * Get the value
     *
     * @return the value
     */
    public String getValue() {
        return value;
    }
}

/**
 * Utility class for performing common YEnc functions
 */
class YEncUtil {

    public static final int LINE_SIZE = 128;

    /**
     * Checks if character must be YEnc escaped.
     *
     * @param c the character to check
     *
     * @return true if must be escaped, false otherwise
     */
    public static boolean yMustEscape(char c) {
        return (c == '\0' || c == '\r' || c == '\n' || c == '=');
    }

    /**
     * Checks if character must be YEnc escaped when first character to encode.
     *
     * @param c the character to check
     *
     * @return true if must be escaped, false otherwise
     */
    public static boolean yEscapeIfFirst(char c) {
        return (c == '\t' || c == '.' || c == ' ');
    }

    /**
     * Checks if character must be YEnc escaped when first character to encode.
     *
     * @param c the character to check
     *
     * @return true if must be escaped, false otherwise
     */
    public static boolean yEscapeIfLast(char c) {
        return (c == '\t' || c == ' ');
    }

    /**
     * Checks if character should be escaped.
     *
     * @param c      the character to check
     * @param index  the starting index of data
     * @param length the length of the data to encode
     *
     * @return
     */
    public static boolean yShouldEscape(char c, int index, int length) {
        return YEncUtil.yMustEscape(c)
                || (index == 0 && YEncUtil.yEscapeIfFirst(c))
                || (index + 1 >= length && YEncUtil.yEscapeIfLast(c));
    }

    /**
     * Calls <code>strToLong()</code> with radix equal to 10.
     *
     * @see #strToLong(String, int, String)
     */
    public static Long strToLong(String s, String name)
            throws Exception {
        return YEncUtil.strToLong(s, 10, name);
    }

    /**
     * Converts the string <code>s</code> to a <code>Long</code> in the
     * specified radix. In case an error occurs, a
     * <code>Exception</code> will be thrown using <code>name</code>
     * as part of the exception message.
     */
    public static Long strToLong(String s, int radix, String name)
            throws Exception {
        try {
            return Long.valueOf(s, radix);
        } catch (NumberFormatException e) {
            throw new Exception("value="
                            + s.toLowerCase()
                            + " supplied "
                            + "for "
                            + name
                            + " is not a number");
        }
    }

    /**
     * Converts the string <code>s</code> to a <code>Integer</code> in the
     * specified radix. In case an error occurs, a
     * <code>Exception</code> will be thrown using <code>name</code>
     * as part of the exception message.
     */
    public static Integer strToInt(String s, String name)
            throws Exception {
        try {
            return Integer.valueOf(s);
        } catch (NumberFormatException e) {
            throw new Exception(
                    "value supplied " + "for " + name + " is not a number");
        }
    }

    /**
     * Checks if line is a YEnc element
     *
     * @param line the line to check
     *
     * @return true if an element, false otherwise
     */
    public static boolean isYEncElement(char[] line) {
        if (line[0] == '=' && line[1] == 'y') {
            return true;
        }
        return false;
    }

    /**
     * Checks if line is a YEnc header
     *
     * @param line the line to check
     *
     * @return true if header, false otherwise
     */
    public static boolean isHeader(char[] line) {
        StringTokenizer st = new StringTokenizer(new String(line), " \t");
        if (st.countTokens() < 2) {
            return false;
        }
        String header = st.nextToken();
        return header.equals("=ybegin");

    }

    /**
     * Checks if line is a YEnc trailer
     *
     * @param line the line to check
     *
     * @return true if trailer, false otherwise
     */
    public static boolean isTrailer(char[] line) {
        StringTokenizer st = new StringTokenizer(new String(line), " \t");
        if (st.countTokens() < 2) {
            return false;
        }
        String header = st.nextToken();

        return header.equals("=yend");
    }

    /**
     * Checks if line is a part
     *
     * @param line the line to check
     *
     * @return true if part, false otherwise
     */
    public static boolean isPart(char[] line) {
        StringTokenizer st = new StringTokenizer(new String(line), " \t");
        if (st.countTokens() < 2) {
            return false;
        }
        String header = st.nextToken();

        return header.equals("=ypart");

    }

    /**
     * Gets NVPair instance based on line provided with name=value format
     *
     * @param s the line to parse
     *
     * @return a NVPair instance
     */
    public static NVPair getProperty(String s) {
        int pos = s.indexOf("=");

        if (pos == -1) {
            return null;
        }
        String value = s.substring(pos + 1);
        String name = s.substring(0, pos);
        return new NVPair(name, value.trim());
    }

    /**
     * Encoded character using YEnc algorithm.
     *
     * @param c the character to encode.
     *
     * @return the encoded value
     */
    public static char yEncode(char c) {
        return (char) ((c + 42) & 255);
    }

    /**
     * Decodes character encoded with YEnc algorithm.
     *
     * @param c the character to decode.
     *
     * @return the decoded value
     */
    public static char yDecode(char c) {
        return (char) ((c - 42) & 255);
    }

    /**
     * Escapes special character for purposes of encoding
     *
     * @param c the character to encode
     *
     * @return the encoded character
     */
    public static final char yEscape(char c) {
        return (char) ((c + 64) & 255);
    }

    private YEncUtil() {
    }
}