/*
 * Copyright LWJGL. All rights reserved.
 * License terms: http://lwjgl.org/license.php
 */
package com.jme3.lwjgl3.utils;

import org.lwjgl.BufferUtils;
import org.lwjgl.PointerBuffer;

import java.nio.ByteBuffer;
import java.util.Arrays;

import static org.lwjgl.system.Pointer.*;
import static org.lwjgl.system.MathUtil.*;
import org.lwjgl.system.MemoryUtil;
import static org.lwjgl.system.MemoryUtil.*;

/**
 * Helper class for alternative API functions. Instead of the user passing their
 * own buffer, thread-local instances of this class are used internally instead.
 */
public class APIBuffer {

    private static final int DEFAULT_CAPACITY = 128;

    private ByteBuffer buffer;
    private long address;

    private int offset;

    private int stackDepth;
    private int[] stack = new int[4];

    public APIBuffer() {
        buffer = BufferUtils.createByteBuffer(DEFAULT_CAPACITY);
        address = memAddress(buffer);
    }

    /**
     * Resets the parameter offset to 0.
     */
    public APIBuffer reset() {
        offset = 0;
        return this;
    }

    /**
     * Pushes the current parameter offset to a stack.
     */
    public APIBuffer push() {
        if (stackDepth == stack.length) {
            stack = Arrays.copyOf(stack, stack.length << 1);
        }

        stack[stackDepth++] = offset;

        // Upward align the current offset to the pointer size.
        offset = (offset + (POINTER_SIZE - 1)) & -POINTER_SIZE;

        return this;
    }

    /**
     * Restores the last pushed parameter offset.
     */
    public APIBuffer pop() {
        offset = stack[--stackDepth];
        return this;
    }

    /**
     * Returns the current parameter offset.
     */
    public int getOffset() {
        return offset;
    }

    /**
     * Sets the current parameter offset.
     */
    public void setOffset(int offset) {
        this.offset = offset;
    }

    /**
     * Returns the memory address of the internal {@link ByteBuffer}. This
     * address may change after a call to one of the {@code <type>Param()}
     * methods.
     */
    public long address() {
        return address;
    }

    /**
     * Returns the memory address of the specified {@code offset}. This address
     * may change after a call to one of the {@code <type>Param()} methods.
     */
    public long address(int offset) {
        return address + offset;
    }

    /**
     * Returns the memory address of the specified {@code offset} or
     * {@link MemoryUtil#NULL NULL} if the specified {@code value} is null. This
     * address may change after a call to one of the {@code <type>Param()}
     * methods.
     */
    public long addressSafe(Object value, int offset) {
        return value == null ? NULL : address(offset);
    }

    /**
     * Returns the {@link ByteBuffer} that backs this {@link APIBuffer}.
     */
    public ByteBuffer buffer() {
        return buffer;
    }

    private void ensureCapacity(int capacity) {
        if (capacity <= buffer.capacity()) {
            return;
        }

        ByteBuffer resized = BufferUtils.createByteBuffer(mathRoundPoT(capacity));

        resized.put(buffer);
        resized.clear();

        buffer = resized;
        address = memAddress(resized);
    }

    // ---------------------------------------------------------------------------------------------------------------------
    private int param(int bytes) {
        return param(bytes, bytes);
    }

    private int param(int bytes, int alignment) {
        // Upward align the current offset to the specified alignment
        int param = (offset + (alignment - 1)) & -alignment;
        ensureCapacity(offset = param + bytes);
        return param;
    }

    /**
     * Ensures space for an additional boolean value and returns the address
     * offset.
     */
    public int booleanParam() {
        return param(1);
    }

    /**
     * Ensures space for an additional byte value and returns the address
     * offset.
     */
    public int byteParam() {
        return param(1);
    }

    /**
     * Ensures space for an additional short value and returns the address
     * offset.
     */
    public int shortParam() {
        return param(2);
    }

    /**
     * Ensures space for an additional int value and returns the address offset.
     */
    public int intParam() {
        return param(4);
    }

    /**
     * Ensures space for an additional long value and returns the address
     * offset.
     */
    public int longParam() {
        return param(8);
    }

