//========================================================================
// This is an example program for the GLFW library
//
// The program uses a "split window" view, rendering four views of the
// same scene in one window (e.g. uesful for 3D modelling software). This
// demo uses scissors to separete the four different rendering areas from
// each other.
//
// (If the code seems a little bit strange here and there, it may be
//  because I am not a friend of orthogonal projections)
//========================================================================

#include <GL/glfw3.h>
#include <math.h>
#include <stdio.h>
#include <stdlib.h>

#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif


//========================================================================
// Global variables
//========================================================================

// Mouse position
static int xpos = 0, ypos = 0;

// Window size
static int width, height;

// Active view: 0 = none, 1 = upper left, 2 = upper right, 3 = lower left,
// 4 = lower right
static int active_view = 0;

// Rotation around each axis
static int rot_x = 0, rot_y = 0, rot_z = 0;

// Do redraw?
static int do_redraw = 1;


//========================================================================
// Draw a solid torus (use a display list for the model)
//========================================================================

#define TORUS_MAJOR     1.5
#define TORUS_MINOR     0.5
#define TORUS_MAJOR_RES 32
#define TORUS_MINOR_RES 32

static void drawTorus( void )
{
    static GLuint torus_list = 0;
    int    i, j, k;
    double s, t, x, y, z, nx, ny, nz, scale, twopi;

    if( !torus_list )
    {
        // Start recording displaylist
        torus_list = glGenLists( 1 );
        glNewList( torus_list, GL_COMPILE_AND_EXECUTE );

        // Draw torus
        twopi = 2.0 * M_PI;
        for( i = 0; i < TORUS_MINOR_RES; i++ )
        {
            glBegin( GL_QUAD_STRIP );
            for( j = 0; j <= TORUS_MAJOR_RES; j++ )
            {
                for( k = 1; k >= 0; k-- )
                {
                    s = (i + k) % TORUS_MINOR_RES + 0.5;
                    t = j % TORUS_MAJOR_RES;

                    // Calculate point on surface
                    x = (TORUS_MAJOR+TORUS_MINOR*cos(s*twopi/TORUS_MINOR_RES))*cos(t*twopi/TORUS_MAJOR_RES);
                    y = TORUS_MINOR * sin(s * twopi / TORUS_MINOR_RES);
                    z = (TORUS_MAJOR+TORUS_MINOR*cos(s*twopi/TORUS_MINOR_RES))*sin(t*twopi/TORUS_MAJOR_RES);

                    // Calculate surface normal
                    nx = x - TORUS_MAJOR*cos(t*twopi/TORUS_MAJOR_RES);
                    ny = y;
                    nz = z - TORUS_MAJOR*sin(t*twopi/TORUS_MAJOR_RES);
                    scale = 1.0 / sqrt( nx*nx + ny*ny + nz*nz );
                    nx *= scale;
                    ny *= scale;
                    nz *= scale;

                    glNormal3f( (float)nx, (float)ny, (float)nz );
                    glVertex3f( (float)x, (float)y, (float)z );
                }
            }
            glEnd();
        }

        // Stop recording displaylist
        glEndList();
    }
    else
    {
        // Playback displaylist
        glCallList( torus_list );
    }
}


//========================================================================
// Draw the scene (a rotating torus)
//========================================================================

static void drawScene( void )
{
    const GLfloat model_diffuse[4]  = {1.0f, 0.8f, 0.8f, 1.0f};
    const GLfloat model_specular[4] = {0.6f, 0.6f, 0.6f, 1.0f};
    const GLfloat model_shininess   = 20.0f;

    glPushMatrix();

    // Rotate the object
    glRotatef( (GLfloat)rot_x*0.5f, 1.0f, 0.0f, 0.0f );
    glRotatef( (GLfloat)rot_y*0.5f, 0.0f, 1.0f, 0.0f );
    glRotatef( (GLfloat)rot_z*0.5f, 0.0f, 0.0f, 1.0f );

    // Set model color (used for orthogonal views, lighting disabled)
    glColor4fv( model_diffuse );

    // Set model material (used for perspective view, lighting enabled)
    glMaterialfv( GL_FRONT, GL_DIFFUSE, model_diffuse );
    glMaterialfv( GL_FRONT, GL_SPECULAR, model_specular );
    glMaterialf(  GL_FRONT, GL_SHININESS, model_shininess );

    // Draw torus
    drawTorus();

    glPopMatrix();
}


