/*
 * Copyright (c) 2002, 2024, Oracle and/or its affiliates.
 *
 * This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License, version 2.0, as published by
 * the Free Software Foundation.
 *
 * This program is designed to work with certain software that is licensed under separate terms, as designated in a particular file or component or in
 * included license documentation. The authors of MySQL hereby grant you an additional permission to link the program and your derivative works with the
 * separately licensed software that they have either included with the program or referenced in the documentation.
 *
 * Without limiting anything contained in the foregoing, this file, which is part of MySQL Connector/J, is also subject to the Universal FOSS Exception,
 * version 1.0, a copy of which can be found at http://oss.oracle.com/licenses/universal-foss-exception.
 *
 * This program 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.0, for more details.
 *
 * You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
 */

package com.mysql.cj.protocol.a;

import java.util.HashMap;
import java.util.Map;

import com.mysql.cj.Constants;
import com.mysql.cj.Messages;
import com.mysql.cj.exceptions.ExceptionFactory;
import com.mysql.cj.exceptions.WrongArgumentException;
import com.mysql.cj.protocol.Message;
import com.mysql.cj.protocol.a.NativeConstants.IntegerDataType;
import com.mysql.cj.protocol.a.NativeConstants.StringLengthDataType;
import com.mysql.cj.protocol.a.NativeConstants.StringSelfDataType;
import com.mysql.cj.util.StringUtils;

/**
 * PacketPayload is the content of a full single packet (independent from
 * on-wire splitting) communicated with the server. We can manipulate the
 * packet's underlying buffer when sending commands with writeInteger(),
 * writeBytes(), etc. We can check the packet type with isEOFPacket(), etc
 * predicates.
 *
 * A position is maintained for reading/writing data. A payload length is
 * maintained allowing the PacketPayload to be decoupled from the size of
 * the underlying buffer.
 */
public class NativePacketPayload implements Message {

    static final int NO_LENGTH_LIMIT = -1;
    public static final long NULL_LENGTH = -1;

    /* Type ids of response packets. */
    public static final short TYPE_ID_ERROR = 0xFF;
    public static final short TYPE_ID_EOF = 0xFE;
    /** It has the same signature as EOF, but may be issued by server only during handshake phase **/
    public static final short TYPE_ID_AUTH_SWITCH = 0xFE;
    public static final short TYPE_ID_LOCAL_INFILE = 0xFB;
    public static final short TYPE_ID_OK = 0;
    public static final short TYPE_ID_AUTH_MORE_DATA = 0x01;
    public static final short TYPE_ID_AUTH_NEXT_FACTOR = 0x02;

    private int payloadLength = 0;

    private byte[] byteBuffer;

    private int position = 0;

    static final int MAX_BYTES_TO_DUMP = 1024;

    private Map<String, Integer> tags = new HashMap<>();

    @Override
    public String toString() {
        int numBytes = this.position <= this.payloadLength ? this.position : this.payloadLength;
        int numBytesToDump = numBytes < MAX_BYTES_TO_DUMP ? numBytes : MAX_BYTES_TO_DUMP;

        this.position = 0;
        String dumped = StringUtils.dumpAsHex(readBytes(StringLengthDataType.STRING_FIXED, numBytesToDump), numBytesToDump);

        if (numBytesToDump < numBytes) {
            return dumped + " ....(packet exceeds max. dump length)";
        }

        return dumped;
    }

    public String toSuperString() {
        return super.toString();
    }

    public NativePacketPayload(byte[] buf) {
        this.byteBuffer = buf;
        this.payloadLength = buf.length;
    }

    public NativePacketPayload(int size) {
        this.byteBuffer = new byte[size];
        this.payloadLength = size;
    }

    public int getCapacity() {
        return this.byteBuffer.length;
    }

    /**
     * Checks that underlying buffer has enough space to store additionalData bytes starting from current position.
     * If buffer size is smaller than required then it is re-allocated with bigger size.
     *
     * @param additionalData
     *            additional data size in bytes
     */
    public final void ensureCapacity(int additionalData) {
        if (this.position + additionalData > this.byteBuffer.length) {
            //
            // Resize, and pad so we can avoid allocing again in the near future
            //
            int newLength = (int) (this.byteBuffer.length * 1.25);

            if (newLength < this.byteBuffer.length + additionalData) {
                newLength = this.byteBuffer.length + (int) (additionalData * 1.25);
            }

            if (newLength < this.byteBuffer.length) {
                newLength = this.byteBuffer.length + additionalData;
            }

            byte[] newBytes = new byte[newLength];

            System.arraycopy(this.byteBuffer, 0, newBytes, 0, this.byteBuffer.length);
            this.byteBuffer = newBytes;
        }
    }