    /**
     * Ensures space for an additional float value and returns the address
     * offset.
     */
    public int floatParam() {
        return param(4);
    }

    /**
     * Ensures space for an additional double value and returns the address
     * offset.
     */
    public int doubleParam() {
        return param(8);
    }

    /**
     * Ensures space for an additional pointer value and returns the address
     * offset.
     */
    public int pointerParam() {
        return param(POINTER_SIZE);
    }

    /**
     * Ensures space for an additional buffer with the specified size (in bytes)
     * and returns the address offset.
     */
    public int bufferParam(int size) {
        return param(size, POINTER_SIZE);
    }

    // ---------------------------------------------------------------------------------------------------------------------
    /**
     * Ensures space for an additional boolean value, sets the specified value
     * at the allocated offset and returns that offset.
     */
    public int booleanParam(boolean value) {
        int offset = booleanParam();
        buffer.put(offset, value ? (byte) 1 : (byte) 0);
        return offset;
    }

    /**
     * Ensures space for an additional byte value, sets the specified value at
     * the allocated offset and returns that offset.
     */
    public int byteParam(byte value) {
        int offset = byteParam();
        buffer.put(offset, value);
        return offset;
    }

    /**
     * Ensures space for an additional short value, sets the specified value at
     * the allocated offset and returns that offset.
     */
    public int shortParam(short value) {
        int offset = shortParam();
        buffer.putShort(offset, value);
        return offset;
    }

    /**
     * Ensures space for an additional int value, sets the specified value at
     * the allocated offset and returns that offset.
     */
    public int intParam(int value) {
        int offset = intParam();
        buffer.putInt(offset, value);
        return offset;
    }

    /**
     * Ensures space for an additional long value, sets the specified value at
     * the allocated offset and returns that offset.
     */
    public int longParam(long value) {
        int offset = longParam();
        buffer.putLong(offset, value);
        return offset;
    }

    /**
     * Ensures space for an additional float value, sets the specified value at
     * the allocated offset and returns that offset.
     */
    public int floatParam(float value) {
        int offset = floatParam();
        buffer.putFloat(offset, value);
        return offset;
    }

    /**
     * Ensures space for an additional double value, sets the specified value at
     * the allocated offset and returns that offset.
     */
    public int doubleParam(double value) {
        int offset = doubleParam();
        buffer.putDouble(offset, value);
        return offset;
    }

    /**
     * Ensures space for an additional pointer value, sets the specified value
     * at the allocated offset and returns that offset.
     */
    public int pointerParam(long value) {
        int offset = pointerParam();
        PointerBuffer.put(buffer, offset, value);
        return offset;
    }
    // ----

    /**
     * Ensures space for an additional pointer buffer, sets the specified memory
     * addresses and returns the address offset.
     */
    public int pointerArrayParam(long... pointers) {
        int buffersAddress = bufferParam(pointers.length << POINTER_SHIFT);
        for (int i = 0; i < pointers.length; i++) {
            pointerParam(buffersAddress, i, pointers[i]);
        }

        return buffersAddress;
    }

    /**
     * Ensures space for an additional pointer buffer, sets the memory addresses
     * of the specified buffers and returns the address offset.
     */
    public int pointerArrayParam(ByteBuffer... buffers) {
        int buffersAddress = bufferParam(buffers.length << POINTER_SHIFT);
        for (int i = 0; i < buffers.length; i++) {
            pointerParam(buffersAddress, i, memAddress(buffers[i]));
        }

        return buffersAddress;
    }

    /**
     * Ensures space for two additional pointer buffers, sets the memory
     * addresses and remaining bytes of the specified buffers and returns the
     * address offset.
     */
    public int pointerArrayParamp(ByteBuffer... buffers) {
        int buffersAddress = pointerArrayParam(buffers);

        int buffersLengths = bufferParam(buffers.length << POINTER_SHIFT);
        for (int i = 0; i < buffers.length; i++) {
            pointerParam(buffersLengths, i, buffers[i].remaining());
        }

        return buffersAddress;
    }