//========================================================================
// Draw a 2D grid (used for orthogonal views)
//========================================================================

static void drawGrid( float scale, int steps )
{
    int   i;
    float x, y;

    glPushMatrix();

    // Set background to some dark bluish grey
    glClearColor( 0.05f, 0.05f, 0.2f, 0.0f);
    glClear( GL_COLOR_BUFFER_BIT );

    // Setup modelview matrix (flat XY view)
    glLoadIdentity();
    gluLookAt( 0.0, 0.0, 1.0,
               0.0, 0.0, 0.0,
               0.0, 1.0, 0.0 );

    // We don't want to update the Z-buffer
    glDepthMask( GL_FALSE );

    // Set grid color
    glColor3f( 0.0f, 0.5f, 0.5f );

    glBegin( GL_LINES );

    // Horizontal lines
    x = scale * 0.5f * (float)(steps-1);
    y = -scale * 0.5f * (float)(steps-1);
    for( i = 0; i < steps; i ++ )
    {
        glVertex3f( -x, y, 0.0f );
        glVertex3f( x, y, 0.0f );
        y += scale;
    }

    // Vertical lines
    x = -scale * 0.5f * (float)(steps-1);
    y = scale * 0.5f * (float)(steps-1);
    for( i = 0; i < steps; i ++ )
    {
        glVertex3f( x, -y, 0.0f );
        glVertex3f( x, y, 0.0f );
        x += scale;
    }

    glEnd();

    // Enable Z-buffer writing again
    glDepthMask( GL_TRUE );

    glPopMatrix();
}


//========================================================================
// Draw all views
//========================================================================

