/**
 * Copyright (C) 2011 JogAmp Community. All rights reserved.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included
 * in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
 * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
 * BRIAN PAUL BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
 * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */
package com.jogamp.opengl.demos;

import java.nio.FloatBuffer;

import com.jogamp.opengl.GL;

import com.jogamp.common.nio.Buffers;
import com.jogamp.opengl.util.GLArrayDataServer;

/**
 * GearsObject.java <BR>
 * @author Brian Paul (converted to Java by Ron Cemer and Sven Gothel) <P>
 */
public abstract class GearsObject {
    public static final FloatBuffer red = Buffers.newDirectFloatBuffer( new float[] { 0.8f, 0.1f, 0.0f, 0.7f } );
    public static final FloatBuffer green = Buffers.newDirectFloatBuffer( new float[] { 0.0f, 0.8f, 0.2f, 0.7f } );
    public static final FloatBuffer blue = Buffers.newDirectFloatBuffer( new float[] { 0.2f, 0.2f, 1.0f, 0.7f } );
    public static final float M_PI = (float)Math.PI;

    public final FloatBuffer gearColor;
    public GLArrayDataServer frontFace;
    public GLArrayDataServer frontSide;
    public GLArrayDataServer backFace;
    public GLArrayDataServer backSide;
    public GLArrayDataServer outwardFace;
    public GLArrayDataServer insideRadiusCyl;
    public boolean isShared;
    protected boolean validateBuffers = false;

    public abstract GLArrayDataServer createInterleaved(boolean useMappedBuffers, int comps, int dataType, boolean normalized, int initialSize, int vboUsage);
    public abstract void addInterleavedVertexAndNormalArrays(GLArrayDataServer array, int components);
    public abstract void draw(GL gl, float x, float y, float angle);

    private GLArrayDataServer createInterleavedClone(final GLArrayDataServer ads) {
      final GLArrayDataServer n = new GLArrayDataServer(ads);
      n.setInterleavedOffset(0);
      return n;
    }

    private void init(final GL gl, final GLArrayDataServer array) {
        array.enableBuffer(gl, true);
        array.enableBuffer(gl, false);
    }

    public void destroy(final GL gl) {
        if(!isShared) {
            // could be already destroyed by shared configuration
            if(null != frontFace) {
                frontFace.destroy(gl);
            }
            if(null != frontSide) {
                frontSide.destroy(gl);
            }
            if(null != backFace) {
                backFace.destroy(gl);
            }
            if(null != backSide) {
                backSide.destroy(gl);
            }
            if(null != outwardFace) {
                outwardFace.destroy(gl);
            }
            if(null != insideRadiusCyl) {
                insideRadiusCyl.destroy(gl);
            }
        }
        frontFace=null;
        frontSide=null;
        backFace=null;
        backSide=null;
        outwardFace=null;
        insideRadiusCyl=null;
        isShared = false;
    }

    public GearsObject ( final GearsObject shared ) {
        isShared = true;
        validateBuffers = shared.validateBuffers;
        frontFace = createInterleavedClone(shared.frontFace);
        addInterleavedVertexAndNormalArrays(frontFace, 3);
        backFace = createInterleavedClone(shared.backFace);
        addInterleavedVertexAndNormalArrays(backFace, 3);
        frontSide = createInterleavedClone(shared.frontSide);
        addInterleavedVertexAndNormalArrays(frontSide, 3);
        backSide= createInterleavedClone(shared.backSide);
        addInterleavedVertexAndNormalArrays(backSide, 3);
        outwardFace = createInterleavedClone(shared.outwardFace);
        addInterleavedVertexAndNormalArrays(outwardFace, 3);
        insideRadiusCyl = createInterleavedClone(shared.insideRadiusCyl);
        addInterleavedVertexAndNormalArrays(insideRadiusCyl, 3);
        gearColor = shared.gearColor;
    }

