/*
 * Use GL_ARB_fragment_program and GL_ARB_vertex_program to implement
 * simple per-pixel lighting.
 *
 * Brian Paul
 * 17 April 2003
 */

#include <assert.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <glad/glad.h>
#include "glut_wrap.h"


static GLfloat Diffuse[4] = { 0.5, 0.5, 1.0, 1.0 };
static GLfloat Specular[4] = { 0.8, 0.8, 0.8, 1.0 };
static GLfloat LightPos[4] = { 0.0, 10.0, 20.0, 1.0 };
static GLfloat Delta = 1.0;

static GLuint FragProg;
static GLuint VertProg;
static GLboolean Anim = GL_TRUE;
static GLboolean Wire = GL_FALSE;
static GLboolean PixelLight = GL_TRUE;
static GLint Win;

static GLint T0 = 0;
static GLint Frames = 0;

static GLfloat Xrot = 0, Yrot = 0;


/* These must match the indexes used in the fragment program */
#define LIGHTPOS 3

/* Set to one to test ARB_fog_linear program option */
#define DO_FRAGMENT_FOG 0

static void normalize (GLfloat *dst, const GLfloat *src)
{
   GLfloat len = sqrt (src[0] * src[0] + src[1] * src[1] + src[2] * src[2]);
   dst[0] = src[0] / len;
   dst[1] = src[1] / len;
   dst[2] = src[2] / len;
}

static void Redisplay( void )
{
   glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );

   if (PixelLight) {
      GLfloat pos[4];

      normalize( pos, LightPos );
      pos[3] = LightPos[3];
      glProgramLocalParameter4fvARB(GL_FRAGMENT_PROGRAM_ARB,
                                         LIGHTPOS, pos);
      glEnable(GL_FRAGMENT_PROGRAM_ARB);
      glEnable(GL_VERTEX_PROGRAM_ARB);
      glDisable(GL_LIGHTING);
   }
   else {
      glLightfv(GL_LIGHT0, GL_POSITION, LightPos);
      glDisable(GL_FRAGMENT_PROGRAM_ARB);
      glDisable(GL_VERTEX_PROGRAM_ARB);
      glEnable(GL_LIGHTING);
   }

   glPushMatrix();
   glRotatef(Xrot, 1, 0, 0);
   glRotatef(Yrot, 0, 1, 0);
   glutSolidSphere(2.0, 10, 5);
   glPopMatrix();

   glutSwapBuffers();

   Frames++;

   if (Anim) {
      GLint t = glutGet(GLUT_ELAPSED_TIME);
      if (t - T0 >= 5000) {
	 GLfloat seconds = (t - T0) / 1000.0;
	 GLfloat fps = Frames / seconds;
	 printf("%d frames in %6.3f seconds = %6.3f FPS\n", Frames, seconds, fps);
         fflush(stdout);
	 T0 = t;
	 Frames = 0;
      }
   }
}


static void Idle(void)
{
   LightPos[0] += Delta;
   if (LightPos[0] > 25.0)
      Delta = -1.0;
   else if (LightPos[0] <- 25.0)
      Delta = 1.0;
   glutPostRedisplay();
}


static void Reshape( int width, int height )
{
   glViewport( 0, 0, width, height );
   glMatrixMode( GL_PROJECTION );
   glLoadIdentity();
   glFrustum( -1.0, 1.0, -1.0, 1.0, 5.0, 25.0 );
   glMatrixMode( GL_MODELVIEW );
   glLoadIdentity();
   glTranslatef( 0.0, 0.0, -15.0 );
}


static void Key( unsigned char key, int x, int y )
{
   (void) x;
   (void) y;
   switch (key) {
     case ' ':
     case 'a':
        Anim = !Anim;
        if (Anim)
           glutIdleFunc(Idle);
        else
           glutIdleFunc(NULL);
        break;
      case 'x':
         LightPos[0] -= 1.0;
         break;
      case 'X':
         LightPos[0] += 1.0;
         break;
      case 'w':
         Wire = !Wire;
         if (Wire)
            glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
         else
            glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
         break;
      case 'p':
         PixelLight = !PixelLight;
         if (PixelLight) {
            printf("Per-pixel lighting\n");
         }
         else {
            printf("Conventional lighting\n");
         }
         break;
      case 27:
         glDeleteProgramsARB(1, &VertProg);
         glDeleteProgramsARB(1, &FragProg);
         glutDestroyWindow(Win);
         exit(0);
         break;
   }
   glutPostRedisplay();
}