    @Override
    public byte[] getByteBuffer() {
        return this.byteBuffer;
    }

    /**
     * Sets the array of bytes to use as a buffer to read from.
     *
     * @param byteBufferToSet
     *            the array of bytes to use as a buffer
     */
    public void setByteBuffer(byte[] byteBufferToSet) {
        this.byteBuffer = byteBufferToSet;
    }

    /**
     * Get the actual length of payload the buffer contains.
     * It can be smaller than underlying buffer size because it can be reused after a big packet.
     *
     * @return payload length
     */
    public int getPayloadLength() {
        return this.payloadLength;
    }

    /**
     * Set the actual length of payload written to buffer.
     * It can be smaller or equal to underlying buffer size.
     *
     * @param bufLengthToSet
     *            length
     */
    public void setPayloadLength(int bufLengthToSet) {
        if (bufLengthToSet > this.byteBuffer.length) {
            throw ExceptionFactory.createException(WrongArgumentException.class, Messages.getString("Buffer.0"));
        }
        this.payloadLength = bufLengthToSet;
    }

    /**
     * To be called after write operations to ensure that payloadLength contains
     * the real size of written data.
     */
    private void adjustPayloadLength() {
        if (this.position > this.payloadLength) {
            this.payloadLength = this.position;
        }
    }

    @Override
    public int getPosition() {
        return this.position;
    }

    /**
     * Set the current position to write to/ read from
     *
     * @param positionToSet
     *            the position (0-based index)
     */
    public void setPosition(int positionToSet) {
        this.position = positionToSet;
    }

    /**
     * Is it a ERROR packet.
     *
     * @return true if it is a ERROR packet
     */
    public boolean isErrorPacket() {
        return (this.byteBuffer[0] & 0xff) == TYPE_ID_ERROR;
    }

    /**
     * Is it a EOF packet.
     * See https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_basic_eof_packet.html
     *
     * @return true if it is a EOF packet
     */
    public final boolean isEOFPacket() {
        return (this.byteBuffer[0] & 0xff) == TYPE_ID_EOF && this.payloadLength <= 5;
    }

    /**
     * Is it a Protocol::AuthSwitchRequest packet.
     * See https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_connection_phase_packets_protocol_auth_switch_request.html
     *
     * @return true if it is a Protocol::AuthSwitchRequest packet
     */
    public final boolean isAuthMethodSwitchRequestPacket() {
        return (this.byteBuffer[0] & 0xff) == TYPE_ID_AUTH_SWITCH;
    }

    /**
     * Is it an OK packet.
     * See https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_basic_ok_packet.html
     *
     * @return true if it is an OK packet
     */
    public final boolean isOKPacket() {
        return (this.byteBuffer[0] & 0xff) == TYPE_ID_OK;
    }

    /**
     * Is it an OK packet for ResultSet. Unlike usual 0x00 signature it has 0xfe signature.
     * See https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_basic_ok_packet.html
     *
     * @return true if it is an OK packet for ResultSet
     */
    public final boolean isResultSetOKPacket() {
        return (this.byteBuffer[0] & 0xff) == TYPE_ID_EOF && this.payloadLength > 5 && this.payloadLength < 16777215;
    }

    /**
     * Is it a Protocol::AuthMoreData packet.
     * See https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_connection_phase_packets_protocol_auth_more_data.html
     *
     * @return true if it is a Protocol::AuthMoreData packet
     */
    public final boolean isAuthMoreDataPacket() {
        return (this.byteBuffer[0] & 0xff) == TYPE_ID_AUTH_MORE_DATA;
    }

    /**
     * Is it a Protocol::AuthNextFactor packet.
     * See https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_connection_phase_packets_protocol_auth_next_factor_request.html
     *
     * @return true if it is a Protocol::AuthNextFactor packet
     */
    public final boolean isAuthNextFactorPacket() {
        return (this.byteBuffer[0] & 0xff) == TYPE_ID_AUTH_NEXT_FACTOR;
    }

