package com.example.opengles3;

import android.os.Debug;
import android.util.Log;

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

class MT3D {
    public float [][] _matrica;
    float [][] _kamera;
    float [][] _projekcija;
    float [][] _transformacije;
    float [][] _pamtiSkaliranje;
    boolean _skaliranje;
	public MT3D() {
		this._matrica = new float[][]{{1, 0, 0, 0}, {0, 1, 0, 0}, {0, 0, 1, 0}, {0, 0, 0, 1}};
		this._kamera = new float[][]{{1, 0, 0, 0}, {0, 1, 0, 0}, {0, 0, 1, 0}, {0, 0, 0, 1}};
		this._projekcija = new float[][]{{1, 0, 0, 0}, {0, 1, 0, 0}, {0, 0, 1, 0}, {0, 0, 0, 1}};
		this._transformacije = new float[4][4];
		this._pamtiSkaliranje = new float[4][4];
		this._skaliranje = false; //da li je primijenjeno skaliranje; olaksava racunanje matrice za transformaciju normale
	}

	//pretvori kamera*matrica u polje
    FloatBuffer poljeModel() {
        float[][] rez = this.mnoziMatrice(this._kamera, this._matrica);
        float[] v = new float[16];
        int brojac = 0;
        for(int i = 0; i < 4; i++) {
          for (int j = 0; j < 4; j++) {
            v[brojac] = rez[j][i];
            brojac++;
          }
        }
        ByteBuffer bb = ByteBuffer.allocateDirect(v.length * 4);
        bb.order(ByteOrder.nativeOrder());
        FloatBuffer zaVratiti = bb.asFloatBuffer();
        zaVratiti.put(v);
        zaVratiti.position(0);
        return zaVratiti;
    }

    //pretvori matricu kamere u polje
    FloatBuffer poljeKamera() {
        float[] v = new float[16];
        int brojac = 0;
        for(int i = 0; i < 4; i++) {
          for (int j = 0; j < 4; j++) {
              v[brojac] = this._kamera[j][i];
              brojac++;
          }
        }
        ByteBuffer bb = ByteBuffer.allocateDirect(v.length * 4);
        bb.order(ByteOrder.nativeOrder());
        FloatBuffer zaVratiti = bb.asFloatBuffer();
        zaVratiti.put(v);
        zaVratiti.position(0);
        return zaVratiti;
    }

    //pretvori projekcija*kamera*matrica u polje
    public FloatBuffer poljeMatrica() {
        float[][] rez = this.mnoziMatrice(this._projekcija, this.mnoziMatrice(this._kamera, this._matrica));
        float[] v = new float[16];
        int brojac = 0;
        for(int i = 0; i < 4; i++) {
          for (int j = 0; j < 4; j++) {
              v[brojac] = rez[j][i];
              brojac++;
          }
        }
        ByteBuffer bb = ByteBuffer.allocateDirect(v.length * 4);
        bb.order(ByteOrder.nativeOrder());
        FloatBuffer zaVratiti = bb.asFloatBuffer();
        zaVratiti.put(v);
        zaVratiti.position(0);
        return zaVratiti;
    }

