/****************************************************************
 * Licensed to the Apache Software Foundation (ASF) under one   *
 * or more contributor license agreements.  See the NOTICE file *
 * distributed with this work for additional information        *
 * regarding copyright ownership.  The ASF licenses this file   *
 * to you under the Apache License, Version 2.0 (the            *
 * "License"); you may not use this file except in compliance   *
 * with the License.  You may obtain a copy of the License at   *
 *                                                              *
 *   http://www.apache.org/licenses/LICENSE-2.0                 *
 *                                                              *
 * Unless required by applicable law or agreed to in writing,   *
 * software distributed under the License is distributed on an  *
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
 * KIND, either express or implied.  See the License for the    *
 * specific language governing permissions and limitations      *
 * under the License.                                           *
 ****************************************************************/

package org.apache.james.mime4j.codec;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

final class QuotedPrintableEncoder {
    private static final byte TAB = 0x09;
    private static final byte SPACE = 0x20;
    private static final byte EQUALS = 0x3D;
    private static final byte CR = 0x0D;
    private static final byte LF = 0x0A;
    private static final byte QUOTED_PRINTABLE_LAST_PLAIN = 0x7E;
    private static final int QUOTED_PRINTABLE_MAX_LINE_LENGTH = 76;
    private static final int QUOTED_PRINTABLE_OCTETS_PER_ESCAPE = 3;
    private static final byte[] HEX_DIGITS = {'0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'};

    private final byte[] inBuffer;
    private final byte[] outBuffer;
    private final boolean binary;
    
    private boolean pendingSpace;
    private boolean pendingTab;
    private boolean pendingCR;
    private int nextSoftBreak;
    private int outputIndex;
    private OutputStream out;
    
    
    public QuotedPrintableEncoder(int bufferSize, boolean binary) {
        inBuffer = new byte[bufferSize];
        outBuffer = new byte[3*bufferSize];
        outputIndex = 0;
        nextSoftBreak = QUOTED_PRINTABLE_MAX_LINE_LENGTH + 1;
        out = null;
        this.binary = binary;
        pendingSpace = false;
        pendingTab = false;
        pendingCR = false;
    }
    
    void initEncoding(final OutputStream out) {
        this.out = out;
        pendingSpace = false;
        pendingTab = false;
        pendingCR = false;
        nextSoftBreak = QUOTED_PRINTABLE_MAX_LINE_LENGTH + 1;
    }
    
    void encodeChunk(byte[] buffer, int off, int len) throws IOException {
        for (int inputIndex = off; inputIndex < len + off; inputIndex++) {
            encode(buffer[inputIndex]);
        }
    }
    
    void completeEncoding() throws IOException {
        writePending();
        flushOutput();
    }
    
    public void encode(final InputStream in, final OutputStream out) throws IOException {
        initEncoding(out);
        int inputLength;
        while((inputLength = in.read(inBuffer)) > -1) {
            encodeChunk(inBuffer, 0, inputLength);
        }
        completeEncoding();
    }
    
    private void writePending() throws IOException {
        if (pendingSpace) {
            plain(SPACE);
        } else if (pendingTab) {
            plain(TAB);
        } else if (pendingCR) {
            plain(CR);
        }
        clearPending();
    }
    
    private void clearPending() throws IOException {
        pendingSpace  = false;
        pendingTab = false;
        pendingCR = false;
    }
    
    private void encode(byte next) throws IOException {
        if (next == LF) {
            if (binary) {
                writePending();
                escape(next);
            } else {
                if (pendingCR) {
                    // Expect either space or tab pending 
                    // but not both
                    if (pendingSpace) {
                        escape(SPACE);
                    } else if (pendingTab) {
                        escape(TAB);
                    }
                    lineBreak();
                    clearPending();
                } else {
                    writePending();
                    plain(next);
                }
            }
        } else if (next == CR) {
            if (binary)  {
                escape(next);
            } else {
                pendingCR = true;
            }
        } else {
            writePending();
            if (next == SPACE) {
                if (binary)  {
                    escape(next);
                } else {
                    pendingSpace = true;
                }
            } else if (next == TAB) {
                if (binary)  {
                    escape(next);
                } else {
                    pendingTab = true;
                }
            } else if (next < SPACE) {
                escape(next);
            } else if (next > QUOTED_PRINTABLE_LAST_PLAIN) {
                escape(next);
            } else if (next == EQUALS) {
                escape(next);
            } else {
                plain(next);
            }
        }
    }
    
    private void plain(byte next) throws IOException {
        if (--nextSoftBreak <= 1) {
            softBreak();
        }
        write(next);
    }
    
    private void escape(byte next) throws IOException {
        if (--nextSoftBreak <= QUOTED_PRINTABLE_OCTETS_PER_ESCAPE) {
            softBreak();
        }
        
        int nextUnsigned = next & 0xff;
        
        write(EQUALS);
        --nextSoftBreak;
        write(HEX_DIGITS[nextUnsigned >> 4]);
        --nextSoftBreak;
        write(HEX_DIGITS[nextUnsigned % 0x10]);
    }
    
    private void write(byte next) throws IOException {
        outBuffer[outputIndex++] = next;
        if (outputIndex >= outBuffer.length) {
            flushOutput();
        }
    }
    
    private void softBreak() throws IOException {
        write(EQUALS);
        lineBreak();
    }

    private void lineBreak() throws IOException {
        write(CR);
        write(LF);
        nextSoftBreak = QUOTED_PRINTABLE_MAX_LINE_LENGTH;
    }
    
    void flushOutput() throws IOException {
        if (outputIndex < outBuffer.length) {
            out.write(outBuffer, 0, outputIndex);
        } else {
            out.write(outBuffer);
        }
        outputIndex = 0;
    }
}