/*
 * 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.sshd.common.util.buffer;

import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.Objects;
import java.util.function.IntUnaryOperator;

import org.apache.sshd.common.util.GenericUtils;
import org.apache.sshd.common.util.NumberUtils;
import org.apache.sshd.common.util.Readable;
import org.apache.sshd.common.util.ValidateUtils;

/**
 * Provides an implementation of {@link Buffer} using a backing byte array
 *
 * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
 */
public class ByteArrayBuffer extends Buffer {
    /**
     * Initial default allocated buffer size if none specified
     */
    public static final int DEFAULT_SIZE = 256;

    private byte[] data;
    private int rpos;
    private int wpos;

    /**
     * Allocates a buffer for writing purposes with {@value #DEFAULT_SIZE} bytes
     */
    public ByteArrayBuffer() {
        this(DEFAULT_SIZE, false);
    }

    /**
     * Allocates a buffer for writing purposes
     *
     * @param size Initial buffer size - <B>Note:</B> it is <U>rounded</U> to the closest power of 2 that is greater or
     *             equal to it.
     * @see        #ByteArrayBuffer(int, boolean)
     */
    public ByteArrayBuffer(int size) {
        this(size, true);
    }

    /**
     * Allocates a buffer for writing purposes
     *
     * @param size     Initial buffer size
     * @param roundOff Whether to round it to closest power of 2 that is greater or equal to the specified size
     */
    public ByteArrayBuffer(int size, boolean roundOff) {
        this(new byte[roundOff ? BufferUtils.getNextPowerOf2(size) : size], false);
    }

    /**
     * Wraps data bytes for reading
     *
     * @param data Data bytes to read from
     * @see        #ByteArrayBuffer(byte[], boolean)
     */
    public ByteArrayBuffer(byte[] data) {
        this(data, 0, data.length, true);
    }

    /**
     * @param data Data bytes to use
     * @param read Whether the data bytes are for reading or writing
     */
    public ByteArrayBuffer(byte[] data, boolean read) {
        this(data, 0, data.length, read);
    }

    /**
     * Wraps data bytes for reading
     *
     * @param data Data bytes to read from
     * @param off  Offset to read from
     * @param len  Available bytes from given offset
     * @see        #ByteArrayBuffer(byte[], int, int, boolean)
     */
    public ByteArrayBuffer(byte[] data, int off, int len) {
        this(data, off, len, true);
    }

    /**
     * @param data Data bytes to use
     * @param off  Offset to read/write (according to <tt>read</tt> parameter)
     * @param len  Available bytes from given offset
     * @param read Whether the data bytes are for reading or writing
     */
    public ByteArrayBuffer(byte[] data, int off, int len, boolean read) {
        if ((off < 0) || (len < 0)) {
            throw new IndexOutOfBoundsException("Invalid offset(" + off + ")/length(" + len + ")");
        }
        this.data = data;
        this.rpos = off;
        this.wpos = (read ? len : 0) + off;
    }

    @Override
    public int rpos() {
        return rpos;
    }

    @Override
    public void rpos(int rpos) {
        this.rpos = rpos;
    }

    @Override
    public int wpos() {
        return wpos;
    }

    @Override
    public void wpos(int wpos) {
        if (wpos > this.wpos) {
            ensureCapacity(wpos - this.wpos);
        }
        this.wpos = wpos;
    }

    @Override
    public int available() {
        return wpos - rpos;
    }

    @Override
    public int capacity() {
        return data.length - wpos;
    }

    @Override
    public byte[] array() {
        return data;
    }

    @Override
    public byte[] getBytesConsumed() {
        byte[] consumed = new byte[rpos];
        System.arraycopy(data, 0, consumed, 0, rpos);
        return consumed;
    }

    @Override
    public byte[] getBytesConsumed(int from) {
        if (from >= rpos) {
            return new byte[0];
        }
        return Arrays.copyOfRange(data, from, rpos);
    }

    @Override
    public byte rawByte(int pos) {
        return data[pos];
    }

    @Override
    public long rawUInt(int pos) {
        return BufferUtils.getUInt(data, pos, Integer.BYTES);
    }

    @Override
    public void compact() {
        int avail = available();
        if (avail > 0) {
            System.arraycopy(data, rpos, data, 0, avail);
        }
        wpos -= rpos;
        rpos = 0;
    }

    @Override
    public Buffer clear(boolean wipeData) {
        rpos = 0;
        wpos = 0;

        if (wipeData) {
            Arrays.fill(data, (byte) 0);
        }

        return this;
    }

    @Override
    public byte getByte() {
        if (rpos >= wpos) {
            throw new BufferException("Underflow: no available bytes in buffer");
        }
        return data[rpos++];
    }

    @Override
    public void putByte(byte b) {
        if (wpos >= data.length) {
            ensureCapacity(Byte.BYTES);
        }
        data[wpos++] = b;
    }

    @Override
    public int getInt() {
        if (rpos + Integer.BYTES > wpos) {
            throw new BufferException("Underflow: not enough bytes in buffer, need " + Integer.BYTES);
        }
        int i = BufferUtils.getInt(data, rpos, Integer.BYTES);
        rpos += Integer.BYTES;
        return i;
    }

