/*
 * Copyright (c) 1999, 2013, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package davmail.ldap;

import java.nio.charset.StandardCharsets;

/**
 * A BER encoder.
 *
 * @author Jagane Sundar
 * @author Scott Seligman
 * @author Vincent Ryan
 */
public final class BerEncoder extends Ber {

    private int curSeqIndex;
    private int[] seqOffset;
    private static final int INITIAL_SEQUENCES = 16;
    private static final int DEFAULT_BUFSIZE = 1024;

    // When buf is full, expand its size by the following factor.
    private static final int BUF_GROWTH_FACTOR = 8;

    /**
     * Creates a BER buffer for encoding.
     */
    public BerEncoder() {
        this(DEFAULT_BUFSIZE);
    }

    /**
     * Creates a BER buffer of a specified size for encoding.
     * Specify the initial bufsize.  Buffer will be expanded as needed.
     * @param bufsize The number of bytes for the buffer.
     */
    public BerEncoder(int bufsize) {
        buf = new byte[bufsize];
        this.bufsize = bufsize;
        offset = 0;

        seqOffset = new int[INITIAL_SEQUENCES];
        curSeqIndex = 0;
    }

    /**
     * Resets encoder to state when newly constructed.  Zeros out
     * internal data structures.
     */
    public void reset() {
        while (offset > 0) {
            buf[--offset] = 0;
        }
        while (curSeqIndex > 0) {
            seqOffset[--curSeqIndex] = 0;
        }
    }

// ------------------ Accessor methods ------------

    /**
     * Gets the number of encoded bytes in this BER buffer.
     */
    public int getDataLen() {
        return offset;
    }

    /**
     * Gets the buffer that contains the BER encoding. Throws an
     * exception if unmatched beginSeq() and endSeq() pairs were
     * encountered. Not entire buffer contains encoded bytes.
     * Use getDataLen() to determine number of encoded bytes.
     * Use getBuffer(true) to get rid of excess bytes in array.
     * @throws IllegalStateException If buffer contains unbalanced sequence.
     */
    public byte[] getBuf() {
        if (curSeqIndex != 0) {
            throw new IllegalStateException("BER encode error: Unbalanced SEQUENCEs.");
        }
        return buf;     // shared buffer, be careful to use this method.
    }

    /**
     * Gets the buffer that contains the BER encoding, trimming unused bytes.
     *
     * @throws IllegalStateException If buffer contains unbalanced sequence.
     */
    @SuppressWarnings("unused")
    public byte[] getTrimmedBuf() {
        int len = getDataLen();
        byte[] trimBuf = new byte[len];

        System.arraycopy(getBuf(), 0, trimBuf, 0, len);
        return trimBuf;
    }

// -------------- encoding methods -------------

    /**
     * Begin encoding a sequence with a tag.
     */
    public void beginSeq(int tag) {

        // Double the size of the SEQUENCE array if it overflows
        if (curSeqIndex >= seqOffset.length) {
            int[] seqOffsetTmp = new int[seqOffset.length * 2];

            System.arraycopy(seqOffset, 0, seqOffsetTmp, 0, seqOffset.length);
            seqOffset = seqOffsetTmp;
        }

        encodeByte(tag);
        seqOffset[curSeqIndex] = offset;

        // Save space for sequence length.
        // %%% Currently we save enough space for sequences up to 64k.
        //     For larger sequences we'll need to shift the data to the right
        //     in endSeq().  If we could instead pad the length field with
        //     zeros, it would be a big win.
        ensureFreeBytes(3);
        offset += 3;

        curSeqIndex++;
    }

    /**
     * Terminate a BER sequence.
     */
    public void endSeq() throws EncodeException {
        curSeqIndex--;
        if (curSeqIndex < 0) {
            throw new IllegalStateException("BER encode error: Unbalanced SEQUENCEs.");
        }

        int start = seqOffset[curSeqIndex] + 3; // index beyond length field
        int len = offset - start;

        if (len <= 0x7f) {
            shiftSeqData(start, len, -2);
            buf[seqOffset[curSeqIndex]] = (byte) len;
        } else if (len <= 0xff) {
            shiftSeqData(start, len, -1);
            buf[seqOffset[curSeqIndex]] = (byte) 0x81;
            buf[seqOffset[curSeqIndex] + 1] = (byte) len;
        } else if (len <= 0xffff) {
            buf[seqOffset[curSeqIndex]] = (byte) 0x82;
            buf[seqOffset[curSeqIndex] + 1] = (byte) (len >> 8);
            buf[seqOffset[curSeqIndex] + 2] = (byte) len;
        } else if (len <= 0xffffff) {
            shiftSeqData(start, len, 1);
            buf[seqOffset[curSeqIndex]] = (byte) 0x83;
            buf[seqOffset[curSeqIndex] + 1] = (byte) (len >> 16);
            buf[seqOffset[curSeqIndex] + 2] = (byte) (len >> 8);
            buf[seqOffset[curSeqIndex] + 3] = (byte) len;
        } else {
            throw new EncodeException("SEQUENCE too long");
        }
    }

    /**
     * Shifts contents of buf in the range [start,start+len) a specified amount.
     * Positive shift value means shift to the right.
     */
    private void shiftSeqData(int start, int len, int shift) {
        if (shift > 0) {
            ensureFreeBytes(shift);
        }
        System.arraycopy(buf, start, buf, start + shift, len);
        offset += shift;
    }

    /**
     * Encode a single byte.
     */
    public void encodeByte(int b) {
        ensureFreeBytes(1);
        buf[offset++] = (byte) b;
    }