static void SpecialKey( int key, int x, int y )
{
   const GLfloat step = 3.0;
   (void) x;
   (void) y;
   switch (key) {
      case GLUT_KEY_UP:
         Xrot -= step;
         break;
      case GLUT_KEY_DOWN:
         Xrot += step;
         break;
      case GLUT_KEY_LEFT:
         Yrot -= step;
         break;
      case GLUT_KEY_RIGHT:
         Yrot += step;
         break;
   }
   glutPostRedisplay();
}


/* A helper for finding errors in program strings */
static int FindLine( const char *program, int position )
{
   int i, line = 1;
   for (i = 0; i < position; i++) {
      if (program[i] == '\n')
         line++;
   }
   return line;
}


static void Init( void )
{
   GLint errorPos;

   /* Yes, this could be expressed more efficiently */
   static const char *fragProgramText =
      "!!ARBfp1.0\n"
#if DO_FRAGMENT_FOG
      "OPTION ARB_fog_linear; \n"
#endif
      "PARAM Diffuse = state.material.diffuse; \n"
      "PARAM Specular = state.material.specular; \n"
      "PARAM LightPos = program.local[3]; \n"
      "TEMP normal, len; \n"
      "TEMP dotProd, specAtten; \n"
      "TEMP diffuseColor, specularColor; \n"

      "# Compute normalized normal \n"
      "DP3 len.x, fragment.texcoord[0], fragment.texcoord[0]; \n"
      "RSQ len.y, len.x; \n"
      "MUL normal.xyz, fragment.texcoord[0], len.y; \n"

      "# Compute dot product of light direction and normal vector\n"
      "DP3_SAT dotProd.x, LightPos, normal;             # limited to [0,1]\n"

      "MUL diffuseColor.xyz, Diffuse, dotProd.x;            # diffuse attenuation\n"

      "POW specAtten.x, dotProd.x, {20.0}.x;          # specular exponent\n"

      "MUL specularColor.xyz, Specular, specAtten.x;      # specular attenuation\n"

      "MOV result.color.w, Diffuse; \n"
#if DO_FRAGMENT_FOG
      "# need to clamp color to [0,1] before fogging \n"
      "ADD_SAT result.color.xyz, diffuseColor, specularColor; # add colors\n"
#else
      "# clamping will be done after program's finished \n"
      "ADD result.color.xyz, diffuseColor, specularColor; # add colors\n"
#endif
      "END \n"
      ;

   static const char *vertProgramText =
      "!!ARBvp1.0\n"
      "ATTRIB pos = vertex.position; \n"
      "ATTRIB norm = vertex.normal; \n"
      "PARAM modelview[4] = { state.matrix.modelview }; \n"
      "PARAM modelviewProj[4] = { state.matrix.mvp }; \n"
      "PARAM invModelview[4] = { state.matrix.modelview.invtrans }; \n"

      "# typical modelview/projection transform \n"
      "DP4 result.position.x, pos, modelviewProj[0]; \n"
      "DP4 result.position.y, pos, modelviewProj[1]; \n"
      "DP4 result.position.z, pos, modelviewProj[2]; \n"
      "DP4 result.position.w, pos, modelviewProj[3]; \n"

      "# transform normal by inv transpose of modelview, put in tex0 \n"
      "DP3 result.texcoord[0].x, norm, invModelview[0]; \n"
      "DP3 result.texcoord[0].y, norm, invModelview[1]; \n"
      "DP3 result.texcoord[0].z, norm, invModelview[2]; \n"
      "DP3 result.texcoord[0].w, norm, invModelview[3]; \n"

#if DO_FRAGMENT_FOG
      "# compute fog coordinate = vertex eye-space Z coord (negated)\n"
      "DP4 result.fogcoord, -pos, modelview[2]; \n"
#endif
      "END\n";
      ;

   if (!glutExtensionSupported("GL_ARB_vertex_program")) {
      printf("Sorry, this demo requires GL_ARB_vertex_program\n");
      exit(1);
   }
   if (!glutExtensionSupported("GL_ARB_fragment_program")) {
      printf("Sorry, this demo requires GL_ARB_fragment_program\n");
      exit(1);
   }
         
   /*
    * Fragment program
    */
   glGenProgramsARB(1, &FragProg);
   assert(FragProg > 0);
   glBindProgramARB(GL_FRAGMENT_PROGRAM_ARB, FragProg);
   glProgramStringARB(GL_FRAGMENT_PROGRAM_ARB,
                           GL_PROGRAM_FORMAT_ASCII_ARB,
                           strlen(fragProgramText),
                           (const GLubyte *) fragProgramText);
   glGetIntegerv(GL_PROGRAM_ERROR_POSITION_ARB, &errorPos);
   if (glGetError() != GL_NO_ERROR || errorPos != -1) {
      int l = FindLine(fragProgramText, errorPos);
      printf("Fragment Program Error (pos=%d line=%d): %s\n", errorPos, l,
             (char *) glGetString(GL_PROGRAM_ERROR_STRING_ARB));
      exit(0);
   }
   assert(glIsProgramARB(FragProg));

   /*
    * Do some sanity tests
    */
   {
      GLdouble v[4];
      glProgramLocalParameter4dARB(GL_FRAGMENT_PROGRAM_ARB, 8,
                                   10.0, 20.0, 30.0, 40.0);
      glGetProgramLocalParameterdvARB(GL_FRAGMENT_PROGRAM_ARB, 8, v);
      assert(v[0] == 10.0);
      assert(v[1] == 20.0);
      assert(v[2] == 30.0);
      assert(v[3] == 40.0);
   }

   /*
    * Vertex program
    */
   glGenProgramsARB(1, &VertProg);
   assert(VertProg > 0);
   glBindProgramARB(GL_VERTEX_PROGRAM_ARB, VertProg);
   glProgramStringARB(GL_VERTEX_PROGRAM_ARB,
                           GL_PROGRAM_FORMAT_ASCII_ARB,
                           strlen(vertProgramText),
                           (const GLubyte *) vertProgramText);
   glGetIntegerv(GL_PROGRAM_ERROR_POSITION_ARB, &errorPos);
   if (glGetError() != GL_NO_ERROR || errorPos != -1) {
      int l = FindLine(vertProgramText, errorPos);
      printf("Vertex Program Error (pos=%d line=%d): %s\n", errorPos, l,
             (char *) glGetString(GL_PROGRAM_ERROR_STRING_ARB));
      exit(0);
   }
   assert(glIsProgramARB(VertProg));

   /*
    * Misc init
    */
   glClearColor(0.3, 0.3, 0.3, 0.0);
   glEnable(GL_DEPTH_TEST);
   glEnable(GL_LIGHT0);
   glEnable(GL_LIGHTING);
   glMaterialfv(GL_FRONT_AND_BACK, GL_DIFFUSE, Diffuse);
   glMaterialfv(GL_FRONT_AND_BACK, GL_SPECULAR, Specular);
   glMaterialf(GL_FRONT_AND_BACK, GL_SHININESS, 20.0);

#if DO_FRAGMENT_FOG
   {
      /* Green-ish fog color */
      static const GLfloat fogColor[4] = {0.5, 1.0, 0.5, 0};
      glFogfv(GL_FOG_COLOR, fogColor);
      glFogf(GL_FOG_START, 5.0);
      glFogf(GL_FOG_END, 25.0);
   }
#endif

   printf("GL_RENDERER = %s\n", (char *) glGetString(GL_RENDERER));
   printf("Press p to toggle between per-pixel and per-vertex lighting\n");
}


int main( int argc, char *argv[] )
{
   glutInitWindowSize( 200, 200 );
   glutInit( &argc, argv );
   glutInitDisplayMode( GLUT_RGB | GLUT_DOUBLE | GLUT_DEPTH );
   Win = glutCreateWindow(argv[0]);
   gladLoadGL();
   glutReshapeFunc( Reshape );
   glutKeyboardFunc( Key );
   glutSpecialFunc( SpecialKey );
   glutDisplayFunc( Redisplay );
   if (Anim)
      glutIdleFunc(Idle);
   Init();
   glutMainLoop();
   return 0;
}
