/*
 * Copyright 2013 Hannes Janetzek
 * Copyright 2019 schedul-xor
 *
 * This file is part of the OpenScienceMap project (http://www.opensciencemap.org).
 *
 * This program is free software: you can redistribute it and/or modify it under the
 * terms of the GNU Lesser General Public License as published by the Free Software
 * Foundation, either version 3 of the License, or (at your option) any later version.
 *
 * 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 Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License along with
 * this program. If not, see <http://www.gnu.org/licenses/>.
 */
package org.oscim.renderer;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;

import static org.oscim.backend.GLAdapter.gl;

public class GLMatrix {

    static final Logger log = LoggerFactory.getLogger(GLMatrix.class);
    private static final boolean dbg = false;

    private final long pointer;
    private final FloatBuffer buffer;

    private static final String INVALID_INPUT = "Bad Array!";

    public GLMatrix() {
        pointer = alloc();
        buffer = (getBuffer(pointer)).order(ByteOrder.nativeOrder()).asFloatBuffer();
    }

    /**
     * Set the Matrix from float array
     *
     * @param m float array to copy
     */
    public void set(float[] m) {
        if (m == null || m.length != 16)
            throw new IllegalArgumentException(INVALID_INPUT);

        set(pointer, m);
    }

    /**
     * Get the Matrix as float array
     *
     * @param m float array to store Matrix
     */
    public void get(float[] m) {
        if (m == null || m.length != 16)
            throw new IllegalArgumentException(INVALID_INPUT);

        get(pointer, m);
    }

    /**
     * Copy values from mat
     *
     * @param mat Matrix to copy
     */
    public void copy(GLMatrix mat) {
        copy(pointer, mat.pointer);
    }

    /**
     * Project Vector with Matrix
     *
     * @param vec3 Vector to project
     */
    public void prj(float[] vec3) {
        if (vec3 == null || vec3.length < 3)
            throw new IllegalArgumentException(INVALID_INPUT);

        prj(pointer, vec3);
    }

    /**
     * Project Vectors with Matrix
     *
     * @param vec3 Vector to project
     */
    public void prj3D(float[] vec3, int offset, int length) {
        if (vec3 == null || vec3.length / (offset + length) < 1)
            throw new IllegalArgumentException(INVALID_INPUT);

        prj3D(pointer, vec3, offset, length);
    }

    /**
     * Project Vectors with Matrix
     *
     * @param vec2 Vector to project
     */
    public void prj2D(float[] vec2, int offset, int length) {
        if (vec2 == null || offset < 0 || (length + offset) * 2 > vec2.length)
            throw new IllegalArgumentException(INVALID_INPUT);

        prj2D(pointer, vec2, offset, length);
    }

    /**
     * Project Vectors with Matrix
     *
     * @param vec2 Vector to project
     */
    public void prj2D(float[] src, int src_offset, float[] dst, int dst_offset, int length) {
        if (src == null || src_offset < 0 || length + src_offset * 2 > src.length)
            throw new IllegalArgumentException(INVALID_INPUT);

        prj2D2(pointer, src, src_offset, dst, dst_offset, length);
    }

    /**
     * Multiply rhs onto Matrix.
     *
     * @param rhs right hand side
     */
    public void multiplyRhs(GLMatrix rhs) {
        smulrhs(pointer, rhs.pointer);
    }

    /**
     * Use this matrix as rhs, multiply it on lhs and store result.
     *
     * @param lhs right hand side
     */
    public void multiplyLhs(GLMatrix lhs) {
        smullhs(pointer, lhs.pointer);
    }

    /**
     * Multiply rhs onto lhs and store result in Matrix.
     * <p/>
     * This matrix MUST be different from lhs and rhs!
     * <p/>
     * when combining matrices for vector projection this
     * has the same effect first as applying rhs then lhs.
     *
     * @param lhs left hand side
     * @param rhs right hand side
     */
    public void multiplyMM(GLMatrix lhs, GLMatrix rhs) {
        smul(pointer, lhs.pointer, rhs.pointer);
    }

    /**
     * Transpose mat and store result in Matrix
     *
     * @param mat to transpose
     */
    public void transposeM(GLMatrix mat) {
        strans(pointer, mat.pointer);
    }

    /**
     * Set rotation
     *
     * @param a angle in degree
     * @param x around x-axis
     * @param y around y-axis
     * @param z around z-axis
     */
    public void setRotation(float a, float x, float y, float z) {
        setRotation(pointer, a, x, y, z);
    }

    /**
     * Set translation
     *
     * @param x along x-axis
     * @param y along y-axis
     * @param z along z-axis
     */
    public void setTranslation(float x, float y, float z) {
        setTranslation(pointer, x, y, z);
    }

