package org.bouncycastle.openpgp;

import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Date;

import org.bouncycastle.bcpg.BCPGOutputStream;
import org.bouncycastle.bcpg.PacketTags;
import org.bouncycastle.util.Strings;

/**
 * Generator for producing literal data packets.
 * <p>
 * A PGPLiteralData is used by invoking one of the open functions to create an OutputStream that raw
 * data can be supplied to for encoding:
 * <ul>
 * <li>If the length of the data to be written is known in advance, use
 * {@link #open(OutputStream, char, String, long, Date)} to create a packet containing a single
 * literal data object.</li>
 * <li>If the length of the data is unknown, use
 * {@link #open(OutputStream, char, String, Date, byte[])} to create a packet consisting of a series
 * of literal data objects (partials).</li>
 * </ul>
 * </p><p>
 * A PGPLiteralDataGenerator is usually used to wrap the OutputStream
 * {@link PGPEncryptedDataGenerator#open(OutputStream, byte[]) obtained} from a
 * {@link PGPEncryptedDataGenerator} or a {@link PGPCompressedDataGenerator}.
 * </p><p>
 * Once literal data has been written to the constructed OutputStream, writing of the object stream
 * is completed by closing the OutputStream obtained from the <code>open()</code> method, or
 * equivalently invoking {@link #close()} on this generator.
 * </p>
 */
public class PGPLiteralDataGenerator implements StreamGenerator
{
    /** Format tag for binary literal data */
    public static final char BINARY = PGPLiteralData.BINARY;
    /** Format tag for textual literal data */
    public static final char    TEXT = PGPLiteralData.TEXT;
    /** Format tag for UTF-8 encoded textual literal data */
    public static final char    UTF8 = PGPLiteralData.UTF8;

    /**
     * The special name indicating a "for your eyes only" packet.
     */
    // TODO: Not used?
    public static final String  CONSOLE = PGPLiteralData.CONSOLE;

    /**
     * The special time for a modification time of "now" or
     * the present time.
     */
    public static final Date    NOW = PGPLiteralData.NOW;

    private BCPGOutputStream    pkOut;
    private boolean             oldFormat = false;

    /**
     * Constructs a generator for literal data objects.
     */
    public PGPLiteralDataGenerator()
    {
    }

    /**
     * Constructs a generator for literal data objects, specifying to use new or old (PGP 2.6.x
     * compatible) format.
     * <p>
     * This can be used for compatibility with PGP 2.6.x.
     * </p>
     * @param oldFormat <code>true</code> to use PGP 2.6.x compatible format.
     */
    public PGPLiteralDataGenerator(
        boolean    oldFormat)
    {
        this.oldFormat = oldFormat;
    }

    private void writeHeader(
        OutputStream    out,
        char            format,
        byte[]          encName,
        long            modificationTime)
        throws IOException
    {
        out.write(format);

        out.write((byte)encName.length);

        for (int i = 0; i != encName.length; i++)
        {
            out.write(encName[i]);
        }

        long    modDate = modificationTime / 1000;

        out.write((byte)(modDate >> 24));
        out.write((byte)(modDate >> 16));
        out.write((byte)(modDate >> 8));
        out.write((byte)(modDate));
    }

    /**
     * Open a literal data packet, returning a stream to store the data inside the packet.
     * <p>
     * The stream created can be closed off by either calling close() on the stream or close() on
     * the generator. Closing the returned stream does not close off the OutputStream parameter out.
     *
     * @param out the underlying output stream to write the literal data packet to.
     * @param format the format of the literal data that will be written to the output stream (one
     *            of {@link #BINARY}, {@link #TEXT} or {@link #UTF8}).
     * @param name the name of the "file" to encode in the literal data object.
     * @param length the length of the data that will be written.
     * @param modificationTime the time of last modification we want stored.
     */
    public OutputStream open(
        OutputStream    out,
        char            format,
        String          name,
        long            length,
        Date            modificationTime)
        throws IOException
    {
        if (pkOut != null)
        {
            throw new IllegalStateException("generator already in open state");
        }

        byte[] encName = Strings.toUTF8ByteArray(name);

        pkOut = new BCPGOutputStream(out, PacketTags.LITERAL_DATA, length + 2 + encName.length + 4, oldFormat);

        writeHeader(pkOut, format, encName, modificationTime.getTime());

        return new WrappedGeneratorStream(pkOut, this);
    }

    /**
     * Open a literal data packet, returning a stream to store the data inside the packet as an
     * indefinite-length stream. The stream is written out as a series of partial packets with a
     * chunk size determined by the size of the passed in buffer.
     * <p>
     * The stream created can be closed off by either calling close() on the stream or close() on
     * the generator. Closing the returned stream does not close off the OutputStream parameter out.
     *
     * <p>
     * <b>Note</b>: if the buffer is not a power of 2 in length only the largest power of 2 bytes
     * worth of the buffer will be used.
     *
     * @param out the underlying output stream to write the literal data packet to.
     * @param format the format of the literal data that will be written to the output stream (one
     *            of {@link #BINARY}, {@link #TEXT} or {@link #UTF8}).
     * @param name the name of the "file" to encode in the literal data object.
     * @param modificationTime the time of last modification we want stored (will be stored to
     *            second level precision).
     * @param buffer a buffer to use to buffer and write partial packets. The returned stream takes
     *            ownership of the buffer.
     *
     * @return the output stream to write data to.
     * @throws IOException if an error occurs writing stream header information to the provider
     *             output stream.
     * @throws IllegalStateException if this generator already has an open OutputStream.
     */
    public OutputStream open(
        OutputStream    out,
        char            format,
        String          name,
        Date            modificationTime,
        byte[]          buffer)
        throws IOException
    {
        if (pkOut != null)
        {
            throw new IllegalStateException("generator already in open state");
        }

        pkOut = new BCPGOutputStream(out, PacketTags.LITERAL_DATA, buffer);

        byte[] encName = Strings.toUTF8ByteArray(name);

        writeHeader(pkOut, format, encName, modificationTime.getTime());

        return new WrappedGeneratorStream(pkOut, this);
    }

    /**
     * Open a literal data packet for the passed in File object, returning an output stream for
     * saving the file contents.
     * <p>
     * This method configures the generator to store the file contents in a single literal data
     * packet, taking the filename and modification time from the file, but does not store the
     * actual file data.
     * </p><p>
     * The stream created can be closed off by either calling close() on the stream or close() on
     * the generator. Closing the returned stream does not close off the OutputStream parameter out.
     * </p>
     * @param out the underlying output stream to write the literal data packet to.
     * @param format the format of the literal data that will be written to the output stream (one
     *            of {@link #BINARY}, {@link #TEXT} or {@link #UTF8}).
     * @param file the file to determine the length and filename from.
     * @return the output stream to write data to.
     * @throws IOException if an error occurs writing stream header information to the provider
     *             output stream.
     * @throws IllegalStateException if this generator already has an open OutputStream.
     */
    public OutputStream open(
        OutputStream    out,
        char            format,
        File            file)
        throws IOException
    {
        return open(out, format, file.getName(), file.length(), new Date(file.lastModified()));
    }

    /**
     * Close the literal data packet - this is equivalent to calling close on the stream
     * returned by the open() method.
     *
     * @throws IOException
     */
    public void close()
        throws IOException
    {
        if (pkOut != null)
        {
            pkOut.finish();
            pkOut.flush();
            pkOut = null;
        }
    }
}
