/*
 * Copyright (C) 2022 The Android Open Source Project
 *
 * Licensed 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 android.graphics;

import android.annotation.ColorInt;
import android.annotation.ColorLong;
import android.annotation.IntDef;
import android.annotation.NonNull;

import libcore.util.NativeAllocationRegistry;

import java.nio.Buffer;
import java.nio.ShortBuffer;

/**
 * Class representing a mesh object.
 *
 * This class represents a Mesh object that can optionally be indexed.
 * A {@link MeshSpecification} is required along with various attributes for
 * detailing the mesh object, including a mode, vertex buffer, optional index buffer, and bounds
 * for the mesh. Once generated, a mesh object can be drawn through
 * {@link Canvas#drawMesh(Mesh, BlendMode, Paint)}
 */
public class Mesh {
    private long mNativeMeshWrapper;
    private boolean mIsIndexed;

    /**
     * Determines how the mesh is represented and will be drawn.
     */
    @IntDef({TRIANGLES, TRIANGLE_STRIP})
    private @interface Mode {}

    /**
     * The mesh will be drawn with triangles without utilizing shared vertices.
     */
    public static final int TRIANGLES = 0;

    /**
     * The mesh will be drawn with triangles utilizing shared vertices.
     */
    public static final int TRIANGLE_STRIP = 1;

    private static class MeshHolder {
        public static final NativeAllocationRegistry MESH_SPECIFICATION_REGISTRY =
                NativeAllocationRegistry.createMalloced(
                        MeshSpecification.class.getClassLoader(), nativeGetFinalizer());
    }

    /**
     * Constructor for a non-indexed Mesh.
     *
     * @param meshSpec     {@link MeshSpecification} used when generating the mesh.
     * @param mode         Determines what mode to draw the mesh in. Must be one of
     *                     {@link Mesh#TRIANGLES} or {@link Mesh#TRIANGLE_STRIP}
     * @param vertexBuffer vertex buffer representing through {@link Buffer}. This provides the data
     *                     for all attributes provided within the meshSpec for every vertex. That
     *                     is, a vertex buffer should be (attributes size * number of vertices) in
     *                     length to be valid. Note that currently implementation will have a CPU
     *                     backed buffer generated.
     * @param vertexCount  the number of vertices represented in the vertexBuffer and mesh.
     * @param bounds       bounds of the mesh object.
     */
    public Mesh(@NonNull MeshSpecification meshSpec, @Mode int mode,
            @NonNull Buffer vertexBuffer, int vertexCount, @NonNull RectF bounds) {
        if (mode != TRIANGLES && mode != TRIANGLE_STRIP) {
            throw new IllegalArgumentException("Invalid value passed in for mode parameter");
        }
        long nativeMesh = nativeMake(meshSpec.mNativeMeshSpec, mode, vertexBuffer,
                vertexBuffer.isDirect(), vertexCount, vertexBuffer.position(), bounds.left,
                bounds.top, bounds.right, bounds.bottom);
        if (nativeMesh == 0) {
            throw new IllegalArgumentException("Mesh construction failed.");
        }

        meshSetup(nativeMesh, false);
    }