    /**
     * Set scale factor
     *
     * @param x axis
     * @param y axis
     * @param z axis
     */
    public void setScale(float x, float y, float z) {
        setScale(pointer, x, y, z);
    }

    /**
     * Set translation and x,y scale
     *
     * @param tx    translate x
     * @param ty    translate y
     * @param scale factor x,y
     */
    public void setTransScale(float tx, float ty, float scale) {
        setTransScale(pointer, tx, ty, scale);
    }

    /**
     * Set Matrix with glUniformMatrix
     *
     * @param location GL location id
     */
    public void setAsUniform(int location) {
        gl.uniformMatrix4fv(location, 1, false, buffer);
        //setAsUniform(pointer, location);
    }

    /**
     * Set single value
     *
     * @param pos   at position
     * @param value value to set
     */
    public void setValue(int pos, float value) {
        setValueAt(pointer, pos, value);
    }

    /**
     * add some offset (similar to glDepthOffset)
     *
     * @param delta offset
     */
    public void addDepthOffset(int delta) {
        addDepthOffset(pointer, delta);
    }

    /**
     * Set identity matrix
     */
    public void setIdentity() {
        identity(pointer);
    }

    /**
     * Free native object
     */
    @Override
    public void finalize() {
        if (pointer != 0)
            delete(pointer);
    }

    private static native long alloc();

    private static native void delete(long self);

    private static native void set(long self, float[] m);

    private static native void copy(long self, long other);

    private static native void identity(long self);

    private static native void get(long self, float[] m);

    private static native void mul(long self, long lhs_ptr);

    private static native void smul(long self, long rhs_ptr, long lhs_ptr);

    private static native void smulrhs(long self, long rhs_ptr);

    private static native void smullhs(long self, long lhs_ptr);

    private static native void strans(long self, long rhs_ptr);

    private static native void prj(long self, float[] vec3);

    private static native void prj3D(long self, float[] vec3, int start, int cnt);

    private static native void prj2D(long self, float[] vec2, int start, int cnt);

    private static native void prj2D2(long self, float[] vec2, int src_offset,
                                      float[] dst_vec, int dst_offset, int length);

    private static native void setRotation(long self, float a, float x, float y, float z);

    private static native void setScale(long self, float x, float y, float z);

    private static native void setTranslation(long self, float x, float y, float z);

    private static native void setTransScale(long self, float tx, float ty, float scale);

    //private static native void setAsUniform(long self, int handle);

    private static native void setValueAt(long self, int pos, float value);

    private static native void addDepthOffset(long self, int delta);

    private static native ByteBuffer getBuffer(long self);

    /* Copyright (C) 2007 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. */

    /**
     * Define a projection matrix in terms of six clip planes
     *
     * @param m      the float array that holds the perspective matrix
     * @param offset the offset into float array m where the perspective
     *               matrix data is written
     */
    public static void frustumM(float[] m, int offset,
                                float left, float right, float bottom, float top,
                                float near, float far) {
        if (left == right) {
            throw new IllegalArgumentException("left == right");
        }
        if (top == bottom) {
            throw new IllegalArgumentException("top == bottom");
        }
        if (near == far) {
            throw new IllegalArgumentException("near == far");
        }
        if (near <= 0.0f) {
            throw new IllegalArgumentException("near <= 0.0f");
        }
        if (far <= 0.0f) {
            throw new IllegalArgumentException("far <= 0.0f");
        }
        final float r_width = 1.0f / (right - left);
        final float r_height = 1.0f / (top - bottom);
        final float r_depth = 1.0f / (near - far);
        final float x = 2.0f * (near * r_width);
        final float y = 2.0f * (near * r_height);
        final float A = (right + left) * r_width;
        final float B = (top + bottom) * r_height;
        final float C = (far + near) * r_depth;
        final float D = 2.0f * (far * near * r_depth);
        m[offset + 0] = x;
        m[offset + 5] = y;
        m[offset + 8] = A;
        m[offset + 9] = B;
        m[offset + 10] = C;
        m[offset + 14] = D;
        m[offset + 11] = -1.0f;
        m[offset + 1] = 0.0f;
        m[offset + 2] = 0.0f;
        m[offset + 3] = 0.0f;
        m[offset + 4] = 0.0f;
        m[offset + 6] = 0.0f;
        m[offset + 7] = 0.0f;
        m[offset + 12] = 0.0f;
        m[offset + 13] = 0.0f;
        m[offset + 15] = 0.0f;
    }

