package org.bouncycastle.util.io.pem;

import java.io.BufferedWriter;
import java.io.IOException;
import java.io.Writer;
import java.util.Iterator;

import org.bouncycastle.util.Strings;
import org.bouncycastle.util.encoders.Base64;

/**
 * A generic PEM writer, based on RFC 1421
 */
public class PemWriter
    extends BufferedWriter
{
    private static final int LINE_LENGTH = 64;

    private final int nlLength;
    private char[]  buf = new char[LINE_LENGTH];

    /**
     * Base constructor.
     *
     * @param out output stream to use.
     */
    public PemWriter(Writer out)
    {
        super(out);

        String nl = Strings.lineSeparator();
        if (nl != null)
        {
            nlLength = nl.length();
        }
        else
        {
            nlLength = 2;
        }
    }

    /**
     * Return the number of bytes or characters required to contain the
     * passed in object if it is PEM encoded.
     *
     * @param obj pem object to be output
     * @return an estimate of the number of bytes
     */
    public int getOutputSize(PemObject obj)
    {
        // BEGIN and END boundaries.
        int size = (2 * (obj.getType().length() + 10 + nlLength)) + 6 + 4;

        if (!obj.getHeaders().isEmpty())
        {
            for (Iterator it = obj.getHeaders().iterator(); it.hasNext();)
            {
                PemHeader hdr = (PemHeader)it.next();

                size += hdr.getName().length() + ": ".length() + hdr.getValue().length() + nlLength;
            }

            size += nlLength;
        }

        // base64 encoding
        int dataLen = ((obj.getContent().length + 2) / 3) * 4;
        
        size += dataLen + (((dataLen + LINE_LENGTH - 1) / LINE_LENGTH) * nlLength);

        return size;
    }
    
    public void writeObject(PemObjectGenerator objGen)
        throws IOException
    {
        PemObject obj = objGen.generate();

        writePreEncapsulationBoundary(obj.getType());

        if (!obj.getHeaders().isEmpty())
        {
            for (Iterator it = obj.getHeaders().iterator(); it.hasNext();)
            {
                PemHeader hdr = (PemHeader)it.next();

                // A CR or LF in a header name or value would inject extra header lines, or a blank
                // line would terminate the header block early -- a PEM header injection. Reject it.
                if (hasLineBreak(hdr.getName()) || hasLineBreak(hdr.getValue()))
                {
                    throw new IllegalArgumentException("PEM header must not contain CR/LF");
                }

                this.write(hdr.getName());
                this.write(": ");
                this.write(hdr.getValue());
                this.newLine();
            }

            this.newLine();
        }
        
        writeEncoded(obj.getContent());
        writePostEncapsulationBoundary(obj.getType());
    }

    private void writeEncoded(byte[] bytes)
        throws IOException
    {
        bytes = Base64.encode(bytes);

        for (int i = 0; i < bytes.length; i += buf.length)
        {
            int index = 0;

            while (index != buf.length)
            {
                if ((i + index) >= bytes.length)
                {
                    break;
                }
                buf[index] = (char)bytes[i + index];
                index++;
            }
            this.write(buf, 0, index);
            this.newLine();
        }
    }

    private void writePreEncapsulationBoundary(
        String type)
        throws IOException
    {
        this.write("-----BEGIN " + type + "-----");
        this.newLine();
    }

    private void writePostEncapsulationBoundary(
        String type)
        throws IOException
    {
        this.write("-----END " + type + "-----");
        this.newLine();
    }

    private static boolean hasLineBreak(String s)
    {
        return s != null && (s.indexOf('\r') >= 0 || s.indexOf('\n') >= 0);
    }
}
