/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
 *
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common Development
 * and Distribution License("CDDL") (collectively, the "License").  You
 * may not use this file except in compliance with the License. You can obtain
 * a copy of the License at https://glassfish.dev.java.net/public/CDDL+GPL.html
 * or glassfish/bootstrap/legal/LICENSE.txt.  See the License for the specific
 * language governing permissions and limitations under the License.
 *
 * When distributing the software, include this License Header Notice in each
 * file and include the License file at glassfish/bootstrap/legal/LICENSE.txt.
 * Sun designates this particular file as subject to the "Classpath" exception
 * as provided by Sun in the GPL Version 2 section of the License file that
 * accompanied this code.  If applicable, add the following below the License
 * Header, with the fields enclosed by brackets [] replaced by your own
 * identifying information: "Portions Copyrighted [year]
 * [name of copyright owner]"
 *
 * Contributor(s):
 *
 * If you wish your version of this file to be governed by only the CDDL or
 * only the GPL Version 2, indicate your decision by adding "[Contributor]
 * elects to include this software in this distribution under the [CDDL or GPL
 * Version 2] license."  If you don't indicate a single choice of license, a
 * recipient has the option to distribute your version of this file under
 * either the CDDL, the GPL Version 2 or to extend the choice of license to
 * its licensees as provided above.  However, if you add GPL Version 2 code
 * and therefore, elected the GPL Version 2 license, then the option applies
 * only if the new code is made subject to such option by the copyright
 * holder.
 */

package com.sun.mail.iap;

import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.util.Vector;
import com.sun.mail.util.ASCIIUtility;

/**
 * @author  John Mani
 */

public class Argument {
    protected Vector items;

    /**
     * Constructor
     */
    public Argument() {
    items = new Vector(1);
    }

    /**
     * append the given Argument to this Argument. All items
     * from the source argument are copied into this destination
     * argument.
     */
    public void append(final Argument arg) {
    items.ensureCapacity(items.size() + arg.items.size());
    for (int i=0; i < arg.items.size(); i++) {
        items.addElement(arg.items.elementAt(i));
    }
    }

    /**
     * Write out given string as an ASTRING, depending on the type
     * of the characters inside the string. The string should
     * contain only ASCII characters. <p>
     *
     * XXX: Hmm .. this should really be called writeASCII()
     *
     * @param s  String to write out
     */
    public void writeString(final String s) {
    items.addElement(new AString(ASCIIUtility.getBytes(s)));
    }

    /**
     * Convert the given string into bytes in the specified
     * charset, and write the bytes out as an ASTRING
     */
    public void writeString(final String s, final String charset)
        throws UnsupportedEncodingException {
    if (charset == null) {
        writeString(s);
    } else {
        items.addElement(new AString(s.getBytes(charset)));
    }
    }

    /**
     * Write out given byte[] as a Literal.
     * @param b  byte[] to write out
     */
    public void writeBytes(final byte[] b)  {
    items.addElement(b);
    }

    /**
     * Write out given ByteArrayOutputStream as a Literal.
     * @param b  ByteArrayOutputStream to be written out.
     */
    public void writeBytes(final ByteArrayOutputStream b)  {
    items.addElement(b);
    }

    /**
     * Write out given data as a literal.
     * @param b  Literal representing data to be written out.
     */
    public void writeBytes(final Literal b)  {
    items.addElement(b);
    }

    /**
     * Write out given string as an Atom. Note that an Atom can contain only
     * certain US-ASCII characters.  No validation is done on the characters 
     * in the string.
     * @param s  String
     */
    public void writeAtom(final String s) {
    items.addElement(new Atom(s));
    }

    /**
     * Write out number.
     * @param i number
     */
    public void writeNumber(final int i) {
    items.addElement(new Integer(i));
    }

    /**
     * Write out number.
     * @param i number
     */
    public void writeNumber(final long i) {
    items.addElement(new Long(i));
    }

    /**
     * Write out as parenthesised list.
     * @param s statement
     */
    public void writeArgument(final Argument c) {
    items.addElement(c);
    }