    /**
     * Inverts a 4 x 4 matrix.
     *
     * @param mInv       the array that holds the output inverted matrix
     * @param mInvOffset an offset into mInv where the inverted matrix is
     *                   stored.
     * @param m          the input array
     * @param mOffset    an offset into m where the matrix is stored.
     * @return true if the matrix could be inverted, false if it could not.
     */
    public static boolean invertM(float[] mInv, int mInvOffset, float[] m,
                                  int mOffset) {
        // Invert a 4 x 4 matrix using Cramer's Rule

        // transpose matrix
        final float src0 = m[mOffset + 0];
        final float src4 = m[mOffset + 1];
        final float src8 = m[mOffset + 2];
        final float src12 = m[mOffset + 3];

        final float src1 = m[mOffset + 4];
        final float src5 = m[mOffset + 5];
        final float src9 = m[mOffset + 6];
        final float src13 = m[mOffset + 7];

        final float src2 = m[mOffset + 8];
        final float src6 = m[mOffset + 9];
        final float src10 = m[mOffset + 10];
        final float src14 = m[mOffset + 11];

        final float src3 = m[mOffset + 12];
        final float src7 = m[mOffset + 13];
        final float src11 = m[mOffset + 14];
        final float src15 = m[mOffset + 15];

        // calculate pairs for first 8 elements (cofactors)
        final float atmp0 = src10 * src15;
        final float atmp1 = src11 * src14;
        final float atmp2 = src9 * src15;
        final float atmp3 = src11 * src13;
        final float atmp4 = src9 * src14;
        final float atmp5 = src10 * src13;
        final float atmp6 = src8 * src15;
        final float atmp7 = src11 * src12;
        final float atmp8 = src8 * src14;
        final float atmp9 = src10 * src12;
        final float atmp10 = src8 * src13;
        final float atmp11 = src9 * src12;

        // calculate first 8 elements (cofactors)
        final float dst0 = (atmp0 * src5 + atmp3 * src6 + atmp4 * src7)
                - (atmp1 * src5 + atmp2 * src6 + atmp5 * src7);
        final float dst1 = (atmp1 * src4 + atmp6 * src6 + atmp9 * src7)
                - (atmp0 * src4 + atmp7 * src6 + atmp8 * src7);
        final float dst2 = (atmp2 * src4 + atmp7 * src5 + atmp10 * src7)
                - (atmp3 * src4 + atmp6 * src5 + atmp11 * src7);
        final float dst3 = (atmp5 * src4 + atmp8 * src5 + atmp11 * src6)
                - (atmp4 * src4 + atmp9 * src5 + atmp10 * src6);
        final float dst4 = (atmp1 * src1 + atmp2 * src2 + atmp5 * src3)
                - (atmp0 * src1 + atmp3 * src2 + atmp4 * src3);
        final float dst5 = (atmp0 * src0 + atmp7 * src2 + atmp8 * src3)
                - (atmp1 * src0 + atmp6 * src2 + atmp9 * src3);
        final float dst6 = (atmp3 * src0 + atmp6 * src1 + atmp11 * src3)
                - (atmp2 * src0 + atmp7 * src1 + atmp10 * src3);
        final float dst7 = (atmp4 * src0 + atmp9 * src1 + atmp10 * src2)
                - (atmp5 * src0 + atmp8 * src1 + atmp11 * src2);

        // calculate pairs for second 8 elements (cofactors)
        final float btmp0 = src2 * src7;
        final float btmp1 = src3 * src6;
        final float btmp2 = src1 * src7;
        final float btmp3 = src3 * src5;
        final float btmp4 = src1 * src6;
        final float btmp5 = src2 * src5;
        final float btmp6 = src0 * src7;
        final float btmp7 = src3 * src4;
        final float btmp8 = src0 * src6;
        final float btmp9 = src2 * src4;
        final float btmp10 = src0 * src5;
        final float btmp11 = src1 * src4;

        // calculate second 8 elements (cofactors)
        final float dst8 = (btmp0 * src13 + btmp3 * src14 + btmp4 * src15)
                - (btmp1 * src13 + btmp2 * src14 + btmp5 * src15);
        final float dst9 = (btmp1 * src12 + btmp6 * src14 + btmp9 * src15)
                - (btmp0 * src12 + btmp7 * src14 + btmp8 * src15);
        final float dst10 = (btmp2 * src12 + btmp7 * src13 + btmp10 * src15)
                - (btmp3 * src12 + btmp6 * src13 + btmp11 * src15);
        final float dst11 = (btmp5 * src12 + btmp8 * src13 + btmp11 * src14)
                - (btmp4 * src12 + btmp9 * src13 + btmp10 * src14);
        final float dst12 = (btmp2 * src10 + btmp5 * src11 + btmp1 * src9)
                - (btmp4 * src11 + btmp0 * src9 + btmp3 * src10);
        final float dst13 = (btmp8 * src11 + btmp0 * src8 + btmp7 * src10)
                - (btmp6 * src10 + btmp9 * src11 + btmp1 * src8);
        final float dst14 = (btmp6 * src9 + btmp11 * src11 + btmp3 * src8)
                - (btmp10 * src11 + btmp2 * src8 + btmp7 * src9);
        final float dst15 = (btmp10 * src10 + btmp4 * src8 + btmp9 * src9)
                - (btmp8 * src9 + btmp11 * src10 + btmp5 * src8);

        // calculate determinant
        final float det =
                src0 * dst0 + src1 * dst1 + src2 * dst2 + src3 * dst3;

        if (det == 0.0f) {
            return false;
        }

        // calculate matrix inverse
        final float invdet = 1.0f / det;
        mInv[mInvOffset] = dst0 * invdet;
        mInv[1 + mInvOffset] = dst1 * invdet;
        mInv[2 + mInvOffset] = dst2 * invdet;
        mInv[3 + mInvOffset] = dst3 * invdet;

        mInv[4 + mInvOffset] = dst4 * invdet;
        mInv[5 + mInvOffset] = dst5 * invdet;
        mInv[6 + mInvOffset] = dst6 * invdet;
        mInv[7 + mInvOffset] = dst7 * invdet;

        mInv[8 + mInvOffset] = dst8 * invdet;
        mInv[9 + mInvOffset] = dst9 * invdet;
        mInv[10 + mInvOffset] = dst10 * invdet;
        mInv[11 + mInvOffset] = dst11 * invdet;

        mInv[12 + mInvOffset] = dst12 * invdet;
        mInv[13 + mInvOffset] = dst13 * invdet;
        mInv[14 + mInvOffset] = dst14 * invdet;
        mInv[15 + mInvOffset] = dst15 * invdet;

        return true;
    }