    //pretvori matricu za transformaciju normala u polje
    float[] poljeNormala(){
        float[][] m = new float[4][4];
        float[][] matNormala;
        float[] v = new float[16];
        float[][] rez = this.mnoziMatrice(this._kamera, this._matrica);
        //izvuci 3x3 podmatricu
        for (int i = 0; i < 3; i++) {
          //m[i] = [];
          for (int j = 0; j < 3; j++) {
            m[i][j] = rez[i][j];
          }
        }
        if (!this._skaliranje) matNormala = m;
        else {
            float det = 1 / (m[0][0]*m[1][1]*m[2][2] + m[0][1]*m[1][2]*m[2][0] + m[0][2]*m[1][0]*m[2][1] -
                    m[2][0]*m[1][1]*m[0][2] - m[2][1]*m[1][2]*m[0][0] - m[2][2]*m[1][0]*m[0][1]);
            float M00 = det*(m[1][1]*m[2][2]-m[2][1]*m[1][2]);
            float M01 = det*(m[2][0]*m[1][2]-m[1][0]*m[2][2]);
            float M02 = det*(m[1][0]*m[2][1]-m[1][1]*m[2][0]);
            float M10 = det*(m[0][2]*m[2][1]-m[0][1]*m[2][2]);
            float M11 = det*(m[0][0]*m[2][2]-m[0][2]*m[2][0]);
            float M12 = det*(m[0][1]*m[2][0]-m[0][0]*m[2][1]);
            float M20 = det*(m[0][1]*m[1][2]-m[1][1]*m[0][2]);
            float M21 = det*(m[0][2]*m[1][0]-m[0][0]*m[1][2]);
            float M22 = det*(m[0][0]*m[1][1]-m[0][1]*m[1][0]);
          matNormala = new float[][]{{M00, M01, M02}, {M10, M11, M12}, {M20, M21, M22}};
        }
        int brojac = 0;
        for(int i = 0; i < 3; i++) {
          for (int j = 0; j < 3; j++) {
            v[brojac]=(matNormala[j][i]);
            brojac++;
          }
        }
        return v;
    }

    //pretvori matricu za transformaciju normala u polje prikladno za Uniform buffer (webgl2)
    /*
    float[] poljeNormalaBuffer() {
    	var v = this.poljeNormala();
        v.splice(3,0,0);
        v.splice(7,0,0);
        v.push(0);
        return v; 
    }*/

    //spremi trenutnu transformaciju na stog
    /*
    spremiMatricu() {
    	if (this._transformacije.length > 32) {
            Log.d("FullStack","Error: stack is full.");
    	} else {
    		this._transformacije.push(this._matrica);
    		this._pamtiSkaliranje.push(this._skaliranje);
    	}
    }

    //vrati zadnje spremljenu transformaciju sa stoga
    vratiMatricu() {
    	if (this._transformacije.length == 0) {
    		console.log("Error: stack is empty.");
    	} else {
    		this._matrica = this._transformacije.pop();
    		this._skaliranje = this._pamtiSkaliranje.pop();
    	}
    }
    */
    //mnozenje matrica
    float [][] mnoziMatrice(float [][] m1, float [][] m2) {
        float [][] rez = new float[][]{{0,0,0,0},{0,0,0,0},{0,0,0,0},{0,0,0,0}};
    	for (int i = 0; i < 4; i++) {
    		for (int j = 0; j < 4; j++) {
    			for (int k = 0; k < 4; k++) {
    				rez[i][j] += m1[i][k] * m2[k][j];
    			}
    		}
    	}
    	return rez;
    }

    //kompozicija transformacija
    void mult(float [][] m) {
        float [][] m1 = new float[][]{{0,0,0,0},{0,0,0,0},{0,0,0,0},{0,0,0,0}};
    	for (int i = 0; i < 4; i++) {
    		for (int j = 0; j < 4; j++) {
    			for (int k = 0; k < 4; k++) {
    				m1[i][j] += this._matrica[i][k] * m[k][j];
    			}
    		}
    	}
    	this._matrica = m1;
    }

    //slika tocke
    float[] slika_tocke(float[] tocka) {
    	float[] ImTocka = new float[]{0,0,0,0};
    	for (int i = 0; i < 4; i++) {
    		for (int k = 0; k < 4; k++) {
    			ImTocka[i] += this._matrica[i][k] * tocka[k];
    		}
    	}
    	return ImTocka;
    }

    //slika tocke primjenom kamere
    float[] kamera_slika_tocke(float[] tocka) {
        float[] ImTocka = new float[]{0,0,0,0};
    	for (int i = 0; i < 4; i++) {
    		for (int k = 0; k < 4; k++) {
    			ImTocka[i] += this._kamera[i][k] * tocka[k];
    		}
    	}
    	return ImTocka;
    }

