package cgp.render;

import cgp.basics.Shape3D;
import cgp.basics.Face3D;
import cgp.basics.Vertex3D;
import cgp.basics.LightParams;

import cgp.math.Point2f;
import cgp.math.Point4f;

import java.util.Enumeration;

/**
 * Ein Renderer, der 
 * - BFC durchfuehrt,
 * - die uebrigen Flaechen am Frustum clippt
 * - mit Texturen darstellt.
 *   
 * Diese Version beruecksichtigt, dass es keinen Unterschied zwischen
 * Flat und CurvedFace bzw. -Vertex mehr gibt.
 *
 * Jetzt wird ein BFCuller benutzt.
 *
 * @author Olaf Mueller
 * @version V1.0 22.07.2002
 */
public class TextureMapper implements Renderer {
 
  public static final float EPS = -0.001f;

  public WCClipper clipper; 
  public BFCuller bfculler;
  public View view;
  public Lighting lights = null;

  public CGCanvas cgc = null;

  int [][] colors;
  int [][] umcolors;
  

  // Dreieck zum Shaden
  // DC-Koordinaten
  Point4f[] dc = {new Point4f(), new Point4f(), new Point4f()};

  // Texturkoordinaten
  Point2f[] tc = {new Point2f(), new Point2f(), new Point2f()};

  // Vertices mit allen Infos
  Vertex3D[] triangle = {new Vertex3D(null, 0, null, new Point2f()),
                         new Vertex3D(null, 0, null, new Point2f()),
                         new Vertex3D(null, 0, null, new Point2f())};


  public TextureMapper() {
    clipper = new WCClipper();
    bfculler = new BFCuller();

    colors = new int[256][256];
    for (int i=0; i<256; i++) {
      for (int j=0; j<256; j++) {
        colors[i][j] = (int)Math.round(i * j / 255f);
      }
    }
    umcolors = new int[256][256];
    for (int i=0; i<256; i++) {
      for (int j=0; j<256; j++) {
        umcolors[i][j] = colors[255 - i][j];
      }
    }
  }

  /**
   * Zentrale Methode; loest das Rendern der Szene aus. Die uebergebene View 
   * liefert alle Informationen, die der Renderer braucht, um die Szene
   * darzustellen.
   * Die Uebergabe der View ermoeglicht den Einsatz derselben
   * Renderer-Instanz in mehreren Views.
   */
  public void renderScene(View view) {
    Scene scene = view.scene;
    lights = scene.lights;
    Camera cam = view.cam;

    Shape3D shape;                               // aktueller Koerper
    Face3D face;                                 // aktuelle Flaeche

    view.cgc.clearBuffer();                      // neues Frame vorbereiten
    view.cgc.clearZBuffer();                     // neues Frame vorbereiten
    Enumeration e = scene.getVisibleShapes(cam); // Liste sichtbarer Koerper
    while(e.hasMoreElements()) {                 // Liste durchlaufen
      shape = (Shape3D)e.nextElement();          // naechsten Koerper holen
      if (shape == cam) continue;
      for(int j=0; j<shape.count; j++) {         // Alle Flaechen
        face = shape.faces[j];
        renderFace(face, view);                  // Flat or Curved
      } // Fuer alle Flaechen
    } // Fuer alle Koerper
    view.cgc.refresh();                          // Frame auf den Schirm
         
  }