    /**
     * Constructor for an indexed Mesh.
     *
     * @param meshSpec     {@link MeshSpecification} used when generating the mesh.
     * @param mode         Determines what mode to draw the mesh in. Must be one of
     *                     {@link Mesh#TRIANGLES} or {@link Mesh#TRIANGLE_STRIP}
     * @param vertexBuffer vertex buffer representing through {@link Buffer}. This provides the data
     *                     for all attributes provided within the meshSpec for every vertex. That
     *                     is, a vertex buffer should be (attributes size * number of vertices) in
     *                     length to be valid. Note that currently implementation will have a CPU
     *                     backed buffer generated.
     * @param vertexCount  the number of vertices represented in the vertexBuffer and mesh.
     * @param indexBuffer  index buffer representing through {@link ShortBuffer}. Indices are
     *                     required to be 16 bits, so ShortBuffer is necessary. Note that
     *                     currently implementation will have a CPU
     *                     backed buffer generated.
     * @param bounds       bounds of the mesh object.
     */
    public Mesh(@NonNull MeshSpecification meshSpec, @Mode int mode,
            @NonNull Buffer vertexBuffer, int vertexCount, @NonNull ShortBuffer indexBuffer,
            @NonNull RectF bounds) {
        if (mode != TRIANGLES && mode != TRIANGLE_STRIP) {
            throw new IllegalArgumentException("Invalid value passed in for mode parameter");
        }
        long nativeMesh = nativeMakeIndexed(meshSpec.mNativeMeshSpec, mode, vertexBuffer,
                vertexBuffer.isDirect(), vertexCount, vertexBuffer.position(), indexBuffer,
                indexBuffer.isDirect(), indexBuffer.capacity(), indexBuffer.position(), bounds.left,
                bounds.top, bounds.right, bounds.bottom);
        if (nativeMesh == 0) {
            throw new IllegalArgumentException("Mesh construction failed.");
        }

        meshSetup(nativeMesh, true);
    }

    /**
     * Sets the uniform color value corresponding to the shader assigned to the mesh. If the shader
     * does not have a uniform with that name or if the uniform is declared with a type other than
     * vec3 or vec4 and corresponding layout(color) annotation then an IllegalArgumentExcepton is
     * thrown.
     *
     * @param uniformName name matching the color uniform declared in the shader program.
     * @param color       the provided sRGB color will be converted into the shader program's output
     *                    colorspace and be available as a vec4 uniform in the program.
     */
    public void setColorUniform(@NonNull String uniformName, @ColorInt int color) {
        setUniform(uniformName, Color.valueOf(color).getComponents(), true);
    }

    /**
     * Sets the uniform color value corresponding to the shader assigned to the mesh. If the shader
     * does not have a uniform with that name or if the uniform is declared with a type other than
     * vec3 or vec4 and corresponding layout(color) annotation then an IllegalArgumentExcepton is
     * thrown.
     *
     * @param uniformName name matching the color uniform declared in the shader program.
     * @param color       the provided sRGB color will be converted into the shader program's output
     *                    colorspace and be available as a vec4 uniform in the program.
     */
    public void setColorUniform(@NonNull String uniformName, @ColorLong long color) {
        Color exSRGB = Color.valueOf(color).convert(ColorSpace.get(ColorSpace.Named.EXTENDED_SRGB));
        setUniform(uniformName, exSRGB.getComponents(), true);
    }

    /**
     * Sets the uniform color value corresponding to the shader assigned to the mesh. If the shader
     * does not have a uniform with that name or if the uniform is declared with a type other than
     * vec3 or vec4 and corresponding layout(color) annotation then an IllegalArgumentExcepton is
     * thrown.
     *
     * @param uniformName name matching the color uniform declared in the shader program.
     * @param color       the provided sRGB color will be converted into the shader program's output
     *                    colorspace and will be made available as a vec4 uniform in the program.
     */
    public void setColorUniform(@NonNull String uniformName, @NonNull Color color) {
        if (color == null) {
            throw new NullPointerException("The color parameter must not be null");
        }

        Color exSRGB = color.convert(ColorSpace.get(ColorSpace.Named.EXTENDED_SRGB));
        setUniform(uniformName, exSRGB.getComponents(), true);
    }

    /**
     * Sets the uniform value corresponding to the shader assigned to the mesh. If the shader does
     * not have a uniform with that name or if the uniform is declared with a type other than a
     * float or float[1] then an IllegalArgumentException is thrown.
     *
     * @param uniformName name matching the float uniform declared in the shader program.
     * @param value       float value corresponding to the float uniform with the given name.
     */
    public void setFloatUniform(@NonNull String uniformName, float value) {
        setFloatUniform(uniformName, value, 0.0f, 0.0f, 0.0f, 1);
    }