    /**
     * Write data according to provided Integer type.
     *
     * @param type
     *            {@link IntegerDataType}
     * @param l
     *            value
     */
    public void writeInteger(IntegerDataType type, long l) {
        byte[] b;
        switch (type) {
            case INT1:
                ensureCapacity(1);
                b = this.byteBuffer;
                b[this.position++] = (byte) (l & 0xff);
                break;

            case INT2:
                ensureCapacity(2);
                b = this.byteBuffer;
                b[this.position++] = (byte) (l & 0xff);
                b[this.position++] = (byte) (l >>> 8);
                break;

            case INT3:
                ensureCapacity(3);
                b = this.byteBuffer;
                b[this.position++] = (byte) (l & 0xff);
                b[this.position++] = (byte) (l >>> 8);
                b[this.position++] = (byte) (l >>> 16);
                break;

            case INT4:
                ensureCapacity(4);
                b = this.byteBuffer;
                b[this.position++] = (byte) (l & 0xff);
                b[this.position++] = (byte) (l >>> 8);
                b[this.position++] = (byte) (l >>> 16);
                b[this.position++] = (byte) (l >>> 24);
                break;

            case INT6:
                ensureCapacity(6);
                b = this.byteBuffer;
                b[this.position++] = (byte) (l & 0xff);
                b[this.position++] = (byte) (l >>> 8);
                b[this.position++] = (byte) (l >>> 16);
                b[this.position++] = (byte) (l >>> 24);
                b[this.position++] = (byte) (l >>> 32);
                b[this.position++] = (byte) (l >>> 40);
                break;

            case INT8:
                ensureCapacity(8);
                b = this.byteBuffer;
                b[this.position++] = (byte) (l & 0xff);
                b[this.position++] = (byte) (l >>> 8);
                b[this.position++] = (byte) (l >>> 16);
                b[this.position++] = (byte) (l >>> 24);
                b[this.position++] = (byte) (l >>> 32);
                b[this.position++] = (byte) (l >>> 40);
                b[this.position++] = (byte) (l >>> 48);
                b[this.position++] = (byte) (l >>> 56);
                break;

            case INT_LENENC:
                if (l < 251) {
                    ensureCapacity(1);
                    writeInteger(IntegerDataType.INT1, l);

                } else if (l < 65536L) {
                    ensureCapacity(3);
                    writeInteger(IntegerDataType.INT1, 252);
                    writeInteger(IntegerDataType.INT2, l);

                } else if (l < 16777216L) {
                    ensureCapacity(4);
                    writeInteger(IntegerDataType.INT1, 253);
                    writeInteger(IntegerDataType.INT3, l);

                } else {
                    ensureCapacity(9);
                    writeInteger(IntegerDataType.INT1, 254);
                    writeInteger(IntegerDataType.INT8, l);
                }
        }

        adjustPayloadLength();
    }

    /**
     * Read data according to provided Integer type.
     *
     * @param type
     *            {@link IntegerDataType}
     * @return long
     */
    public final long readInteger(IntegerDataType type) {
        byte[] b = this.byteBuffer;
        switch (type) {
            case INT1:
                return b[this.position++] & 0xff;

            case INT2:
                return b[this.position++] & 0xff | (b[this.position++] & 0xff) << 8;

            case INT3:
                return b[this.position++] & 0xff | (b[this.position++] & 0xff) << 8 | (b[this.position++] & 0xff) << 16;

            case INT4:
                return (long) b[this.position++] & 0xff | ((long) b[this.position++] & 0xff) << 8 | (long) (b[this.position++] & 0xff) << 16
                        | (long) (b[this.position++] & 0xff) << 24;

            case INT6:
                return b[this.position++] & 0xff | (long) (b[this.position++] & 0xff) << 8 | (long) (b[this.position++] & 0xff) << 16
                        | (long) (b[this.position++] & 0xff) << 24 | (long) (b[this.position++] & 0xff) << 32 | (long) (b[this.position++] & 0xff) << 40;

            case INT8:
                return b[this.position++] & 0xff | (long) (b[this.position++] & 0xff) << 8 | (long) (b[this.position++] & 0xff) << 16
                        | (long) (b[this.position++] & 0xff) << 24 | (long) (b[this.position++] & 0xff) << 32 | (long) (b[this.position++] & 0xff) << 40
                        | (long) (b[this.position++] & 0xff) << 48 | (long) (b[this.position++] & 0xff) << 56;

            case INT_LENENC:
                int sw = b[this.position++] & 0xff;
                switch (sw) {
                    case 251:
                        return NULL_LENGTH; // represents a NULL in a ProtocolText::ResultsetRow
                    case 252:
                        return readInteger(IntegerDataType.INT2);
                    case 253:
                        return readInteger(IntegerDataType.INT3);
                    case 254:
                        return readInteger(IntegerDataType.INT8);
                    default:
                        return sw;
                }

            default:
                return b[this.position++] & 0xff;
        }
    }