static void drawAllViews( void )
{
    const GLfloat light_position[4] = {0.0f, 8.0f, 8.0f, 1.0f};
    const GLfloat light_diffuse[4]  = {1.0f, 1.0f, 1.0f, 1.0f};
    const GLfloat light_specular[4] = {1.0f, 1.0f, 1.0f, 1.0f};
    const GLfloat light_ambient[4]  = {0.2f, 0.2f, 0.3f, 1.0f};
    double aspect;

    // Calculate aspect of window
    if( height > 0 )
    {
        aspect = (double)width / (double)height;
    }
    else
    {
        aspect = 1.0;
    }

    // Clear screen
    glClearColor( 0.0f, 0.0f, 0.0f, 0.0f);
    glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );

    // Enable scissor test
    glEnable( GL_SCISSOR_TEST );

    // Enable depth test
    glEnable( GL_DEPTH_TEST );
    glDepthFunc( GL_LEQUAL );


    // ** ORTHOGONAL VIEWS **

    // For orthogonal views, use wireframe rendering
    glPolygonMode( GL_FRONT_AND_BACK, GL_LINE );

    // Enable line anti-aliasing
    glEnable( GL_LINE_SMOOTH );
    glEnable( GL_BLEND );
    glBlendFunc( GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA );

    // Setup orthogonal projection matrix
    glMatrixMode( GL_PROJECTION );
    glLoadIdentity();
    glOrtho( -3.0*aspect, 3.0*aspect, -3.0, 3.0, 1.0, 50.0 );

    // Upper left view (TOP VIEW)
    glViewport( 0, height/2, width/2, height/2 );
    glScissor( 0, height/2, width/2, height/2 );
    glMatrixMode( GL_MODELVIEW );
    glLoadIdentity();
    gluLookAt( 0.0f, 10.0f, 1e-3f,   // Eye-position (above)
               0.0f, 0.0f, 0.0f,     // View-point
               0.0f, 1.0f, 0.0f );   // Up-vector
    drawGrid( 0.5, 12 );
    drawScene();

    // Lower left view (FRONT VIEW)
    glViewport( 0, 0, width/2, height/2 );
    glScissor( 0, 0, width/2, height/2 );
    glMatrixMode( GL_MODELVIEW );
    glLoadIdentity();
    gluLookAt( 0.0f, 0.0f, 10.0f,    // Eye-position (in front of)
               0.0f, 0.0f, 0.0f,     // View-point
               0.0f, 1.0f, 0.0f );   // Up-vector
    drawGrid( 0.5, 12 );
    drawScene();

    // Lower right view (SIDE VIEW)
    glViewport( width/2, 0, width/2, height/2 );
    glScissor( width/2, 0, width/2, height/2 );
    glMatrixMode( GL_MODELVIEW );
    glLoadIdentity();
    gluLookAt( 10.0f, 0.0f, 0.0f,    // Eye-position (to the right)
               0.0f, 0.0f, 0.0f,     // View-point
               0.0f, 1.0f, 0.0f );   // Up-vector
    drawGrid( 0.5, 12 );
    drawScene();

    // Disable line anti-aliasing
    glDisable( GL_LINE_SMOOTH );
    glDisable( GL_BLEND );


    // ** PERSPECTIVE VIEW **

    // For perspective view, use solid rendering
    glPolygonMode( GL_FRONT_AND_BACK, GL_FILL );

    // Enable face culling (faster rendering)
    glEnable( GL_CULL_FACE );
    glCullFace( GL_BACK );
    glFrontFace( GL_CW );

    // Setup perspective projection matrix
    glMatrixMode( GL_PROJECTION );
    glLoadIdentity();
    gluPerspective( 65.0f, aspect, 1.0f, 50.0f );

    // Upper right view (PERSPECTIVE VIEW)
    glViewport( width/2, height/2, width/2, height/2 );
    glScissor( width/2, height/2, width/2, height/2 );
    glMatrixMode( GL_MODELVIEW );
    glLoadIdentity();
    gluLookAt( 3.0f, 1.5f, 3.0f,     // Eye-position
               0.0f, 0.0f, 0.0f,     // View-point
               0.0f, 1.0f, 0.0f );   // Up-vector

    // Configure and enable light source 1
    glLightfv( GL_LIGHT1, GL_POSITION, light_position );
    glLightfv( GL_LIGHT1, GL_AMBIENT, light_ambient );
    glLightfv( GL_LIGHT1, GL_DIFFUSE, light_diffuse );
    glLightfv( GL_LIGHT1, GL_SPECULAR, light_specular );
    glEnable( GL_LIGHT1 );
    glEnable( GL_LIGHTING );

    // Draw scene
    drawScene();

    // Disable lighting
    glDisable( GL_LIGHTING );

    // Disable face culling
    glDisable( GL_CULL_FACE );

    // Disable depth test
    glDisable( GL_DEPTH_TEST );

    // Disable scissor test
    glDisable( GL_SCISSOR_TEST );


    // Draw a border around the active view
    if( active_view > 0 && active_view != 2 )
    {
        glViewport( 0, 0, width, height );
        glMatrixMode( GL_PROJECTION );
        glLoadIdentity();
        glOrtho( 0.0, 2.0, 0.0, 2.0, 0.0, 1.0 );
        glMatrixMode( GL_MODELVIEW );
        glLoadIdentity();
        glColor3f( 1.0f, 1.0f, 0.6f );
        glTranslatef( (GLfloat) ((active_view - 1) & 1), (GLfloat) (1 - (active_view - 1) / 2), 0.0f );
        glBegin( GL_LINE_STRIP );
          glVertex2i( 0, 0 );
          glVertex2i( 1, 0 );
          glVertex2i( 1, 1 );
          glVertex2i( 0, 1 );
          glVertex2i( 0, 0 );
        glEnd();
    }
}