    //vektorski produkt
    float[] VP(float[] u, float[] v) {
        float[] vek = new float[]{0,0,0};
        vek[0] = u[1]*v[2] - u[2]*v[1];
        vek[1] = u[2]*v[0] - u[0]*v[2];
        vek[2] = u[0]*v[1] - u[1]*v[0];
        return vek;
    }

    //translacija
    void pomakni(float px, float py,float pz) {
        float[][] m = new float[][]{{1,0,0,px},{0,1,0,py},{0,0,1,pz},{0,0,0,1}};
        this.mult(m);
    }

    //skaliranje
    void skaliraj(float sx,float  sy,float  sz) {
        float[][] m = new float[][]{{sx,0,0,0},{0,sy,0,0},{0,0,sz,0},{0,0,0,1}};
        if (!this._skaliranje) this._skaliranje = true;
        this.mult(m);
    }

    //zrcaljenja na koordinatnim osima
    void zrcaliNaX() {
        float[][] m = new float[][]{{1,0,0,0},{0,-1,0,0},{0,0,-1,0},{0,0,0,1}};
        this.mult(m);
    }
    void zrcaliNaY() {
        float[][] m = new float[][]{{-1,0,0,0},{0,1,0,0},{0,0,-1,0},{0,0,0,1}};
        this.mult(m);
    }
    void zrcaliNaZ() {
        float[][] m = new float[][]{{-1,0,0,0},{0,-1,0,0},{0,0,1,0},{0,0,0,1}};
        this.mult(m);
    }

    //zrcaljenja na koordinatnim ravninama
    void zrcaliNaXY() {
        float[][] m = new float[][]{{1,0,0,0},{0,1,0,0},{0,0,-1,0},{0,0,0,1}};
        this.mult(m);
    }
    void zrcaliNaXZ() {
        float[][] m = new float[][]{{1,0,0,0},{0,-1,0,0},{0,0,1,0},{0,0,0,1}};
        this.mult(m);
    }
    void zrcaliNaYZ() {
        float[][] m = new float[][]{{-1,0,0,0},{0,1,0,0},{0,0,1,0},{0,0,0,1}};
        this.mult(m);
    }

    //rotacije oko koordinatnih osi u stupnjevima
    void rotirajX(float kut) {
        float fi = (float)(kut * Math.PI / 180);
        float cosfi = (float)Math.cos(fi);
        float sinfi = (float)Math.sin(fi);
        float[][] m = new float[][]{{1,0,0,0},{0,cosfi,-sinfi,0},{0,sinfi,cosfi,0},{0,0,0,1}};
        this.mult(m);
    }
    void rotirajY(float kut) {
        float fi = (float)(kut * Math.PI / 180);
        float cosfi = (float)Math.cos(fi);
        float sinfi = (float)Math.sin(fi);
        float[][] m = new float[][]{{cosfi,0,sinfi,0},{0,1,0,0},{-sinfi,0,cosfi,0},{0,0,0,1}};
        this.mult(m);
    }
    void rotirajZ(float kut) {
        float fi = (float)(kut * Math.PI / 180);
        float cosfi = (float)Math.cos(fi);
        float sinfi = (float)Math.sin(fi);
        float[][] m = new float[][]{{cosfi,-sinfi,0,0},{sinfi,cosfi,0,0},{0,0,1,0},{0,0,0,1}};
        this.mult(m);
    }

    //identiteta
    void identitet() {
        this._matrica = new float[][]{{1, 0, 0, 0}, {0, 1, 0, 0}, {0, 0, 1, 0}, {0, 0, 0, 1}};
        this._skaliranje = false;
    }