    /**
     * Sets the uniform value corresponding to the shader assigned to the mesh. If the shader does
     * not have a uniform with that name or if the uniform is declared with a type other than a
     * vec2 or float[2] then an IllegalArgumentException is thrown.
     *
     * @param uniformName name matching the float uniform declared in the shader program.
     * @param value1      first float value corresponding to the float uniform with the given name.
     * @param value2      second float value corresponding to the float uniform with the given name.
     */
    public void setFloatUniform(@NonNull String uniformName, float value1, float value2) {
        setFloatUniform(uniformName, value1, value2, 0.0f, 0.0f, 2);
    }

    /**
     * Sets the uniform value corresponding to the shader assigned to the mesh. If the shader does
     * not have a uniform with that name or if the uniform is declared with a type other than a
     * vec3 or float[3] then an IllegalArgumentException is thrown.
     *
     * @param uniformName name matching the float uniform declared in the shader program.
     * @param value1      first float value corresponding to the float uniform with the given name.
     * @param value2      second float value corresponding to the float uniform with the given name.
     * @param value3      third float value corresponding to the float unifiform with the given
     *                    name.
     */
    public void setFloatUniform(
            @NonNull String uniformName, float value1, float value2, float value3) {
        setFloatUniform(uniformName, value1, value2, value3, 0.0f, 3);
    }

    /**
     * Sets the uniform value corresponding to the shader assigned to the mesh. If the shader does
     * not have a uniform with that name or if the uniform is declared with a type other than a
     * vec4 or float[4] then an IllegalArgumentException is thrown.
     *
     * @param uniformName name matching the float uniform declared in the shader program.
     * @param value1      first float value corresponding to the float uniform with the given name.
     * @param value2      second float value corresponding to the float uniform with the given name.
     * @param value3      third float value corresponding to the float uniform with the given name.
     * @param value4      fourth float value corresponding to the float uniform with the given name.
     */
    public void setFloatUniform(
            @NonNull String uniformName, float value1, float value2, float value3, float value4) {
        setFloatUniform(uniformName, value1, value2, value3, value4, 4);
    }

    /**
     * Sets the uniform value corresponding to the shader assigned to the mesh. If the shader does
     * not have a uniform with that name or if the uniform is declared with a type other than a
     * float (for N=1), vecN, or float[N], where N is the length of the values param, then an
     * IllegalArgumentException is thrown.
     *
     * @param uniformName name matching the float uniform declared in the shader program.
     * @param values      float value corresponding to the vec4 float uniform with the given name.
     */
    public void setFloatUniform(@NonNull String uniformName, @NonNull float[] values) {
        setUniform(uniformName, values, false);
    }

    private void setFloatUniform(
            String uniformName, float value1, float value2, float value3, float value4, int count) {
        if (uniformName == null) {
            throw new NullPointerException("The uniformName parameter must not be null");
        }
        nativeUpdateUniforms(
                mNativeMeshWrapper, uniformName, value1, value2, value3, value4, count);
    }

    private void setUniform(String uniformName, float[] values, boolean isColor) {
        if (uniformName == null) {
            throw new NullPointerException("The uniformName parameter must not be null");
        }
        if (values == null) {
            throw new NullPointerException("The uniform values parameter must not be null");
        }

        nativeUpdateUniforms(mNativeMeshWrapper, uniformName, values, isColor);
    }

    /**
     * Sets the uniform value corresponding to the shader assigned to the mesh. If the shader does
     * not have a uniform with that name or if the uniform is declared with a type other than int
     * or int[1] then an IllegalArgumentException is thrown.
     *
     * @param uniformName name matching the int uniform delcared in the shader program.
     * @param value       value corresponding to the int uniform with the given name.
     */
    public void setIntUniform(@NonNull String uniformName, int value) {
        setIntUniform(uniformName, value, 0, 0, 0, 1);
    }

    /**
     * Sets the uniform value corresponding to the shader assigned to the mesh. If the shader does
     * not have a uniform with that name or if the uniform is declared with a type other than ivec2
     * or int[2] then an IllegalArgumentException is thrown.
     *
     * @param uniformName name matching the int uniform delcared in the shader program.
     * @param value1      first value corresponding to the int uniform with the given name.
     * @param value2      second value corresponding to the int uniform with the given name.
     */
    public void setIntUniform(@NonNull String uniformName, int value1, int value2) {
        setIntUniform(uniformName, value1, value2, 0, 0, 2);
    }