    public static void lookAt(float[] m, int offset,
                              float eyex, float eyey, float eyez,
                              float centerx, float centery, float centerz,
                              float upx, float upy, float upz) {
        float z0 = eyex - centerx;
        float z1 = eyey - centery;
        float z2 = eyez - centerz;
        double len = 1.0 / Math.sqrt(z0 * z0 + z1 * z1 + z2 * z2);
        z0 *= len;
        z1 *= len;
        z2 *= len;

        float x0 = upy * z2 - upz * z1;
        float x1 = upz * z0 - upx * z2;
        float x2 = upx * z1 - upy * z0;
        len = Math.sqrt(x0 * x0 + x1 * x1 + x2 * x2);
        if (len == 0) {
            x0 = x1 = x2 = 0;
        } else {
            len = 1.0 / len;
            x0 *= len;
            x1 *= len;
            x2 *= len;
        }

        float y0 = z1 * x2 - z2 * x1;
        float y1 = z2 * x0 - z0 * x2;
        float y2 = z0 * x1 - z1 * x0;
        len = Math.sqrt(y0 * y0 + y1 * y1 + y2 * y2);
        if (len == 0) {
            y0 = y1 = y2 = 0;
        } else {
            len = 1.0 / len;
            y0 *= len;
            y1 *= len;
            y2 *= len;
        }

        m[offset + 0] = x0;
        m[offset + 1] = y0;
        m[offset + 2] = z0;
        m[offset + 3] = 0.0f;
        m[offset + 4] = x1;
        m[offset + 5] = y1;
        m[offset + 6] = z1;
        m[offset + 7] = 0.0f;
        m[offset + 8] = x2;
        m[offset + 9] = y2;
        m[offset + 10] = z2;
        m[offset + 11] = 0.0f;
        m[offset + 12] = -(x0 * eyex + x1 * eyey + x2 * eyez);
        m[offset + 13] = -(y0 * eyex + y1 * eyey + y2 * eyez);
        m[offset + 14] = -(z0 * eyex + z1 * eyey + z2 * eyez);
        m[offset + 15] = 1.0f;
    }

    public static void orthoM(float[] m, int offset,
                              float left, float right, float bottom, float top,
                              float near, float far) {
        final float r_width = 1.0f / (left - right);
        final float r_height = 1.0f / (bottom - top);
        final float r_depth = 1.0f / (near - far);
        m[offset + 0] = -2.0f * r_width;
        m[offset + 5] = -2.0f * r_height;
        m[offset + 10] = 2.0f * r_depth;
        m[offset + 12] = (left + right) * r_width;
        m[offset + 13] = (top + bottom) * r_height;
        m[offset + 14] = (far + near) * r_depth;
        m[offset + 1] = 0.0f;
        m[offset + 2] = 0.0f;
        m[offset + 3] = 0.0f;
        m[offset + 4] = 0.0f;
        m[offset + 6] = 0.0f;
        m[offset + 7] = 0.0f;
        m[offset + 8] = 0.0f;
        m[offset + 9] = 0.0f;
        m[offset + 11] = 0.0f;
        m[offset + 15] = 1.0f;
    }
}