    //smicanje
    void smicanje(float alpha, float beta, float gamma) {
        float tanA = (float)Math.tan(alpha*Math.PI/180);
        float tanB = (float)Math.tan(beta*Math.PI/180);
        float tanC = (float)Math.tan(gamma*Math.PI/180);
        float[][] m = new float[][]{{1,tanB,tanC,0},{tanA,1,tanC,0},{tanA,tanB,1,0},{0,0,0,1}};
        if (!this._skaliranje) this._skaliranje = true;
        this.mult(m);
    }

    //opcenitije transformacije
    void rotiraj_oko_osi(float x0, float y0,float z0,float u1,float u2,float u3,float kut) {
        // os je zadana tockom (x0,y0,z0) i vektorom smjera (u1,u2,u3)
        float korijen = (float)Math.sqrt(u1*u1+u2*u2+u3*u3);
        float a = u1/korijen;
        float b = u2/korijen;
        float c = u3/korijen;
        float d = (float)Math.sqrt(b*b+c*c);
        float[][] Rx1 = new float[][]{{1,0,0,0},{0,c/d,-b/d,0},{0,b/d,c/d,0},{0,0,0,1}};
        float[][] Ry1 = new float[][]{{d,0,-a,0},{0,1,0,0},{a,0,d,0},{0,0,0,1}};
        float[][] Rx2 = new float[][]{{1,0,0,0},{0,c/d,b/d,0},{0,-b/d,c/d,0},{0,0,0,1}};
        float[][] Ry2 = new float[][]{{d,0,a,0},{0,1,0,0},{-a,0,d,0},{0,0,0,1}};
        this.pomakni(x0,y0,z0);
        this.mult(Rx2);
        this.mult(Ry2);
        this.rotirajZ(kut);
        this.mult(Ry1);
        this.mult(Rx1);
        this.pomakni(-x0,-y0,-z0);
    }
    void zrcali_os(float x0, float y0,float z0,float u1,float u2,float u3) {
        // os je zadana tockom (x0,y0,z0) i vektorom smjera (u1,u2,u3)
        float korijen = (float)Math.sqrt(u1*u1+u2*u2+u3*u3);
        float a = u1/korijen;
        float b = u2/korijen;
        float c = u3/korijen;
        float d = (float)Math.sqrt(b*b+c*c);
        float[][] Rx1 = new float[][]{{1,0,0,0},{0,c/d,-b/d,0},{0,b/d,c/d,0},{0,0,0,1}};
        float[][] Ry1 = new float[][]{{d,0,-a,0},{0,1,0,0},{a,0,d,0},{0,0,0,1}};
        float[][] Rx2 = new float[][]{{1,0,0,0},{0,c/d,b/d,0},{0,-b/d,c/d,0},{0,0,0,1}};
        float[][] Ry2 = new float[][]{{d,0,a,0},{0,1,0,0},{-a,0,d,0},{0,0,0,1}};
        this.pomakni(x0,y0,z0);
        this.mult(Rx2);
        this.mult(Ry2);
        this.zrcaliNaZ();
        this.mult(Ry1);
        this.mult(Rx1);
        this.pomakni(-x0,-y0,-z0);
    }
    void zrcali_ravnina(float x0, float y0,float z0,float u1,float u2,float u3) {
        // ravnina je zadana tockom (x0,y0,z0) i normalom (u1,u2,u3)
        float korijen = (float)Math.sqrt(u1*u1+u2*u2+u3*u3);
        float a = u1/korijen;
        float b = u2/korijen;
        float c = u3/korijen;
        float d = (float)Math.sqrt(b*b+c*c);
        float[][] Rx1 = new float[][]{{1,0,0,0},{0,c/d,-b/d,0},{0,b/d,c/d,0},{0,0,0,1}};
        float[][] Ry1 = new float[][]{{d,0,-a,0},{0,1,0,0},{a,0,d,0},{0,0,0,1}};
        float[][] Rx2 = new float[][]{{1,0,0,0},{0,c/d,b/d,0},{0,-b/d,c/d,0},{0,0,0,1}};
        float[][] Ry2 = new float[][]{{d,0,a,0},{0,1,0,0},{-a,0,d,0},{0,0,0,1}};
        this.pomakni(x0,y0,z0);
        this.mult(Rx2);
        this.mult(Ry2);
        this.zrcaliNaXY();
        this.mult(Ry1);
        this.mult(Rx1);
        this.pomakni(-x0,-y0,-z0);
    }
    void zrcali_ravnina2(float A,float B,float C,float D) {
        // ravnina je zadana u opcem obliku Ax+By+Cz+D=0
        float x0, y0, z0;
        if (A!=0) {
          x0 = -D/A;
          y0 = 0;
          z0 = 0;
        } else if (B!=0) {
          x0 = 0;
          y0 = -D/B;
          z0 = 0;
        } else {
          x0 = 0;
          y0 = 0;
          z0 = -D/C;
        }
        float korijen = (float)Math.sqrt(A*A+B*B+C*C);
        float a = A/korijen;
        float b = B/korijen;
        float c = C/korijen;
        float d = (float)Math.sqrt(b*b+c*c);
        float[][] Rx1 = new float[][]{{1,0,0,0},{0,c/d,-b/d,0},{0,b/d,c/d,0},{0,0,0,1}};
        float[][] Ry1 = new float[][]{{d,0,-a,0},{0,1,0,0},{a,0,d,0},{0,0,0,1}};
        float[][] Rx2 = new float[][]{{1,0,0,0},{0,c/d,b/d,0},{0,-b/d,c/d,0},{0,0,0,1}};
        float[][] Ry2 = new float[][]{{d,0,a,0},{0,1,0,0},{-a,0,d,0},{0,0,0,1}};
        this.pomakni(x0,y0,z0);
        this.mult(Rx2);
        this.mult(Ry2);
        this.zrcaliNaXY();
        this.mult(Ry1);
        this.mult(Rx1);
        this.pomakni(-x0,-y0,-z0);
    }