  public void renderFace(Face3D f, View view) {
    Camera cam = view.cam;
    cgc = view.cgc;

    LightParams lp = new LightParams();

    Vertex3D[] knoten;                           // sichtbare Punkte

    int kc=0;                                    // Anzahl sichtb. Punkte
    f = bfculler.bfc(f, cam);                    // Flaeche holen u. bfc
    if(f != null) {
      knoten = clipper.clip(f, cam);             // Flaeche clippen
      if((knoten != null) && (knoten.length >2)){// Flaeche z.T. sichtbar?
        Vertex3D tmptri;
        kc = knoten.length;

        float tmpw;
        float l0,l1,l2;                        // Lichtintensitaeten


        // gemaess der Anleitung von Hecker. Allerdings scheint es noch
        // ein paar Probleme zu geben...


        // Alle Informationen fuer den ersten Vertex uebertragen
            
        // Geometriekoordinaten projizieren
        view.WC2DC.multiply(knoten[0].wc, triangle[0].wc);
        tmpw = 1f/triangle[0].wc.w;              // homogene Koordinate merken
        triangle[0].wc.homogenize();             // teile durch homogene Koord.
        triangle[0].wc.w = tmpw;                 // homogene Koordinate retten

        // Texturkoordinaten projizieren
        triangle[0].tc.u = knoten[0].tc.u*tmpw;
        triangle[0].tc.v = knoten[0].tc.v*tmpw;

        // Normalen retten
        triangle[0].wc_normal = knoten[0].wc_normal;
        // Beleuchtung ermitteln
        l0 = lights.getLight(knoten[0].wc, knoten[0].wc_normal ,lp);
        
        // Flaeche setzen
        triangle[0].face = f;

        // Ab hier: zweiter Vertex

        view.WC2DC.multiply(knoten[1].wc, triangle[2].wc);
        tmpw = 1f/triangle[2].wc.w;              // homogene Koordinate merken
        triangle[2].wc.homogenize();             // teile durch homogene Koord.
        triangle[2].wc.w = tmpw;                 // homogene Koordinate retten

        // Texturkoordinaten projizieren
        triangle[2].tc.u = knoten[1].tc.u*tmpw;
        triangle[2].tc.v = knoten[1].tc.v*tmpw;

        // Normale retten
        triangle[2].wc_normal = knoten[1].wc_normal;
        // Beleuchtung ermitteln
        l2 = lights.getLight(knoten[1].wc, knoten[1].wc_normal ,lp);

        // Flaeche setzen
        triangle[2].face = f;

        for(int k=2; k<kc; k++) {                // Fuer alle Punkte
          tmptri      = triangle[1];             // Fuer ersten Punkt
          triangle[1] = triangle[2];
          triangle[2] = tmptri;

          l1 = l2;
          view.WC2DC.multiply(knoten[k].wc, triangle[2].wc);
          tmpw = 1f/triangle[2].wc.w;            // homogene Koordinate merken
          triangle[2].wc.homogenize();           // teile durch homogene Koord.
          triangle[2].wc.w = tmpw;               // homogene Koordinate retten

          triangle[2].tc.u = knoten[k].tc.u*tmpw;
          triangle[2].tc.v = knoten[k].tc.v*tmpw;

          // Normale retten
          triangle[2].wc_normal = knoten[k].wc_normal;
          // Beleuchtung ermitteln
          l2 = lights.getLight(knoten[k].wc, knoten[k].wc_normal ,lp);

          // Flaeche setzen
          triangle[2].face = f;

          shade(triangle, (l0+l1+l2)/3.0f);
        } // fuer alle Punkte dieser Flaeche
      } // Flaeche nicht entartet und mind. z.T. sichtbar
    } // Flaeche nicht abgewandt
  }