    /*
     * Encodes an int.
     *<blockquote><pre>
     * BER integer ::= 0x02 berlength byte {byte}*
     *</pre></blockquote>
     */
    public void encodeInt(int i) {
        encodeInt(i, 0x02);
    }

    /**
     * Encodes an int and a tag.
     *<blockquote><pre>
     * BER integer w tag ::= tag berlength byte {byte}*
     *</pre></blockquote>
     */
    public void encodeInt(int i, int tag) {
        int mask = 0xff800000;
        int intsize = 4;

        while( (((i & mask) == 0) || ((i & mask) == mask)) && (intsize > 1) ) {
            intsize--;
            i <<= 8;
        }

        encodeInt(i, tag, intsize);
    }

    //
    // encodes an int using numbytes for the actual encoding.
    //
    private void encodeInt(int i, int tag, int intsize) {

        //
        // integer ::= 0x02 asnlength byte {byte}*
        //

        if (intsize > 4) {
            throw new IllegalArgumentException("BER encode error: INTEGER too long.");
        }

        ensureFreeBytes(2 + intsize);

        buf[offset++] = (byte) tag;
        buf[offset++] = (byte) intsize;

        int mask = 0xff000000;

        while (intsize-- > 0) {
            buf[offset++] = (byte) ((i & mask) >> 24);
            i <<= 8;
        }
    }

    /**
     * Encodes a boolean.
     *<blockquote><pre>
     * BER boolean ::= 0x01 0x01 {0xff|0x00}
     *</pre></blockquote>
     */
    @SuppressWarnings("unused")
    public void encodeBoolean(boolean b) {
        encodeBoolean(b, ASN_BOOLEAN);
    }


    /**
     * Encodes a boolean and a tag
     *<blockquote><pre>
     * BER boolean w TAG ::= tag 0x01 {0xff|0x00}
     *</pre></blockquote>
     */
    public void encodeBoolean(boolean b, int tag) {
        ensureFreeBytes(3);

        buf[offset++] = (byte) tag;
        buf[offset++] = 0x01;
        buf[offset++] = b ? (byte) 0xff : (byte) 0x00;
    }

    /**
     * Encodes a string.
     *<blockquote><pre>
     * BER string ::= 0x04 strlen byte1 byte2...
     *</pre></blockquote>
     * The string is converted into bytes using UTF-8 or ISO-Latin-1.
     */
    public void encodeString(String str, boolean encodeUTF8)
            throws EncodeException {
        encodeString(str, ASN_OCTET_STR, encodeUTF8);
    }

    /**
     * Encodes a string and a tag.
     *<blockquote><pre>
     * BER string w TAG ::= tag strlen byte1 byte2...
     *</pre></blockquote>
     */
    public void encodeString(String str, int tag, boolean encodeUTF8)
            throws EncodeException {

        encodeByte(tag);

        int i = 0;
        int count;
        byte[] bytes = null;

        if (str == null) {
            count = 0;
        } else if (encodeUTF8) {
            bytes = str.getBytes(StandardCharsets.UTF_8);
            count = bytes.length;
        } else {
            bytes = str.getBytes(StandardCharsets.ISO_8859_1);
            count = bytes.length;
        }

        encodeLength(count);

        ensureFreeBytes(count);
        while (i < count) {
            buf[offset++] = bytes[i++];
        }
    }

    /**
     * Encodes a portion of an octet string and a tag.
     */
    public void encodeOctetString(byte[] tb, int tag, int tboffset, int length)
            throws EncodeException {

        encodeByte(tag);
        encodeLength(length);

        if (length > 0) {
            ensureFreeBytes(length);
            System.arraycopy(tb, tboffset, buf, offset, length);
            offset += length;
        }
    }

    /**
     * Encodes an octet string and a tag.
     */
    public void encodeOctetString(byte[] tb, int tag) throws EncodeException {
        encodeOctetString(tb, tag, 0, tb.length);
    }

    private void encodeLength(int len) throws EncodeException {
        ensureFreeBytes(4);     // worst case

        if (len < 128) {
            buf[offset++] = (byte) len;
        } else if (len <= 0xff) {
            buf[offset++] = (byte) 0x81;
            buf[offset++] = (byte) len;
        } else if (len <= 0xffff) {
            buf[offset++] = (byte) 0x82;
            buf[offset++] = (byte) (len >> 8);
            buf[offset++] = (byte) (len & 0xff);
        } else if (len <= 0xffffff) {
            buf[offset++] = (byte) 0x83;
            buf[offset++] = (byte) (len >> 16);
            buf[offset++] = (byte) (len >> 8);
            buf[offset++] = (byte) (len & 0xff);
        } else {
            throw new EncodeException("string too long");
        }
    }

    /**
     * Encodes an array of strings.
     */
    @SuppressWarnings("unused")
    public void encodeStringArray(String[] strs, boolean encodeUTF8)
            throws EncodeException {
        if (strs == null)
            return;
        for (String str : strs) {
            encodeString(str, encodeUTF8);
        }
    }

    /**
     * Ensures that there are at least "len" unused bytes in "buf".
     * When more space is needed "buf" is expanded by a factor of
     * BUF_GROWTH_FACTOR, then "len" bytes are added if "buf" still
     * isn't large enough.
     */
    private void ensureFreeBytes(int len) {
        if (bufsize - offset < len) {
            int newsize = bufsize * BUF_GROWTH_FACTOR;
            if (newsize - offset < len) {
                newsize += len;
            }
            byte[] newbuf = new byte[newsize];
            // Only copy bytes in the range [0, offset)
            System.arraycopy(buf, 0, newbuf, 0, offset);

            buf = newbuf;
            bufsize = newsize;
        }
    }
}