    //preslikava [xmin,xmax] x [ymin,ymax] x [zmin,zmax] u [-1,1] x [-1,1] x [-1,1]
    //i pritom se ne cuvaju proporcije projekcijom na canvas
    void OrtogonalnaProjekcija(float xmin, float xmax, float ymin,float ymax,float zmin, float zmax) {
    	this._projekcija = new float[][]{{2/(xmax-xmin),0,0,(xmin+xmax)/(xmin-xmax)},
                {0,2/(ymax-ymin),0,(ymin+ymax)/(ymin-ymax)},
                {0,0,2/(zmin-zmax),(zmin+zmax)/(zmin-zmax)},
                {0,0,0,1}};
    }

    //preslikava [xmin,xmax] x [ymin,ymax] x [zmin,zmax] u [-1,1] x [-1,1] x [-1,1]
    //i pritom se cuvaju proporcije projekcijom na canvas tako da se po potrebi
    //poveca ili smanji interval [ymin,ymax]
    void OrtogonalnaProjekcijaX(float xmin, float xmax, float ymin, float ymax, float zmin, float zmax, float w, float h) {
        float k = (h / w * (xmax - xmin) - (ymax - ymin)) / 2;
        float y1 = ymin - k;
        float y2 = ymax + k;
        this.OrtogonalnaProjekcija(xmin, xmax, y1, y2, zmin, zmax);
    }

    //preslikava [xmin,xmax] x [ymin,ymax] x [zmin,zmax] u [-1,1] x [-1,1] x [-1,1]
    //i pritom se cuvaju proporcije projekcijom na canvas tako da se po potrebi
    //poveca ili smanji interval [xmin,xmax]
    void OrtogonalnaProjekcijaY(float xmin, float xmax, float ymin, float ymax, float zmin, float zmax, float w, float h) {
        float k = (w / h * (ymax - ymin) - (xmax - xmin)) / 2;
        float x1 = xmin - k;
        float x2 = xmax + k;
        this.OrtogonalnaProjekcija(x1, x2, ymin, ymax, zmin, zmax);
    }