//========================================================================
// Window size callback function
//========================================================================

static void windowSizeFun( GLFWwindow window, int w, int h )
{
    width  = w;
    height = h > 0 ? h : 1;
    do_redraw = 1;
}


//========================================================================
// Window refresh callback function
//========================================================================

static void windowRefreshFun( GLFWwindow window )
{
    do_redraw = 1;
}


//========================================================================
// Mouse position callback function
//========================================================================

static void mousePosFun( GLFWwindow window, int x, int y )
{
    // Depending on which view was selected, rotate around different axes
    switch( active_view )
    {
        case 1:
            rot_x += y - ypos;
            rot_z += x - xpos;
            do_redraw = 1;
            break;
        case 3:
            rot_x += y - ypos;
            rot_y += x - xpos;
            do_redraw = 1;
            break;
        case 4:
            rot_y += x - xpos;
            rot_z += y - ypos;
            do_redraw = 1;
            break;
        default:
            // Do nothing for perspective view, or if no view is selected
            break;
    }

    // Remember mouse position
    xpos = x;
    ypos = y;
}


//========================================================================
// Mouse button callback function
//========================================================================

static void mouseButtonFun( GLFWwindow window, int button, int action )
{
    // Button clicked?
    if( ( button == GLFW_MOUSE_BUTTON_LEFT ) && action == GLFW_PRESS )
    {
        // Detect which of the four views was clicked
        active_view = 1;
        if( xpos >= width/2 )
        {
            active_view += 1;
        }
        if( ypos >= height/2 )
        {
            active_view += 2;
        }
    }

    // Button released?
    else if( button == GLFW_MOUSE_BUTTON_LEFT )
    {
        // Deselect any previously selected view
        active_view = 0;
    }

    do_redraw = 1;
}


//========================================================================
// main()
//========================================================================

int main( void )
{
    GLFWwindow window;

    // Initialise GLFW
    if( !glfwInit() )
    {
        fprintf( stderr, "Failed to initialize GLFW\n" );
        exit( EXIT_FAILURE );
    }

    glfwOpenWindowHint(GLFW_DEPTH_BITS, 16);

    // Open OpenGL window
    window = glfwOpenWindow( 500, 500, GLFW_WINDOWED, "Split view demo", NULL );
    if (!window)
    {
        fprintf( stderr, "Failed to open GLFW window\n" );
        glfwTerminate();
        exit( EXIT_FAILURE );
    }

    // Enable vsync
    glfwSwapInterval( 1 );

    // Enable sticky keys
    glfwEnable( window, GLFW_STICKY_KEYS );

    // Enable mouse cursor (only needed for fullscreen mode)
    glfwEnable( window, GLFW_MOUSE_CURSOR );

    // Set callback functions
    glfwSetWindowSizeCallback( window, windowSizeFun );
    glfwSetWindowRefreshCallback( window, windowRefreshFun );
    glfwSetMousePosCallback( window, mousePosFun );
    glfwSetMouseButtonCallback( window, mouseButtonFun );

    // Main loop
    do
    {
        // Only redraw if we need to
        if( do_redraw )
        {
            // Draw all views
            drawAllViews();

            // Swap buffers
            glfwSwapBuffers();

            do_redraw = 0;
        }

        // Wait for new events
        glfwWaitEvents();

    } // Check if the ESC key was pressed or the window was closed
    while( glfwIsWindow(window) &&
           glfwGetKey(window, GLFW_KEY_ESC) != GLFW_PRESS );

    // Close OpenGL window and terminate GLFW
    glfwTerminate();

    exit( EXIT_SUCCESS );
}