    // ---------------------------------------------------------------------------------------------------------------------
    /**
     * ASCII encodes the specified strings with a null-terminator and ensures
     * space for a buffer filled with the memory addresses of the encoded
     * strings.
     *
     * <p>
     * The encoded buffers must be later freed with
     * {@link #pointerArrayFree(int, int)}.</p>
     *
     * @return the offset to the memory address buffer
     */
    public int pointerArrayParamASCII(CharSequence... strings) {
        int buffersAddress = bufferParam(strings.length << POINTER_SHIFT);
        for (int i = 0; i < strings.length; i++) {
            ByteBuffer buffer = MemoryUtil.memASCII(strings[i]);

            pointerParam(buffersAddress, i, memAddress(buffer));
        }

        return buffersAddress;
    }

    /**
     * ASCII encodes the specified strings and ensures space for two additional
     * buffers filled with the lengths and memory addresses of the encoded
     * strings, respectively. The lengths are 4-bytes integers and the memory
     * address buffer starts immediately after the lengths buffer.
     *
     * <p>
     * The encoded buffers must be later freed with
     * {@link #pointerArrayFree(int, int)}.</p>
     *
     * @return the offset to the lengths buffer
     */
    public int pointerArrayParamASCIIi(CharSequence... strings) {
        int buffersAddress = bufferParam(strings.length << POINTER_SHIFT);
        int lengthsAddress = bufferParam(strings.length << 2);

        for (int i = 0; i < strings.length; i++) {
            ByteBuffer buffer = MemoryUtil.memASCII(strings[i]);

            pointerParam(buffersAddress, i, memAddress(buffer));
            intParam(lengthsAddress, i, buffer.remaining());
        }

        return buffersAddress;
    }

    /**
     * ASCII encodes the specified strings and ensures space for two additional
     * buffers filled with the lengths and memory addresses of the encoded
     * strings, respectively. The lengths are pointer-sized integers and the
     * memory address buffer starts immediately after the lengths buffer.
     *
     * <p>
     * The encoded buffers must be later freed with
     * {@link #pointerArrayFree(int, int)}.</p>
     *
     * @return the offset to the lengths buffer
     */
    public int pointerArrayParamASCIIp(CharSequence... strings) {
        int buffersAddress = bufferParam(strings.length << POINTER_SHIFT);
        int lengthsAddress = bufferParam(strings.length << POINTER_SHIFT);

        for (int i = 0; i < strings.length; i++) {
            ByteBuffer buffer = MemoryUtil.memASCII(strings[i]);

            pointerParam(buffersAddress, i, memAddress(buffer));
            pointerParam(lengthsAddress, i, buffer.remaining());
        }

        return buffersAddress;
    }

    /**
     * UTF8 encodes the specified strings with a null-terminator and ensures
     * space for a buffer filled with the memory addresses of the encoded
     * strings.
     *
     * <p>
     * The encoded buffers must be later freed with
     * {@link #pointerArrayFree(int, int)}.</p>
     *
     * @return the offset to the memory address buffer
     */
    public int pointerArrayParamUTF8(CharSequence... strings) {
        int buffersAddress = bufferParam(strings.length << POINTER_SHIFT);
        for (int i = 0; i < strings.length; i++) {
            ByteBuffer buffer = MemoryUtil.memUTF8(strings[i]);

            pointerParam(buffersAddress, i, memAddress(buffer));
        }

        return buffersAddress;
    }

    /**
     * UTF8 encodes the specified strings and ensures space for two additional
     * buffers filled with the lengths and memory addresses of the encoded
     * strings, respectively. The lengths are 4-bytes integers and the memory
     * address buffer starts immediately after the lengths buffer.
     *
     * <p>
     * The encoded buffers must be later freed with
     * {@link #pointerArrayFree(int, int)}.</p>
     *
     * @return the offset to the lengths buffer
     */
    public int pointerArrayParamUTF8i(CharSequence... strings) {
        int buffersAddress = bufferParam(strings.length << POINTER_SHIFT);
        int lengthsAddress = bufferParam(strings.length << 2);

        for (int i = 0; i < strings.length; i++) {
            ByteBuffer buffer = MemoryUtil.memUTF8(strings[i]);

            pointerParam(buffersAddress, i, memAddress(buffer));
            intParam(lengthsAddress, i, buffer.remaining());
        }

        return buffersAddress;
    }