    //koordinatni sustav kamere
    void postaviKameru( float x0, float y0, float z0, float x1, float y1, float z1, float Vx, float Vy, float Vz) {
        float[] V = new float[]{Vx, Vy, Vz};
        float normaN = (float)Math.sqrt((x0-x1)*(x0-x1)+(y0-y1)*(y0-y1)+(z0-z1)*(z0-z1));
        float[] n = new float[]{(x0-x1)/normaN, (y0-y1)/normaN, (z0-z1)/normaN};
        float[] U = this.VP(V, n);
        float normaU = (float)Math.sqrt(U[0]*U[0]+U[1]*U[1]+U[2]*U[2]);
        float[] u = new float[]{U[0]/normaU, U[1]/normaU, U[2]/normaU};
        float[] v = this.VP(n, u);
        float[][] mtr = new float[][]{{u[0], u[1], u[2], -u[0]*x0-u[1]*y0-u[2]*z0},
                {v[0], v[1], v[2], -v[0]*x0-v[1]*y0-v[2]*z0},
                {n[0], n[1], n[2], -n[0]*x0-n[1]*y0-n[2]*z0},
                {0, 0, 0, 1}};
        this._kamera = mtr;
    }

    //preslikava [xmin,xmax] x [ymin,ymax] x [zmin,zmax] u [-1,1] x [-1,1] x [-1,1]
    //tako da odbacivanjem z-koordinate dobijemo perspektivu
    //ne cuvaju se proprorcije na canvas
    void PerspektivnaProjekcija(float xmin, float xmax, float ymin, float ymax, float zmin, float zmax) {
    	this._projekcija = new float[][]{{2*zmin/(xmax-xmin),0,(xmax+xmin)/(xmax-xmin),0},
                {0,2*zmin/(ymax-ymin),(ymax+ymin)/(ymax-ymin),0},
                {0,0,(zmin+zmax)/(zmin-zmax),2*zmin*zmax/(zmin-zmax)},
                {0,0,-1,0}};
    }

    //preslikava [xmin,xmax] x [ymin,ymax] x [zmin,zmax] u [-1,1] x [-1,1] x [-1,1]
    //tako da odbacivanjem z-koordinate dobijemo perspektivu
    //cuvaju se proporcije projekcijom na canvas tako da se po potrebi
    //poveca ili smanji interval [ymin,ymax]
    void PerspektivnaProjekcijaX(float xmin, float xmax, float ymin, float ymax, float zmin, float zmax, float w, float h) {
    	float k = (h / w * (xmax - xmin) - (ymax - ymin)) / 2;
        float y1 = ymin - k;
        float y2 = ymax + k;
        this.PerspektivnaProjekcija(xmin, xmax, y1, y2, zmin, zmax);
    }

    //preslikava [xmin,xmax] x [ymin,ymax] x [zmin,zmax] u [-1,1] x [-1,1] x [-1,1]
    //tako da odbacivanjem z-koordinate dobijemo perspektivu
    //cuvaju se proporcije projekcijom na canvas tako da se po potrebi
    //poveca ili smanji interval [xmin,xmax]
    void PerspektivnaProjekcijaY(float xmin, float xmax, float ymin, float ymax, float zmin, float zmax, float w, float h) {
    	float k = (w / h * (ymax - ymin) - (xmax - xmin)) / 2;
        float x1 = xmin - k;
        float x2 = xmax + k;
        this.PerspektivnaProjekcija(x1, x2, ymin, ymax, zmin, zmax);
    }

    //perspektivnu projekciju zadajemo s vertikalnim kutom pogleda,
    //omjerom sirine i visine slike, te granicama na z-osi
    void Perspektiva(float theta, float omjer, float near, float far) {
      float top = near * (float) Math.tan(theta * Math.PI / 360 );
      float right = omjer * top;
      this.PerspektivnaProjekcija(-right, right, -top, top, near, far);
    }
}