  // Faerbt das Dreieck in p mit der Textur von f
  private void shade(Vertex3D[] triangle, float fi) {
    Vertex3D va = triangle[0];
    Vertex3D vb = triangle[1];
    Vertex3D vc = triangle[2];
    Vertex3D vA, vB, vC;

    Point4f pa = triangle[0].wc;
    Point4f pb = triangle[1].wc;
    Point4f pc = triangle[2].wc;
    Point4f pA, pB, pC;    // pA.y <= pB.y <= pC.y

    Point2f ta = triangle[0].tc;
    Point2f tb = triangle[1].tc;
    Point2f tc = triangle[2].tc;
    Point2f tA, tB, tC;

    float sxAB, sxAC, sxBC;  // Steigungen in Dimension x
    float szAB, szAC, szBC;  // Steigungen in Dimension z
    float suAB, suAC, suBC;  // Steigungen in u-Dimension
    float svAB, svAC, svBC;  // Steigungen in u-Dimension
    float swAB, swAC, swBC;  // Steigungen in 1/w-Dimension
   
    float onedy;

    int y, yEnd;
    float yPreStep;

    if (va.wc.y <= vb.wc.y) {
      vA = va; vB = vb;
    }
    else {
      vA = vb; vB = va;
    }

    if (vc.wc.y >= vB.wc.y) {
      vC = vc;
    }
    else {
      vC = vB;
      if (vc.wc.y >= vA.wc.y) {
        vB = vc;
      }
      else {
        vB = vA; vA = vc;
      }
    }

    // Sortierung abgeschlossen

    //Jetzt folgt der Test auf negative z-Koordinaten
    if(((vA.wc.z <0) || (vB.wc.z<0)) || (vC.wc.z<0)) {
      System.out.println("shade:Negative z-Koordinate im Dreieck:");
      System.out.println("vA="+vA+"; vB="+vB+"; vC="+vC);
    }
    
    
    // Jetzt kommt die Berechnung der Gradienten
    // Wir haben in dieser Implementation 5 Parameter:
    // x, z, w, u und v.
    // Diese liegen bereits in der Form
    // x/w, z/w, 1/w, u/w und v/w vor.
    Gradients gradAC = new Gradients(5);
    Gradients gradAB = new Gradients(5);
    Gradients gradBC = new Gradients(5);

    if (vA.wc.y != vC.wc.y) { // Kante AC nicht horizontal -> Steigungen berechnen 

      onedy = 1f/(vC.wc.y - vA.wc.y);
    
      sxAC = (vC.wc.x - vA.wc.x) * onedy;
      szAC = (vC.wc.z - vA.wc.z) * onedy;
      swAC = (vC.wc.w - vA.wc.w) * onedy;
      suAC = (vC.tc.u - vA.tc.u) * onedy;
      svAC = (vC.tc.v - vA.tc.v) * onedy;
    }
    else { // Steigungen werden nicht benoetigt - ausser fuer den Compiler ;-)
      sxAC = 0f;
      szAC = 0f;
      swAC = 0f;
      suAC = 0f;
      svAC = 0f;
    }

    // Y=Scanline beginnt beim obertsen Punkt (A)
    y = ceil(vA.wc.y);
    yPreStep = y - vA.wc.y;

    float xAC = vA.wc.x + yPreStep*sxAC;
    float zAC = vA.wc.z + yPreStep*szAC;
    float wAC = vA.wc.w + yPreStep*swAC;
    float uAC = vA.tc.u + yPreStep*suAC;
    float vAC = vA.tc.v + yPreStep*svAC;

    yEnd = ceil(vB.wc.y);

    if (vA.wc.y == vB.wc.y) { // Kante AB horizontal
    } else {
      onedy = 1f/(vB.wc.y - vA.wc.y);

      sxAB = (vB.wc.x - vA.wc.x) * onedy;  // Steigungen berechnen
      szAB = (vB.wc.z - vA.wc.z) * onedy;
      swAB = (vB.wc.w - vA.wc.w) * onedy;
      suAB = (vB.tc.u - vA.tc.u) * onedy;
      svAB = (vB.tc.v - vA.tc.v) * onedy;


                                                 // laeuft von A nach B ...
      float xAB = vA.wc.x + yPreStep*sxAB;       // in x/w-Dimension
      float zAB = vA.wc.z + yPreStep*szAB;       // in z/w-Dimension
      float wAB = vA.wc.w + yPreStep*swAB;       // in 1/w-Dimension
      float uAB = vA.tc.u + yPreStep*suAB;       // in u/w-Dimension
      float vAB = vA.tc.v + yPreStep*svAB;       // in v/w-Dimension

      for (; y < yEnd; y++) {
        //System.out.println("shade: gleich folgt Scanline mit zAC="+zAC+"; zAB="+zAB);
        putScanline(y, xAC, xAB, zAC, zAB, uAC, uAB, vAC, vAB, wAC, wAB, vA.face, fi);

        // Schritt entlang Kante AC in x- und z-Richtung machen
        xAC += sxAC;
        zAC += szAC;

        // Schritt entlang Kante AC in u- und v-Richtung machen
        uAC += suAC;
        vAC += svAC;

        // Schritt entlang der 1/w Dimension
        wAC += swAC;

        // Schritt entlang Kante AB in x- und z-Richtung machen
        xAB += sxAB;
        zAB += szAB;

        // Schritt entlang Kante AB in u- und v-Richtung machen
        uAB += suAB;
        vAB += svAB;
       
        // Schritt entlang der Kante AB in 1/w-Richtung
        wAB += swAB;
      }
    }
    
    if (vB.wc.y == vC.wc.y) {  // Kante BC horizontal
    } else {
      onedy = 1f/(vC.wc.y - vB.wc.y);

      sxBC = (vC.wc.x - vB.wc.x) * onedy;  // Steigungen berechnen
      szBC = (vC.wc.z - vB.wc.z) * onedy;
      swBC = (vC.wc.w - vB.wc.w) * onedy;
      suBC = (vC.tc.u - vB.tc.u) * onedy;
      svBC = (vC.tc.v - vB.tc.v) * onedy;
 
      yPreStep = yEnd - vB.wc.y;
      yEnd = ceil(vC.wc.y);

      // Wir laufen von B nach C (xAC laeuft weiter von A nach C)
      float xBC = vB.wc.x + yPreStep*sxBC;
      float zBC = vB.wc.z + yPreStep*szBC;
      float wBC = vB.wc.w + yPreStep*swBC;
      float uBC = vB.tc.u + yPreStep*suBC;
      float vBC = vB.tc.v + yPreStep*svBC;

      for (; y < yEnd; y++) {
        putScanline(y, xBC,  xAC, zBC, zAC, uBC, uAC, vBC, vAC, wBC, wAC, vA.face, fi);

        // Schritt entlang Kante BC in x- und z-Richtung machen
        xBC += sxBC;
        zBC += szBC;

        // Schritt in u- und v-Richtung entlang der Kante BC
        uBC += suBC;
        vBC += svBC;

        // Schritt in Richtung der 1/w-Dimension
        wBC += swBC;

        // Schritt entlang Kante AC in x- und z-Richtung machen
        xAC += sxAC;
        zAC += szAC;

        // Schritt in u- und v-Richtung entlang der Kante AC
        uAC += suAC;
        vAC += svAC;

        // Schritt in Richtung der 1/w-Dimension
        wAC += swAC;
      }
    }
  }
  