    @Override
    public void putInt(long i) {
        ValidateUtils.checkTrue(((int) i) == i, "Invalid INT32 value: %d", i);
        if (wpos + Integer.BYTES > data.length) {
            ensureCapacity(Integer.BYTES);
        }
        BufferUtils.putUInt(i, data, wpos, Integer.BYTES);
        wpos += Integer.BYTES;
    }

    @Override
    public long getUInt() {
        if (rpos + Integer.BYTES > wpos) {
            throw new BufferException("Underflow: not enough bytes in buffer, need " + Integer.BYTES);
        }
        long l = BufferUtils.getUInt(data, rpos, Integer.BYTES);
        rpos += Integer.BYTES;
        return l;
    }

    @Override
    public void putUInt(long i) {
        ValidateUtils.checkTrue((i & 0xFFFF_FFFFL) == i, "Invalid UINT32 value: %d", i);
        if (wpos + Integer.BYTES > data.length) {
            ensureCapacity(Integer.BYTES);
        }
        BufferUtils.putUInt(i, data, wpos, Integer.BYTES);
        wpos += Integer.BYTES;
    }

    @Override
    public int putBuffer(Readable buffer, boolean expand) {
        int required = expand ? buffer.available() : Math.min(buffer.available(), capacity());
        ensureCapacity(required);
        buffer.getRawBytes(data, wpos, required);
        wpos += required;
        return required;
    }

    @Override
    public void putBuffer(ByteBuffer buffer) {
        int required = buffer.remaining();
        ensureCapacity(required + Integer.BYTES);
        putUInt(required);
        buffer.get(data, wpos, required);
        wpos += required;
    }

    @Override
    public void putBytes(byte[] b, int off, int len) {
        ValidateUtils.checkTrue(len >= 0, "Negative raw bytes length: %d", len);
        if (wpos + Integer.BYTES + len > data.length) {
            ensureCapacity(Integer.BYTES + len);
        }
        BufferUtils.putUInt(len, data, wpos, Integer.BYTES);
        wpos += Integer.BYTES;
        System.arraycopy(b, off, data, wpos, len);
        wpos += len;
    }

    @Override
    public void putRawBytes(byte[] d, int off, int len) {
        ValidateUtils.checkTrue(len >= 0, "Negative raw bytes length: %d", len);
        if (wpos + len > data.length) {
            ensureCapacity(len);
        }
        System.arraycopy(d, off, data, wpos, len);
        wpos += len;
    }

    @Override
    public String getString(Charset charset) {
        Objects.requireNonNull(charset, "No charset specified");

        int reqLen = getInt();
        int len = ensureAvailable(reqLen);
        String s = new String(data, rpos, len, charset);
        rpos += len;
        return s;
    }

    @Override
    public void getRawBytes(byte[] buf, int off, int len) {
        ensureAvailable(len);
        copyRawBytes(0, buf, off, len);
        rpos += len;
    }

    @Override
    protected void copyRawBytes(int offset, byte[] buf, int pos, int len) {
        if ((offset < 0) || (pos < 0) || (len < 0)) {
            throw new IndexOutOfBoundsException(
                    "Invalid offset(" + offset + ")/position(" + pos + ")/length(" + len + ") required");
        }
        System.arraycopy(data, rpos + offset, buf, pos, len);
    }

    @Override
    public Buffer ensureCapacity(int capacity, IntUnaryOperator growthFactor) {
        ValidateUtils.checkTrue(capacity >= 0, "Negative capacity requested: %d", capacity);

        int maxSize = size();
        int curPos = wpos();
        int remaining = maxSize - curPos;
        if (remaining < capacity) {
            int minimum = curPos + capacity;
            int actual = growthFactor.applyAsInt(minimum);
            if (actual < minimum) {
                throw new IllegalStateException(
                        "ensureCapacity(" + capacity + ") actual (" + actual + ") below min. (" + minimum + ")");
            }
            byte[] tmp = new byte[actual];
            System.arraycopy(data, 0, tmp, 0, data.length);
            data = tmp;
        }

        return this;
    }

    @Override
    protected int size() {
        return data.length;
    }

    /**
     * Creates a compact buffer (i.e., one that starts at offset zero) containing a <U>copy</U> of the original data
     *
     * @param  data The original data buffer
     * @return      A {@link ByteArrayBuffer} containing a <U>copy</U> of the original data starting at zero read
     *              position
     * @see         #getCompactClone(byte[], int, int)
     */
    public static ByteArrayBuffer getCompactClone(byte[] data) {
        return getCompactClone(data, 0, NumberUtils.length(data));
    }

    /**
     * Creates a compact buffer (i.e., one that starts at offset zero) containing a <U>copy</U> of the original data
     *
     * @param  data   The original data buffer
     * @param  offset The offset of the valid data in the buffer
     * @param  len    The size (in bytes) of of the valid data in the buffer
     * @return        A {@link ByteArrayBuffer} containing a <U>copy</U> of the original data starting at zero read
     *                position
     */
    public static ByteArrayBuffer getCompactClone(byte[] data, int offset, int len) {
        byte[] clone = (len > 0) ? new byte[len] : GenericUtils.EMPTY_BYTE_ARRAY;
        if (len > 0) {
            System.arraycopy(data, offset, clone, 0, len);
        }

        return new ByteArrayBuffer(clone, true);
    }
}