    public GearsObject (
            final GL gl,
            final boolean useMappedBuffers,
            final FloatBuffer gearColor,
            final float inner_radius,
            final float outer_radius,
            final float width, final int teeth, final float tooth_depth, final boolean validateBuffers)
    {
        final float dz = width * 0.5f;
        int i;
        float r0, r1, r2;
        float angle, da;
        float u, v, len;
        final float s[] = new float[5];
        final float c[] = new float[5];
        final float normal[] = new float[3];
        // final int tris_per_tooth = 32;

        this.validateBuffers = validateBuffers;
        this.isShared = false;
        this.gearColor = gearColor;

        r0 = inner_radius;
        r1 = outer_radius - tooth_depth / 2.0f;
        r2 = outer_radius + tooth_depth / 2.0f;

        da = 2.0f * (float) Math.PI / teeth / 4.0f;

        s[4] = 0; // sin(0f)
        c[4] = 1; // cos(0f)

        final int vboUsage = GL.GL_STATIC_DRAW;

        frontFace = createInterleaved(useMappedBuffers, 6, GL.GL_FLOAT, false, 4*teeth+2, vboUsage);
        addInterleavedVertexAndNormalArrays(frontFace, 3);
        backFace = createInterleaved(useMappedBuffers, 6, GL.GL_FLOAT, false, 4*teeth+2, vboUsage);
        addInterleavedVertexAndNormalArrays(backFace, 3);
        frontSide = createInterleaved(useMappedBuffers, 6, GL.GL_FLOAT, false, 6*teeth, vboUsage);
        addInterleavedVertexAndNormalArrays(frontSide, 3);
        backSide = createInterleaved(useMappedBuffers, 6, GL.GL_FLOAT, false, 6*teeth, vboUsage);
        addInterleavedVertexAndNormalArrays(backSide, 3);
        outwardFace = createInterleaved(useMappedBuffers, 6, GL.GL_FLOAT, false, 4*4*teeth+2, vboUsage);
        addInterleavedVertexAndNormalArrays(outwardFace, 3);
        insideRadiusCyl = createInterleaved(useMappedBuffers, 6, GL.GL_FLOAT, false, 2*teeth+2, vboUsage);
        addInterleavedVertexAndNormalArrays(insideRadiusCyl, 3);

        if( useMappedBuffers ) {
            frontFace.mapStorage(gl, GL.GL_WRITE_ONLY);
            backFace.mapStorage(gl, GL.GL_WRITE_ONLY);
            frontSide.mapStorage(gl, GL.GL_WRITE_ONLY);
            backSide.mapStorage(gl, GL.GL_WRITE_ONLY);
            outwardFace.mapStorage(gl, GL.GL_WRITE_ONLY);
            insideRadiusCyl.mapStorage(gl, GL.GL_WRITE_ONLY);
        }

        for (i = 0; i < teeth; i++) {
            angle = i * 2.0f * M_PI / teeth;
            sincos(angle + da * 0f, s, 0, c, 0);
            sincos(angle + da * 1f, s, 1, c, 1);
            sincos(angle + da * 2f, s, 2, c, 2);
            sincos(angle + da * 3f, s, 3, c, 3);

            /* front  */
            normal[0] = 0.0f;
            normal[1] = 0.0f;
            normal[2] = 1.0f;

            /* front face - GL.GL_TRIANGLE_STRIP */
            vert(frontFace, r0 * c[0], r0 * s[0], dz, normal);
            vert(frontFace, r1 * c[0], r1 * s[0], dz, normal);
            vert(frontFace, r0 * c[0], r0 * s[0], dz, normal);
            vert(frontFace, r1 * c[3], r1 * s[3], dz, normal);

            /* front sides of teeth - GL.GL_TRIANGLES */
            vert(frontSide, r1 * c[0], r1 * s[0], dz, normal);
            vert(frontSide, r2 * c[1], r2 * s[1], dz, normal);
            vert(frontSide, r2 * c[2], r2 * s[2], dz, normal);
            vert(frontSide, r1 * c[0], r1 * s[0], dz, normal);
            vert(frontSide, r2 * c[2], r2 * s[2], dz, normal);
            vert(frontSide, r1 * c[3], r1 * s[3], dz, normal);

            /* back */
            normal[0] = 0.0f;
            normal[1] = 0.0f;
            normal[2] = -1.0f;

            /* back face - GL.GL_TRIANGLE_STRIP */
            vert(backFace, r1 * c[0], r1 * s[0], -dz, normal);
            vert(backFace, r0 * c[0], r0 * s[0], -dz, normal);
            vert(backFace, r1 * c[3], r1 * s[3], -dz, normal);
            vert(backFace, r0 * c[0], r0 * s[0], -dz, normal);

            /* back sides of teeth - GL.GL_TRIANGLES*/
            vert(backSide, r1 * c[3], r1 * s[3], -dz, normal);
            vert(backSide, r2 * c[2], r2 * s[2], -dz, normal);
            vert(backSide, r2 * c[1], r2 * s[1], -dz, normal);
            vert(backSide, r1 * c[3], r1 * s[3], -dz, normal);
            vert(backSide, r2 * c[1], r2 * s[1], -dz, normal);
            vert(backSide, r1 * c[0], r1 * s[0], -dz, normal);

            /* outward faces of teeth */
            u = r2 * c[1] - r1 * c[0];
            v = r2 * s[1] - r1 * s[0];
            len = (float)Math.sqrt(u * u + v * v);
            u /= len;
            v /= len;
            normal[0] =    v;
            normal[1] =   -u;
            normal[2] = 0.0f;

            vert(outwardFace, r1 * c[0], r1 * s[0],  dz, normal);
            vert(outwardFace, r1 * c[0], r1 * s[0], -dz, normal);
            vert(outwardFace, r2 * c[1], r2 * s[1],  dz, normal);
            vert(outwardFace, r2 * c[1], r2 * s[1], -dz, normal);

            normal[0] = c[0];
            normal[1] = s[0];
            vert(outwardFace, r2 * c[1], r2 * s[1],  dz, normal);
            vert(outwardFace, r2 * c[1], r2 * s[1], -dz, normal);
            vert(outwardFace, r2 * c[2], r2 * s[2],  dz, normal);
            vert(outwardFace, r2 * c[2], r2 * s[2], -dz, normal);

            normal[0] = ( r1 * s[3] - r2 * s[2] );
            normal[1] = ( r1 * c[3] - r2 * c[2] ) * -1.0f ;
            vert(outwardFace, r2 * c[2], r2 * s[2],  dz, normal);
            vert(outwardFace, r2 * c[2], r2 * s[2], -dz, normal);
            vert(outwardFace, r1 * c[3], r1 * s[3],  dz, normal);
            vert(outwardFace, r1 * c[3], r1 * s[3], -dz, normal);

            normal[0] = c[0];
            normal[1] = s[0];
            vert(outwardFace, r1 * c[3], r1 * s[3],  dz, normal);
            vert(outwardFace, r1 * c[3], r1 * s[3], -dz, normal);
            vert(outwardFace, r1 * c[0], r1 * s[0],  dz, normal);
            vert(outwardFace, r1 * c[0], r1 * s[0], -dz, normal);

            /* inside radius cylinder */
            normal[0] = c[0] * -1.0f;
            normal[1] = s[0] * -1.0f;
            normal[2] = 0.0f;
            vert(insideRadiusCyl, r0 * c[0], r0 * s[0], -dz, normal);
            vert(insideRadiusCyl, r0 * c[0], r0 * s[0],  dz, normal);
        }
        /* finish front face */
        normal[0] = 0.0f;
        normal[1] = 0.0f;
        normal[2] = 1.0f;
        vert(frontFace, r0 * c[4], r0 * s[4], dz, normal);
        vert(frontFace, r1 * c[4], r1 * s[4], dz, normal);
        frontFace.seal(true);

        /* finish back face */
        normal[2] = -1.0f;
        vert(backFace, r1 * c[4], r1 * s[4], -dz, normal);
        vert(backFace, r0 * c[4], r0 * s[4], -dz, normal);
        backFace.seal(true);

        backSide.seal(true);
        frontSide.seal(true);

        /* finish outward face */
        sincos(da * 1f, s, 1, c, 1);
        u = r2 * c[1] - r1 * c[4];
        v = r2 * s[1] - r1 * s[4];
        len = (float)Math.sqrt(u * u + v * v);
        u /= len;
        v /= len;
        normal[0] =    v;
        normal[1] =   -u;
        normal[2] = 0.0f;
        vert(outwardFace, r1 * c[4], r1 * s[4],  dz, normal);
        vert(outwardFace, r1 * c[4], r1 * s[4], -dz, normal);
        outwardFace.seal(true);

        /* finish inside radius cylinder */
        normal[0] = c[4] * -1.0f;
        normal[1] = s[4] * -1.0f;
        normal[2] = 0.0f;
        vert(insideRadiusCyl, r0 * c[4], r0 * s[4], -dz, normal);
        vert(insideRadiusCyl, r0 * c[4], r0 * s[4],  dz, normal);
        insideRadiusCyl.seal(true);

        if( useMappedBuffers ) {
            frontFace.unmapStorage(gl);
            backFace.unmapStorage(gl);
            frontSide.unmapStorage(gl);
            backSide.unmapStorage(gl);
            outwardFace.unmapStorage(gl);
            insideRadiusCyl.unmapStorage(gl);
        } else {
            /** Init VBO and data .. */
            init(gl, frontFace);
            init(gl, frontSide);
            init(gl, backFace);
            init(gl, backSide);
            init(gl, outwardFace);
            init(gl, insideRadiusCyl);
        }
    }

    @Override
    public String toString() {
        final int ffVBO = null != frontFace ? frontFace.getVBOName() : 0;
        final int fsVBO = null != frontSide ? frontSide.getVBOName() : 0;
        final int bfVBO = null != backFace ? backFace.getVBOName() : 0;
        final int bsVBO = null != backSide ? backSide.getVBOName() : 0;
        return "GearsObj[0x"+Integer.toHexString(hashCode())+", vbo ff "+ffVBO+", fs "+fsVBO+", bf "+bfVBO+", bs "+bsVBO+"]";
    }

    static void vert(final GLArrayDataServer array, final float x, final float y, final float z, final float n[]) {
        array.putf(x);
        array.putf(y);
        array.putf(z);
        array.putf(n[0]);
        array.putf(n[1]);
        array.putf(n[2]);
    }

    static void sincos(final float x, final float sin[], final int sinIdx, final float cos[], final int cosIdx) {
        sin[sinIdx] = (float) Math.sin(x);
        cos[cosIdx] = (float) Math.cos(x);
    }
}