    /**
     * Write all bytes from given byte array into internal buffer starting with current buffer position.
     *
     * @param type
     *            on-wire data type
     * @param b
     *            from byte array
     */
    public final void writeBytes(StringSelfDataType type, byte[] b) {
        writeBytes(type, b, 0, b.length);
    }

    /**
     * Write all bytes from given byte array into internal buffer starting with current buffer position.
     *
     * @param type
     *            on-wire data type
     * @param b
     *            from byte array
     */
    public final void writeBytes(StringLengthDataType type, byte[] b) {
        writeBytes(type, b, 0, b.length);
    }

    /**
     * Write len bytes from given byte array into internal buffer.
     * Read starts from given offset, write starts with current buffer position.
     *
     * @param type
     *            on-wire data type
     * @param b
     *            from byte array
     * @param offset
     *            starting index of b
     * @param len
     *            number of bytes to be written
     */
    public void writeBytes(StringSelfDataType type, byte[] b, int offset, int len) {
        switch (type) {
            case STRING_EOF:
                writeBytes(StringLengthDataType.STRING_FIXED, b, offset, len);
                break;

            case STRING_TERM:
                ensureCapacity(len + 1);
                writeBytes(StringLengthDataType.STRING_FIXED, b, offset, len);
                this.byteBuffer[this.position++] = 0;
                break;

            case STRING_LENENC:
                ensureCapacity(len + 9);
                writeInteger(IntegerDataType.INT_LENENC, len);
                writeBytes(StringLengthDataType.STRING_FIXED, b, offset, len);
                break;
        }

        adjustPayloadLength();
    }

    /**
     * Write len bytes from given byte array into internal buffer.
     * Read starts from given offset, write starts with current buffer position.
     *
     * @param type
     *            on-wire data type
     * @param b
     *            from byte array
     * @param offset
     *            starting index of b
     * @param len
     *            number of bytes to be written
     */
    public void writeBytes(StringLengthDataType type, byte[] b, int offset, int len) {
        switch (type) {
            case STRING_FIXED:
            case STRING_VAR:
                ensureCapacity(len);
                System.arraycopy(b, offset, this.byteBuffer, this.position, len);
                this.position += len;
                break;
        }

        adjustPayloadLength();
    }

    /**
     * Read bytes from internal buffer starting from current position into the new byte array.
     * The length of data to read depends on {@link StringSelfDataType}.
     *
     * @param type
     *            {@link StringSelfDataType}
     * @return bytes
     */
    public byte[] readBytes(StringSelfDataType type) {
        byte[] b;
        switch (type) {
            case STRING_TERM:
                int i = this.position;
                while (i < this.payloadLength && this.byteBuffer[i] != 0) {
                    i++;
                }
                b = readBytes(StringLengthDataType.STRING_FIXED, i - this.position);
                this.position++; // skip terminating byte
                return b;

            case STRING_LENENC:
                long l = readInteger(IntegerDataType.INT_LENENC);
                return l == NULL_LENGTH ? null : l == 0 ? Constants.EMPTY_BYTE_ARRAY : readBytes(StringLengthDataType.STRING_FIXED, (int) l);

            case STRING_EOF:
                return readBytes(StringLengthDataType.STRING_FIXED, this.payloadLength - this.position);
        }
        return null;
    }

    /**
     * Set position to next value in internal buffer skipping the current value according to {@link StringSelfDataType}.
     *
     * @param type
     *            {@link StringSelfDataType}
     */
    public void skipBytes(StringSelfDataType type) {
        switch (type) {
            case STRING_TERM:
                while (this.position < this.payloadLength && this.byteBuffer[this.position] != 0) {
                    this.position++;
                }
                this.position++; // skip terminating byte
                break;

            case STRING_LENENC:
                long len = readInteger(IntegerDataType.INT_LENENC);
                if (len != NULL_LENGTH && len != 0) {
                    this.position += (int) len;
                }
                break;

            case STRING_EOF:
                this.position = this.payloadLength;
                break;
        }
    }