    /*
     * Write out all the buffered items into the output stream.
     */
    public void write(final Protocol protocol) 
        throws IOException, ProtocolException {
    final int size = items != null ? items.size() : 0;
    final DataOutputStream os = (DataOutputStream)protocol.getOutputStream();

    for (int i=0; i < size; i++) {
        if (i > 0) {
            os.write(' ');
        }

        final Object o = items.elementAt(i);
        if (o instanceof Atom) {
        os.writeBytes(((Atom)o).string);
        } else if (o instanceof Number) {
        os.writeBytes(((Number)o).toString());
        } else if (o instanceof AString) {
        astring(((AString)o).bytes, protocol);
        } else if (o instanceof byte[]) {
        literal((byte[])o, protocol);
        } else if (o instanceof ByteArrayOutputStream) {
        literal((ByteArrayOutputStream)o, protocol);
        } else if (o instanceof Literal) {
        literal((Literal)o, protocol);
        } else if (o instanceof Argument) {
        os.write('('); // open parans
        ((Argument)o).write(protocol);
        os.write(')'); // close parans
        }
    }
    }

    private static final char[] UPPER_NIL = { 'N', 'I', 'L' };

    private static final char[] LOWER_NIL = { 'n', 'i', 'l' };

    private static boolean isNIL(final byte[] bytes) {
        final int len = bytes.length;
        if (len != UPPER_NIL.length) {
            return false;
        }
        boolean matches = true;
        for (int i = 0; matches && i < len; i++) {
            final byte b = bytes[i];
            matches &= (b == ((byte) UPPER_NIL[i]) || b == ((byte) LOWER_NIL[i]));
        }
        return matches;
    }

    /**
     * Write out given String as either an Atom, QuotedString or Literal
     */
    private void astring(final byte[] bytes, final Protocol protocol) 
            throws IOException, ProtocolException {
    final DataOutputStream os = (DataOutputStream)protocol.getOutputStream();
    final int len = bytes.length;

    // If length is greater than 1024 bytes, send as literal
    if (len > 1024) {
        literal(bytes, protocol);
        return;
    }

        // if 0 length, send as quoted-string
        boolean quote = len == 0 ? true: false;
    boolean escape = false;

    if (isNIL(bytes)) {
        quote = true;
    }

    byte b;
    for (int i = 0; i < len; i++) {
        b = bytes[i];
        if (b == '\0' || b == '\r' || b == '\n' || ((b & 0xff) > 0177)) {
        // NUL, CR or LF means the bytes need to be sent as literals
        literal(bytes, protocol);
        return;
        }
        if (b == '*' || b == '%' || b == '(' || b == ')' || b == '{' ||
        b == '"' || b == '\\' || ((b & 0xff) <= ' ')) {
        quote = true;
        if (b == '"' || b == '\\') {
            escape = true;
        }
        }
    }

    if (quote) {
        os.write('"');
    }

        if (escape) {
            // already quoted
            for (int i = 0; i < len; i++) {
                b = bytes[i];
                if (b == '"' || b == '\\') {
                    os.write('\\');
                }
                os.write(b);
            }
        } else {
            os.write(bytes);
        }
 

    if (quote) {
        os.write('"');
    }
    }

    /**
     * Write out given byte[] as a literal
     */
    private void literal(final byte[] b, final Protocol protocol) 
            throws IOException, ProtocolException {
    startLiteral(protocol, b.length).write(b);
    }

    /**
     * Write out given ByteArrayOutputStream as a literal.
     */
    private void literal(final ByteArrayOutputStream b, final Protocol protocol) 
            throws IOException, ProtocolException {
    b.writeTo(startLiteral(protocol, b.size()));
    }

    /**
     * Write out given Literal as a literal.
     */
    private void literal(final Literal b, final Protocol protocol) 
            throws IOException, ProtocolException {
    b.writeTo(startLiteral(protocol, b.size()));
    }

    private OutputStream startLiteral(final Protocol protocol, final int size) 
            throws IOException, ProtocolException {
    final DataOutputStream os = (DataOutputStream)protocol.getOutputStream();
    final boolean nonSync = protocol.supportsNonSyncLiterals();

    os.write('{');
    os.writeBytes(Integer.toString(size));
    if (nonSync) {
        os.writeBytes("+}\r\n");
    } else {
        os.writeBytes("}\r\n");
    }
    os.flush();

    // If we are using synchronized literals, wait for the server's
    // continuation signal
    if (!nonSync) {
        for (; ;) {
        final Response r = protocol.readResponse();
        if (r.isContinuation()) {
            break;
        }
        if (r.isTagged())
         {
            throw new LiteralException(r);
        // XXX - throw away untagged responses;
        //   violates IMAP spec, hope no servers do this
        }
        }
    }
    return os;
    }
}

class Atom {
    String string;

    Atom(final String s) {
    string = s;
    }
}

class AString {
    byte[] bytes;

    AString(final byte[] b) {
    bytes = b;
    }
}