    /**
     * UTF8 encodes the specified strings and ensures space for two additional
     * buffers filled with the lengths and memory addresses of the encoded
     * strings, respectively. The lengths are pointer-sized integers and the
     * memory address buffer starts immediately after the lengths buffer.
     *
     * <p>
     * The encoded buffers must be later freed with
     * {@link #pointerArrayFree(int, int)}.</p>
     *
     * @return the offset to the lengths buffer
     */
    public int pointerArrayParamUTF8p(CharSequence... strings) {
        int buffersAddress = bufferParam(strings.length << POINTER_SHIFT);
        int lengthsAddress = bufferParam(strings.length << POINTER_SHIFT);

        for (int i = 0; i < strings.length; i++) {
            ByteBuffer buffer = MemoryUtil.memUTF8(strings[i]);

            pointerParam(buffersAddress, i, memAddress(buffer));
            pointerParam(lengthsAddress, i, buffer.remaining());
        }

        return buffersAddress;
    }

    /**
     * UTF16 encodes the specified strings with a null-terminator and ensures
     * space for a buffer filled with the memory addresses of the encoded
     * strings.
     *
     * <p>
     * The encoded buffers must be later freed with
     * {@link #pointerArrayFree(int, int)}.</p>
     *
     * @return the offset to the memory address buffer
     */
    public int pointerArrayParamUTF16(CharSequence... strings) {
        int buffersAddress = bufferParam(strings.length << POINTER_SHIFT);
        for (int i = 0; i < strings.length; i++) {
            ByteBuffer buffer = MemoryUtil.memUTF16(strings[i]);

            pointerParam(buffersAddress, i, memAddress(buffer));
        }

        return buffersAddress;
    }

    /**
     * UTF16 encodes the specified strings and ensures space for two additional
     * buffers filled with the lengths and memory addresses of the encoded
     * strings, respectively. The lengths are 4-bytes integers and the memory
     * address buffer starts immediately after the lengths buffer.
     *
     * <p>
     * The encoded buffers must be later freed with
     * {@link #pointerArrayFree(int, int)}.</p>
     *
     * @return the offset to the lengths buffer
     */
    public int pointerArrayParamUTF16i(CharSequence... strings) {
        int buffersAddress = bufferParam(strings.length << POINTER_SHIFT);
        int lengthsAddress = bufferParam(strings.length << 2);

        for (int i = 0; i < strings.length; i++) {
            ByteBuffer buffer = MemoryUtil.memUTF16(strings[i]);

            pointerParam(buffersAddress, i, memAddress(buffer));
            intParam(lengthsAddress, i, buffer.remaining());
        }

        return buffersAddress;
    }

    /**
     * UTF16 encodes the specified strings and ensures space for two additional
     * buffers filled with the lengths and memory addresses of the encoded
     * strings, respectively. The lengths are pointer-sized integers and the
     * memory address buffer starts immediately after the lengths buffer.
     *
     * <p>
     * The encoded buffers must be later freed with
     * {@link #pointerArrayFree(int, int)}.</p>
     *
     * @return the offset to the lengths buffer
     */
    public int pointerArrayParamUTF16p(CharSequence... strings) {
        int buffersAddress = bufferParam(strings.length << POINTER_SHIFT);
        int lengthsAddress = bufferParam(strings.length << POINTER_SHIFT);

        for (int i = 0; i < strings.length; i++) {
            ByteBuffer buffer = MemoryUtil.memUTF16(strings[i]);

            pointerParam(buffersAddress, i, memAddress(buffer));
            pointerParam(lengthsAddress, i, buffer.remaining());
        }

        return buffersAddress;
    }

    // ---------------------------------------------------------------------------------------------------------------------
    /**
     * Frees {@code length} memory blocks stored in the APIBuffer, starting at
     * the specified {@code offset}.
     */
    public void pointerArrayFree(int offset, int length) {
        for (int i = 0; i < length; i++) {
            nmemFree(pointerValue(offset + (i << POINTER_SHIFT)));
        }
    }