    /**
     * Read len bytes from internal buffer starting from current position into the new byte array.
     *
     * @param type
     *            {@link StringLengthDataType}
     * @param len
     *            length
     * @return bytes
     */
    public byte[] readBytes(StringLengthDataType type, int len) {
        byte[] b;
        switch (type) {
            case STRING_FIXED:
            case STRING_VAR:
                b = new byte[len];
                System.arraycopy(this.byteBuffer, this.position, b, 0, len);
                this.position += len;
                return b;
        }
        return null;
    }

    /**
     * Read bytes from internal buffer starting from current position decoding them into String using the specified character encoding.
     * The length of data to read depends on {@link StringSelfDataType}.
     *
     * @param type
     *            {@link StringSelfDataType}
     * @param encoding
     *            if null then platform default encoding is used
     * @return string
     */
    public String readString(StringSelfDataType type, String encoding) {
        String res = null;
        switch (type) {
            case STRING_TERM:
                int i = this.position;
                while (i < this.payloadLength && this.byteBuffer[i] != 0) {
                    i++;
                }
                res = readString(StringLengthDataType.STRING_FIXED, encoding, i - this.position);
                this.position++; // skip terminating byte
                break;

            case STRING_LENENC:
                if (this.position >= this.payloadLength) { // Servers do not always respect STRING_LENENC encoding: Bug#35630063.
                    return "";
                }
                long l = readInteger(IntegerDataType.INT_LENENC);
                return l == NULL_LENGTH ? null : l == 0 ? "" : readString(StringLengthDataType.STRING_FIXED, encoding, (int) l);

            case STRING_EOF:
                return readString(StringLengthDataType.STRING_FIXED, encoding, this.payloadLength - this.position);

        }
        return res;
    }

    /**
     * Read len bytes from internal buffer starting from current position decoding them into String using the specified character encoding.
     *
     * @param type
     *            {@link StringLengthDataType}
     * @param encoding
     *            if null then platform default encoding is used
     * @param len
     *            length
     * @return string
     */
    public String readString(StringLengthDataType type, String encoding, int len) {
        String res = null;
        switch (type) {
            case STRING_FIXED:
            case STRING_VAR:
                if (this.position + len > this.payloadLength) {
                    throw ExceptionFactory.createException(WrongArgumentException.class, Messages.getString("Buffer.1"));
                }

                res = StringUtils.toString(this.byteBuffer, this.position, len, encoding);
                this.position += len;
                break;

        }
        return res;
    }

    public static String extractSqlFromPacket(String possibleSqlQuery, NativePacketPayload packet, int endOfQueryPacketPosition, int maxQuerySizeToLog) {
        String extractedSql = null;

        if (possibleSqlQuery != null) {
            if (possibleSqlQuery.length() > maxQuerySizeToLog) {
                StringBuilder truncatedQueryBuf = new StringBuilder(possibleSqlQuery.substring(0, maxQuerySizeToLog));
                truncatedQueryBuf.append(Messages.getString("MysqlIO.25"));
                extractedSql = truncatedQueryBuf.toString();
            } else {
                extractedSql = possibleSqlQuery;
            }
        }

        if (extractedSql == null) {
            // This is probably from a client-side prepared statement

            int extractPosition = endOfQueryPacketPosition;

            boolean truncated = false;

            if (endOfQueryPacketPosition > maxQuerySizeToLog) {
                extractPosition = maxQuerySizeToLog;
                truncated = true;
            }

            extractedSql = StringUtils.toString(packet.getByteBuffer(), 1, extractPosition - 1);

            if (truncated) {
                extractedSql += Messages.getString("MysqlIO.25");
            }
        }

        return extractedSql;
    }

    /**
     * Tag current position with the given key for future reference.
     *
     * @param key
     *            the position tag key name.
     * @return
     *         the previous value of this tag, if there was one, or -1.
     */
    public int setTag(String key) {
        Integer pos = this.tags.put(key, getPosition());
        return pos == null ? -1 : pos;
    }

    /**
     * Gets the value of the position tag for the given key.
     *
     * @param key
     *            the position tag key name.
     * @return
     *         the position value of this tag, if there was one, or -1.
     */
    public int getTag(String key) {
        Integer pos = this.tags.get(key);
        return pos == null ? -1 : pos;
    }

}