    /**
     * Sets the uniform value corresponding to the shader assigned to the mesh. If the shader does
     * not have a uniform with that name or if the uniform is declared with a type other than ivec3
     * or int[3] then an IllegalArgumentException is thrown.
     *
     * @param uniformName name matching the int uniform delcared in the shader program.
     * @param value1      first value corresponding to the int uniform with the given name.
     * @param value2      second value corresponding to the int uniform with the given name.
     * @param value3      third value corresponding to the int uniform with the given name.
     */
    public void setIntUniform(@NonNull String uniformName, int value1, int value2, int value3) {
        setIntUniform(uniformName, value1, value2, value3, 0, 3);
    }

    /**
     * Sets the uniform value corresponding to the shader assigned to the mesh. If the shader does
     * not have a uniform with that name or if the uniform is declared with a type other than ivec4
     * or int[4] then an IllegalArgumentException is thrown.
     *
     * @param uniformName name matching the int uniform delcared in the shader program.
     * @param value1      first value corresponding to the int uniform with the given name.
     * @param value2      second value corresponding to the int uniform with the given name.
     * @param value3      third value corresponding to the int uniform with the given name.
     * @param value4      fourth value corresponding to the int uniform with the given name.
     */
    public void setIntUniform(
            @NonNull String uniformName, int value1, int value2, int value3, int value4) {
        setIntUniform(uniformName, value1, value2, value3, value4, 4);
    }

    /**
     * Sets the uniform value corresponding to the shader assigned to the mesh. If the shader does
     * not have a uniform with that name or if the uniform is declared with a type other than an
     * int (for N=1), ivecN, or int[N], where N is the length of the values param, then an
     * IllegalArgumentException is thrown.
     *
     * @param uniformName name matching the int uniform delcared in the shader program.
     * @param values      int values corresponding to the vec4 int uniform with the given name.
     */
    public void setIntUniform(@NonNull String uniformName, @NonNull int[] values) {
        if (uniformName == null) {
            throw new NullPointerException("The uniformName parameter must not be null");
        }
        if (values == null) {
            throw new NullPointerException("The uniform values parameter must not be null");
        }
        nativeUpdateUniforms(mNativeMeshWrapper, uniformName, values);
    }

    /**
     * @hide so only calls from module can utilize it
     */
    long getNativeWrapperInstance() {
        return mNativeMeshWrapper;
    }

    private void setIntUniform(
            String uniformName, int value1, int value2, int value3, int value4, int count) {
        if (uniformName == null) {
            throw new NullPointerException("The uniformName parameter must not be null");
        }

        nativeUpdateUniforms(
                mNativeMeshWrapper, uniformName, value1, value2, value3, value4, count);
    }

    private void meshSetup(long nativeMeshWrapper, boolean isIndexed) {
        mNativeMeshWrapper = nativeMeshWrapper;
        this.mIsIndexed = isIndexed;
        MeshHolder.MESH_SPECIFICATION_REGISTRY.registerNativeAllocation(this, mNativeMeshWrapper);
    }

    private static native long nativeGetFinalizer();

    private static native long nativeMake(long meshSpec, int mode, Buffer vertexBuffer,
            boolean isDirect, int vertexCount, int vertexOffset, float left, float top, float right,
            float bottom);

    private static native long nativeMakeIndexed(long meshSpec, int mode, Buffer vertexBuffer,
            boolean isVertexDirect, int vertexCount, int vertexOffset, ShortBuffer indexBuffer,
            boolean isIndexDirect, int indexCount, int indexOffset, float left, float top,
            float right, float bottom);

    private static native void nativeUpdateUniforms(long builder, String uniformName, float value1,
            float value2, float value3, float value4, int count);

    private static native void nativeUpdateUniforms(
            long builder, String uniformName, float[] values, boolean isColor);

    private static native void nativeUpdateUniforms(long builder, String uniformName, int value1,
            int value2, int value3, int value4, int count);

    private static native void nativeUpdateUniforms(long builder, String uniformName, int[] values);

}