    // ---------------------------------------------------------------------------------------------------------------------
    /**
     * Sets an int value at the specified index of the int buffer that starts at
     * the specified offset.
     */
    public void intParam(int offset, int index, int value) {
        buffer.putInt(offset + (index << 2), value);
    }

    /**
     * Sets a pointer value at the specified index of the pointer buffer that
     * starts at the specified offset.
     */
    public void pointerParam(int offset, int index, long value) {
        PointerBuffer.put(buffer, offset + (index << POINTER_SHIFT), value);
    }

    // ---------------------------------------------------------------------------------------------------------------------
    /**
     * Ensures space for the specified string encoded in ASCII, encodes the
     * string at the allocated offset and returns that offset.
     */
    public int stringParamASCII(CharSequence value, boolean nullTerminated) {
        if (value == null) {
            return -1;
        }

        int offset = bufferParam(value.length() + (nullTerminated ? 1 : 0));
        MemoryUtil.memASCII(value, nullTerminated, buffer, offset);
        return offset;
    }

    /**
     * Ensures space for the specified string encoded in UTF-8, encodes the
     * string at the allocated offset and returns that offset.
     */
    public int stringParamUTF8(CharSequence value, boolean nullTerminated) {
        if (value == null) {
            return -1;
        }

        int encodedLen = MemoryUtil.memLengthUTF8(value, nullTerminated);
        int offset = bufferParam(encodedLen);
        MemoryUtil.memUTF8(value, nullTerminated, buffer, offset);
        return offset;
    }

    /**
     * Ensures space for the specified string encoded in UTF-16, encodes the
     * string at the allocated offset and returns that offset.
     */
    public int stringParamUTF16(CharSequence value, boolean nullTerminated) {
        if (value == null) {
            return -1;
        }

        int offset = bufferParam((value.length() + (nullTerminated ? 1 : 0)) << 1);
        MemoryUtil.memUTF16(value, nullTerminated, buffer, offset);
        return offset;
    }

    // ---------------------------------------------------------------------------------------------------------------------
    /**
     * Returns the boolean value at the specified offset.
     */
    public boolean booleanValue(int offset) {
        return buffer.get(offset) != 0;
    }

    /**
     * Returns the boolean value at the specified offset.
     */
    public byte byteValue(int offset) {
        return buffer.get(offset);
    }

    /**
     * Returns the short value at the specified offset.
     */
    public short shortValue(int offset) {
        return buffer.getShort(offset);
    }

    /**
     * Returns the int value at the specified offset.
     */
    public int intValue(int offset) {
        return buffer.getInt(offset);
    }

    /**
     * Returns the long value at the specified offset.
     */
    public long longValue(int offset) {
        return buffer.getLong(offset);
    }

    /**
     * Returns the float value at the specified offset.
     */
    public float floatValue(int offset) {
        return buffer.getFloat(offset);
    }

    /**
     * Returns the double value at the specified offset.
     */
    public double doubleValue(int offset) {
        return buffer.getDouble(offset);
    }

    /**
     * Returns the pointer value at the specified offset.
     */
    public long pointerValue(int offset) {
        return PointerBuffer.get(buffer, offset);
    }

    /**
     * Returns the ASCII string value at the specified byte range.
     */
    public String stringValueASCII(int offset, int limit) {
        buffer.position(offset);
        buffer.limit(limit);
        try {
            return MemoryUtil.memASCII(buffer);
        } finally {
            buffer.clear();
        }
    }

    /**
     * Returns the UTF8 string value at the specified byte range.
     */
    public String stringValueUTF8(int offset, int limit) {
        buffer.position(offset);
        buffer.limit(limit);
        try {
            return MemoryUtil.memUTF8(buffer);
        } finally {
            buffer.clear();
        }
    }

    /**
     * Returns the UTF16 string value at the specified byte range.
     */
    public String stringValueUTF16(int offset, int limit) {
        buffer.position(offset);
        buffer.limit(limit);
        try {
            return MemoryUtil.memUTF16(buffer);
        } finally {
            buffer.clear();
        }
    }
}
