/* Copyright (c) 2001-2024, The HSQL Development Group
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, this
 * list of conditions and the following disclaimer.
 *
 * Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation
 * and/or other materials provided with the distribution.
 *
 * Neither the name of the HSQL Development Group nor the names of its
 * contributors may be used to endorse or promote products derived from this
 * software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL HSQL DEVELOPMENT GROUP, HSQLDB.ORG,
 * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */


package org.hsqldb.server;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;

import org.hsqldb.HsqlException;
import org.hsqldb.types.BinaryData;

/**
 * An atomic transfer packet received from a HyperSQL client.
 *
 * Since we read and cache all data for the packet upon instantiation, the
 * available method is reliable and may be relied upon.
 *
 * @see #available()
 *
 * @author Blaine Simpson (blaine dot simpson at admc dot com)
 * @since 1.9.0
 */
class OdbcPacketInputStream extends DataInputStream {

    char                packetType;
    private InputStream bufferStream;

    /**
     * Instantiate a packet of the specified type and size.
     */
    static OdbcPacketInputStream newOdbcPacketInputStream(char cType,
            InputStream streamSource, int sizeInt) throws IOException {
        return newOdbcPacketInputStream(cType, streamSource,
                                        Integer.valueOf(sizeInt));
    }

    /**
     * Instantiate a packet of the specified type, with size determined by
     * the first int read from the given stream.
     */
    static OdbcPacketInputStream newOdbcPacketInputStream(char cType,
            InputStream streamSource) throws IOException {
        return newOdbcPacketInputStream(cType, streamSource, null);
    }

    static private OdbcPacketInputStream newOdbcPacketInputStream(char cType,
            InputStream streamSource,
            Integer packetSizeObj) throws IOException {

        int bytesRead, i;
        int packetSize = 0;

        if (packetSizeObj == null) {
            byte[] fourBytes = new byte[4];

            bytesRead = 0;

            while ((i =
                    streamSource.read(fourBytes, bytesRead, fourBytes.length
                                      - bytesRead)) > 0) {
                bytesRead += i;
            }

            if (bytesRead != fourBytes.length) {
                throw new EOFException("Failed to read size header int");
            }

            packetSize = ((fourBytes[0] & 0xff) << 24)
                         + ((fourBytes[1] & 0xff) << 16)
                         + ((fourBytes[2] & 0xff) << 8)
                         + (fourBytes[3] & 0xff) - 4;

            // Minus 4 because this counts the size int itself.
        } else {
            packetSize = packetSizeObj.intValue();
        }

        byte[] xferBuffer = new byte[packetSize];

        bytesRead = 0;

        while ((i = streamSource.read(xferBuffer, bytesRead, xferBuffer.length
                                      - bytesRead)) > 0) {
            bytesRead += i;
        }

        if (bytesRead != xferBuffer.length) {
            throw new EOFException(
                "Failed to read packet contents from given stream");
        }

        return new OdbcPacketInputStream(cType,
                                         new ByteArrayInputStream(xferBuffer));
    }

    private OdbcPacketInputStream(char packetType, InputStream bufferStream) {

        super(bufferStream);

        this.packetType = packetType;
    }

    /**
     * Generate a String/String Map from null-terminated String pairs, until
     * a '\0' character is read in place of the first key character.
     *
     * @return the generated Map
     * @throws EOFException if the rest of packet does not contained the
     *                   required, well-formed null-terminated string pairs.
     */
    Map<String, String> readStringPairs() throws IOException {

        String key;
        HashMap<String, String> map = new HashMap<>();

        while (true) {
            key = readString();

            if (key.length() < 1) {
                break;
            }

            map.put(key, readString());
        }

        return map;
    }

    /**
     * Reads a NULL-TERMINATED String.
     *
     * @throws IOException if attempt to read past end of packet.
     */
    String readString() throws IOException {

        /* Would be MUCH easier to do this with Java6's String
         * encoding/decoding operations */
        ByteArrayOutputStream baos = new ByteArrayOutputStream();

        baos.write((byte) 'X');
        baos.write((byte) 'X');

        // Place-holders to be replaced with short length
        int i;

        while ((i = readByte()) > 0) {
            baos.write((byte) i);
        }

        byte[] ba = baos.toByteArray();

        baos.close();

        int len = ba.length - 2;

        ba[0] = (byte) (len >>> 8);
        ba[1] = (byte) len;

        DataInputStream dis =
            new DataInputStream(new ByteArrayInputStream(ba));
        String s = dis.readUTF();

        dis.close();

        return s;
    }

    BinaryData readSizedBinaryData() throws IOException {

        int len = readInt();

        try {
            return (len < 0) ? null
                             : new BinaryData(len, this);
        } catch (HsqlException he) {
            throw new IOException(he.getMessage());
        }
    }

    String readSizedString() throws IOException {

        int len = readInt();

        return (len < 0) ? null
                         : readString(len);
    }

    /**
     * These Strings are not null-terminated.
     *
     * @param len Bytes to read (not necessarily characters to be returned!
     * @throws IOException if attempt to read past end of packet.
     */
    String readString(int len) throws IOException {

        /* Would be MUCH easier to do this with Java6's String
         * encoding/decoding operations */
        int    bytesRead = 0;
        int    i;
        byte[] ba = new byte[len + 2];

        ba[0] = (byte) (len >>> 8);
        ba[1] = (byte) len;

        while ((i = read(ba, 2 + bytesRead, len - bytesRead)) > -1
                && bytesRead < len) {
            bytesRead += i;
        }

        if (bytesRead != len) {
            throw new EOFException("Packet ran dry");
        }

        for (i = 2; i < ba.length - 1; i++) {
            if (ba[i] == 0) {
                throw new RuntimeException("Null internal to String at offset "
                                           + (i - 2));
            }
        }

        DataInputStream dis =
            new DataInputStream(new ByteArrayInputStream(ba));
        String s = dis.readUTF();

        //String s = DataInputStream.readUTF(dis);
        // TODO:  Test the previous two to see if one works better for
        // high-order characters.
        dis.close();

        return s;
    }

    public char readByteChar() throws IOException {
        return (char) readByte();
    }
}