  private void putScanline(int y, // Berechnet waagerechte Linie in Hoehe y
    float x1, float x2,                          // x-Komponenten
    float z1, float z2,                          // mit Z-Komponenten z1, z2
    float u1, float u2,                          // u-Komponenten
    float v1, float v2,                          // v-Komponenten
    float w1, float w2,                          // 1/w Komponenten
    Face3D f, float fi) {                        // Flaeche mit Textur und Farbintensitaet
    
    int width = f.tex.getUSize()-1;                 // Texturgroesse besorgen
    int height = f.tex.getVSize()-1;                 // Texturgroesse besorgen
    //int width = f.tex.getUSize();                 // Texturgroesse besorgen
    //int height = f.tex.getVSize();                 // Texturgroesse besorgen

    int alpha = 255 << 24;            // bereitet Endform der Farbe vor
    int rmask = 255 << 16;            // bereitet Maske fuer Rot
    int gmask = 255 << 8;            // bereitet Maske fuer Gruen
    int bmask = 255;                  // bereitet Maske fuer Blau
    int r,g,b;                        // Farben selbst
    float fix;

    if (x1 == x2) {        // Linie ist Punkt
      int x = (int)x1;

      int col = f.tex.getTexel(f.uMode.getUInt(u1/w1,f.tex), f.vMode.getVInt(v1/w1,f.tex));
      
      r = rmask & col;                  // bekomme aus Texel-Farbe nur Rot-Anteil
      g = gmask & col;                  // bekomme aus Texel-Farbe nur Gruen-Anteil
      b = bmask & col;                  // bekomme aus Texel-Farbe nur Blau-Anteil
      
      r = r >> 16;                  // ist < 256
      g = g >> 8;                  // ist < 256
      fix = fi * 2f;
      
      if (fi < 0.5f) {
        int fixint = (int)(255 * fix);
        r = colors[r][fixint];            // hole den interpolierten Wert fuer Rot
      r = r << 16;
      g = colors[g][fixint];            // hole den interpolierten Wert fuer Gruen
      g = g << 8;
                              // hole den interpolierten Wert fuer Blau
        col = ((alpha | r) | g) | colors[b][fixint];      // und setze col auf interpolierte Gesamtfarbe
      }
      else {
        fix --;
        int fixint = (int)(255 * fix);
        r += umcolors[r][fixint];            // hole den interpolierten Wert fuer Rot
      r = r << 16;
      g += umcolors[g][fixint];            // hole den interpolierten Wert fuer Gruen
      g = g << 8;
                              // hole den interpolierten Wert fuer Blau
        col = ((alpha | r) | g) | umcolors[b][fixint];      // und setze col auf interpolierte Gesamtfarbe
      }  
            
      cgc.setPixelWithZ(x, y, z1, col);

    }
    else {
      float xl, xr;
      float zl, zr;
      float ul, ur;
      float vl, vr;
      float onewl, onewr;

      if (x1 < x2) {
        xl = x1; xr = x2; zl = z1; zr = z2;
        ul = u1; ur = u2; vl = v1; vr = v2;
        onewl = w1; onewr = w2;
      }
      else {
        xl = x2; xr = x1; zl = z2; zr = z1;
        ul = u2; ur = u1; vl = v2; vr = v1;
        onewl = w2; onewr = w1;
      }

      float onedx = 1f / (xr - xl);

      float sz = (zr - zl) * onedx; // Steigung berechnen
      float su = (ur - ul) * onedx; // Steigung berechnen
      float sv = (vr - vl) * onedx; // Steigung berechnen
      float sonew = (onewr - onewl) * onedx; // Steigung in 1/w-Richtung

      int x = ceil(xl);
      float xPreStep = x - xl;
      int xEnd = ceil(xr); 

      float z = zl + sz*xPreStep;              // x laeuft von Links nach Rechts
      float u = ul + su*xPreStep;
      float v = vl + sv*xPreStep;
      float onew = onewl + sonew*xPreStep;
      float w = 1/onew;                        // Aus 1/w wieder w machen
      float uNow = u*w;
      float vNow = v*w;


      for (; x < xEnd; x++) {
      int col = f.tex.getTexel(f.uMode.getUInt(uNow,f.tex), f.vMode.getVInt(vNow,f.tex));
      
      r = rmask & col;                  // bekomme aus Texel-Farbe nur Rot-Anteil
      g = gmask & col;                  // bekomme aus Texel-Farbe nur Gruen-Anteil
      b = bmask & col;                  // bekomme aus Texel-Farbe nur Blau-Anteil
      
      r = r >> 16;                  // ist < 256
      g = g >> 8;                  // ist < 256
      fix = fi * 2f;
      
      if (fi < 0.5f) {
        int fixint = (int)(255 * fix);
        r = colors[r][fixint];            // hole den interpolierten Wert fuer Rot
      r = r << 16;
      g = colors[g][fixint];            // hole den interpolierten Wert fuer Gruen
      g = g << 8;
                              // hole den interpolierten Wert fuer Blau
        col = ((alpha | r) | g) | colors[b][fixint];      // und setze col auf interpolierte Gesamtfarbe
      }
      else {
        fix --;
        int fixint = (int)(255 * fix);
        r += umcolors[r][fixint];      // hole den interpolierten Wert fuer Rot
      r = r << 16;
      g += umcolors[g][fixint];      // hole den interpolierten Wert fuer Gruen
      g = g << 8;
                              // hole den interpolierten Wert fuer Blau
        col = ((alpha | r ) | g ) | (umcolors[b][fixint] + b); // und setze col auf interpolierte Gesamtfarbe
      }  
 
      cgc.setPixelWithZ(x, y, z, col);

        z += sz;
        u += su;
        v += sv;
        onew += sonew;
        w = 1/onew;                        // Aus 1/w wieder w machen
        uNow = u*w;
        vNow = v*w;
      }
    }
  }

  /** 
   * Liefert den naechstgroesseren Ganzahlwert bezogen auf x; ausser x ist
   * bereits eine Ganzzahl.
   */
  private static int ceil(float x) {
    int ganz = (int)x;
    return x-(float)ganz == 0f ? ganz : x<0 ? ganz : ganz+1;
  }
}