/*
 * @(#)MimeHeader.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.email.EmailAddress;
import com.jscape.inet.email.EmailAddressList;
import com.jscape.util.ByteBuffer;
import com.jscape.util.ByteBufferTokenizer;

import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
import java.util.Enumeration;
import java.util.StringTokenizer;
import java.util.Vector;

import static com.jscape.inet.mime.MimeConstants.COLON;
import static com.jscape.inet.mime.MimeConstants.SEMI_COLON;


/**
 * Implements methods for managing mime headers for use in a
 * <code>MimeMessage</code>.
 */
@SuppressWarnings("unchecked")
public class MimeHeader
        implements Serializable {
    /**
     * Represents the delimiter between a header name and header value.
     */
    private static final byte[] PARTS_DELIM = { SEMI_COLON };

    /**
     * Represents MIME header name.
     */
    protected String name;

    /**
     * Represents MIME header value.
     */
    protected String value;
    private Vector attrs = null;

    /**
     * Represents if folding should be on.
     */
    private boolean folding = true;

    /**
     * Represents if header has been folded.
     */
    private boolean folded = false;

    /**
     * Constructs a new <code>MimeHeader</code> instance.
     *
     * @throws MimeException if a MIME related parsing error occurs
     */
    public MimeHeader()
            throws MimeException {
        this("", "", "us-ascii");
    }

    /**
     * Constructs a new <code>MimeHeader</code> instance.
     *
     * @param name  the name of header
     * @param value the value of header
     *
     * @throws MimeException if a MIME related parsing error occurs
     */
    public MimeHeader(String name, String value)
            throws MimeException {
        this(name, value, "us-ascii");
    }

    /**
     * Constructs a new <code>MimeHeader</code> instance.
     *
     * @param name              the name of header
     * @param value             the value of header
     * @param characterEncoding the character encoding to encode value as
     *
     * @throws MimeException thrown if character encoding can not be completed
     *                       or header does not allow character encoding
     */
    public MimeHeader(String name, String value, String characterEncoding)
            throws MimeException {
        this.name = name;
        this.value = value;
        this.attrs = new Vector();

        try {
            this.value = encodeHeaderValue(name, value, characterEncoding);
        } catch (UnsupportedEncodingException ue) {
            throw new MimeException("Unable to encode value using " + characterEncoding);
        }
    }

    /**
     * Constructs a new <code>MimeHeader</code> instance. The MIME header data
     * is parsed.
     *
     * @param data the MIME header data
     *
     * @throws MimeException if a MIME related parsing error occurs
     */
    public MimeHeader(byte[] data)
            throws MimeException {
        this();
        parse(data);
    }

    /**
     * Encodes header according to RFC 2047.
     *
     * @param name              the header name
     * @param value             the header value
     * @param characterEncoding the character encoding to use
     *
     * @return an encoded header according to RFC 2047
     *
     * @throws UnsupportedEncodingException if character encoding is invalid
     */
    private String encodeHeaderValue(String name, String value,
                                     String characterEncoding)
            throws UnsupportedEncodingException {
        if (name.equalsIgnoreCase("Content-Type")
                || name.equalsIgnoreCase("Content-Disposition")
                || name.equalsIgnoreCase("Received")) {
            // not allowed to encode these headers
            return value;
        } else if (name.equalsIgnoreCase("To")
                || name.equalsIgnoreCase("From")
                || name.equalsIgnoreCase("Cc")
                || name.equalsIgnoreCase("Reply-To")) {
            EmailAddressList addrList = new EmailAddressList(value);
            Enumeration addr = addrList.getEmailAddresses();
            StringBuffer addresses = new StringBuffer();

            int count = 0;

            while (addr.hasMoreElements()) {
                count++;

                // special handling for email addresses
                EmailAddress email = (EmailAddress) addr.nextElement();
                String namePart = email.getName();
                String emailPart = email.getEmail();

                if (count != 1) {
                    addresses.append(", "); // removed CRLF
                }

                if ((namePart == null) || (namePart.trim().length() == 0)) {
                    addresses.append(emailPart);
                } else {
                    StringBuffer buffer = new StringBuffer();
                    namePart = namePart.trim();
                    emailPart = emailPart.trim();

                    if (namePart.startsWith("\"") && namePart.endsWith("\"")) {
                        buffer.append("\"");
                        buffer.append(characterSetEncode(namePart.substring(1,
                                                                            namePart.length() - 1), characterEncoding, 0));
                        buffer.append("\"");
                    } else {
                        buffer.append(characterSetEncode(namePart,
                                                         characterEncoding, 0));
                    }

                    buffer.append(" <" + emailPart + ">");
                    addresses.append(buffer.toString());
                }
            }

            return addresses.toString();
        } else {
            // encode value portion of header offset by 2 characters to include colon and space
            return characterSetEncode(value, characterEncoding,
                                      name.length() + 2);
        }
    }

    /**
     * Encodes a value according to RFC 2047.
     *
     * @param value             the value to encode
     * @param characterEncoding the character set to encode data in
     * @param offset            the number of characters to offset when calculating maximum line length
     *
     * @return the value encoded according to RFC 2047
     *
     * @throws UnsupportedEncodingException if the character encoding is invalid
     */
    private String characterSetEncode(String value, String characterEncoding,
                                      int offset)
            throws UnsupportedEncodingException {
        if (value == null) {
            value = "";
        }

        return RFC2047Encoder.encode(value, characterEncoding, offset);
    }

    /**
     * Parses information contained in MIME header.
     *
     * @param data the MIME header
     *
     * @throws MimeException if an error occurs while parsing
     */
    protected void parse(byte[] data)
            throws MimeException {
        ByteBuffer buffer = new ByteBuffer(data);
        parse(buffer);
    }

    /**
     * Parses information contained in MIME header.
     *
     * @param buffer the MIME header
     *
     * @throws MimeException if an error occurs while parsing
     */
    protected void parse(ByteBuffer buffer)
            throws MimeException {
        buffer = new ByteBuffer(unfold(buffer.toString()).getBytes(StandardCharsets.US_ASCII));

        try {
            int colon = buffer.indexOf(COLON);
            ByteBuffer namePart = buffer.getBuffer(0, colon);
            ByteBuffer valuePart = buffer.getBuffer(colon + 1);

            // strip off leading space
            if (valuePart.startsWith(" ", StandardCharsets.US_ASCII)) {
                valuePart = valuePart.getBuffer(1);
            }

            // set name and value of header
            this.name = namePart.toString(StandardCharsets.US_ASCII);

            // special handling for Content-Type header
            if (name.toLowerCase().startsWith("content-type") || name.toLowerCase().startsWith("content-disposition")) {
                ByteBufferTokenizer attrsTokenizer = new ByteBufferTokenizer(valuePart, PARTS_DELIM);

                // set value part of Content-Type header
                ByteBuffer tmp = attrsTokenizer.nextToken();
                this.value = tmp.toString(StandardCharsets.US_ASCII);

                // add any attributes to header
                while (attrsTokenizer.hasMoreElements()) {
                    tmp = attrsTokenizer.nextToken();

                    // check if attribute contains any data
                    if (tmp.toString().trim().length() == 0) {
                        break;
                    }

                    // parse each MIME header attribute
                    MimeHeaderAttr attr = new MimeHeaderAttr();
                    attr.parse(tmp);
                    addAttribute(attr);
                }
            } else {
                this.value = valuePart.toString(StandardCharsets.US_ASCII);
            }
        } catch (UnsupportedEncodingException ue) {
            throw new MimeException(
                    "unable to decode header with US-ASCII character set");
        } catch (Exception e) {
            throw new MimeException("error parsing MIME header " + e, e);
        }
    }

    /**
     * Gets name for this <code>MimeHeader</code>.
     *
     * @return name
     */
    public String getName() {
        return name;
    }

    /**
     * Gets value for this <code>MimeHeader</code>. Any encoded values in
     * header will be decoded according to RFC 2047. Strips quotes from value.
     *
     * @return value
     */
    public String getValue() {
        return MimeMessage.removeChar(getValueString(), '\"');
    }

    /**
     * Gets value for this <code>MimeHeader</code>. Leaves quotes in value. Any
     * encoded values in header will be decoded according to RFC 2047.
     *
     * @return value
     */
    public String getValueString() {
        if (!name.toLowerCase().equals("content-type") && !name.toLowerCase().equals("content-disposition")) {
            return !value.contains("=?") ? value : RFC2047Encoder.getDecodedValue(value);
        } else {
            return value;
        }
    }

    /**
     * Sets name of this <code>MimeHeader</code>.
     *
     * @param name the header name
     */
    public void setName(String name) {
        this.name = name;
    }

    /**
     * Sets the value of this <code>MimeHeader</code>.
     *
     * @param value the header value
     */
    public void setValue(String value) {
        this.value = value;
    }

    /**
     * Adds attribute to this <code>MimeHeader</code>.
     *
     * @param attr a MimeHeaderAttr
     *
     * @see MimeHeaderAttr
     */
    public void addAttribute(MimeHeaderAttr attr) {
        int index = -1;

        for (int i = 0; i < attrs.size(); i++) {
            MimeHeaderAttr att = (MimeHeaderAttr) attrs.elementAt(i);

            if (att.name.toLowerCase().equals(attr.name.toLowerCase())) {
                index = i;
            }
        }

        // replace or add new
        if (index != -1) {
            attrs.set(index, attr);
        } else {
            attrs.add(attr);
        }
    }

    /**
     * Adds attribute to this <code>MimeHeader</code>.
     *
     * @param name  the name of the MIME header attribute
     * @param value the value of the MIME header attribute
     *
     * @throws MimeException if a MIME related error occurs
     */
    public void addAttribute(String name, String value)
            throws MimeException {
        MimeHeaderAttr attr = new MimeHeaderAttr(name, value);
        addAttribute(attr);
    }

    /**
     * Removes specified index from attributes.
     *
     * @param index the index of attribute to remove
     */
    public void removeAttribute(int index) {
        this.attrs.removeElementAt(index);
    }

    /**
     * Gets attribute by name.
     *
     * @param name the name of attribute to retrieve
     *
     * @return a <code>MimeHeaderAttr</code>
     *
     * @see MimeHeaderAttr
     */
    public MimeHeaderAttr getAttribute(String name) {
        for (int i = 0; i < attrs.size(); i++) {
            MimeHeaderAttr att = (MimeHeaderAttr) attrs.elementAt(i);

            if (att.name.toLowerCase().equals(name.toLowerCase())) {
                return att;
            }
        }

        return null;
    }

    /**
     * Gets all attributes for this <code>MimeHeader</code>. Returns an
     * Enumeration of <code>MimeHeaderAttr</code>
     *
     * @return Enumeration
     *
     * @see MimeHeaderAttr
     */
    public Enumeration getAttributes() {
        return attrs.elements();
    }

    /**
     * Gets String representation of this <code>MimeHeader</code>.
     *
     * @return String
     */
    public String toString() {
        StringBuffer ret = new StringBuffer(name);
        ret.append(": ");
        ret.append(value);

        for (int i = 0; i < attrs.size(); i++) {
            ret.append("; ");

            MimeHeaderAttr mha = (MimeHeaderAttr) attrs.elementAt(i);
            ret.append(mha.toString());
        }

        return fold(ret.toString());
    }

    /**
     * Gets folded version of MimeHeader.  Folded headers have a maximum length of 72 character per line.
     *
     * @param header the header to fold
     *
     * @return a folded header
     */
    protected String fold(String header) {
        if (!folding) {
            return header;
        }

        int maxHeaderLen = 75;

        if ((header.length() > maxHeaderLen) && !folded) {
            StringBuffer buf = new StringBuffer();
            String right = header;
            String left = null;
            int count = 0;
            int index = 0;

            // split up header name and value
            index = right.indexOf(":");
            left = right.substring(0, ++index);
            right = right.substring(index);
            buf.append(left);

            outer:
            while (true) {
                // try to break up using spaces as delimiters
                int len = maxHeaderLen;
                index = right.lastIndexOf(" ", len);

                // no more spaces found try to force line breaks
                if (index <= 0) {
                    inner:
                    while (true) {
                        try {
                            // set length
                            if (count == 0) {
                                len = maxHeaderLen - left.length();
                            } else {
                                len = maxHeaderLen;
                            }

                            // get left portion
                            left = right.substring(0, len);

                            // increment index
                            //index+=len;

                            // get new right portion
                            right = right.substring(len);

                            // do not indent first iteration
                            if (count == 0) {
                                count++;
                                buf.append(left);
                            } else {
                                buf.append(" " + left);
                            }

                            // indent if necessary
                            if (right.length() > maxHeaderLen) {
                                buf.append("\r\n");

                                continue;
                            } else {
                                buf.append("\r\n " + right);

                                break inner;
                            }
                        } catch (IndexOutOfBoundsException ie) {
                            // catch error
                            buf.append(right + "\r\n");

                            break inner;
                        }
                    }

                    break outer;
                }

                // spaces found try to determine line breaks
                left = right.substring(0, index);
                right = right.substring(index);

                // do not indent first iteration
                if (count == 0) {
                    count++;
                    buf.append(left);
                } else {
                    buf.append(" " + left);
                }

                if (right.length() > maxHeaderLen) {
                    buf.append("\r\n");

                    continue;
                } else {
                    buf.append("\r\n " + right);

                    break;
                }
            }

            folded = true;

            return buf.toString();
        } else {
            return header;
        }
    }

    /**
     * Gets unfolded version of MimeHeader.  Folded headers have a maximum length of 72 character per line.
     *
     * @param header the header to unfold
     *
     * @return an unfolded header
     */
    protected String unfold(String header) {
        StringBuffer buffer = new StringBuffer();
        StringTokenizer st = new StringTokenizer(header, "\r\n");
        boolean first = true;

        while (st.hasMoreElements()) {
            String tmp = (String) st.nextElement();
            tmp = tmp.trim();

            if (first) {
                buffer.append(tmp);
            } else {
                buffer.append(" " + tmp);
            }

            first = false;
        }

        return buffer.toString();
    }

    /**
     * Checks if folding should be enabled for this <code>MimeHeader</code>.
     * Folding is enabled by default.  If folding is enabled then the maximum length of a header is 76 characters.
     * Headers which exceed 76 characters will be folded according to RFC 822.
     *
     * @return <code>true</code> if folding is enabled, <code>false</code> otherwise
     */
    public boolean isFolding() {
        return this.folding;
    }

    /**
     * Enables or disables folding for this <code>MimeHeader</code>.
     * Folding is enabled by default.  If folding is enabled then the maximum length of a header is 76 characters.
     * Headers which exceed 76 characters will be folded according to RFC 822.
     *
     * @param folding <code>true</code> to enable folding, <code>false</code> to disable
     */
    public void setFolding(boolean folding) {
        this.folding = folding;
    }

    /**
     * Checks if this header is valid.
     *
     * @return true if header is valid, false otherwise
     */
    public boolean isValid() {
        return (name != null) && (name.trim().length() > 0);
    }
}
