/*---------------------------------------------------------------------
 *  Implementation file for TGS Background Image Node
 *
 *  Copyright (C) 1997,1998 Template Graphics Software Inc.
 *  All Rights Reserved
 *
 *  Permission is granted for licensed users of the Open Inventor SDK
 *  to use this code as long as the TGS copyright statement remains
 *  in the file.
 *
 *  Created: mmh 24-Sep-97
 *  Updated: mmh 24-Nov-97 - Chg to use texture mapping for stretched
 *                           and tiled cases (should be faster on S3,
 *                           or other accelerated boards)
 *           jsg 24-Nov-97 - Chg to make image persistent
 *           mmh 28-Nov-97 - Reconcile 24-Nov changes by incorporating
 *                           glRender code from SoTexture2
 *
 *  Note1: Only tested on Win32 (should be portable to UNIX).
 *
 *  Note2: Basically this node must be the first one in the scene.
 *-------------------------------------------------------------------*/

 // Issue: When, if ever, should we release the display list (texture
 //        object) containing our background texture?  Functionally
 //        not a problem, but does use memory.  It will only be created
 //        if a render occurs in Stretched or Tiled modes.  Currently
 //        it is released if mode is changed to NONE so there is a way
 //        for a user to free this memory.
 //
 // Issue: When using texturing to apply image, should we take into
 //        account the current background color setting?  Currently
 //        we always modulate against a white polygon.  Using backgrd
 //        color adds flexibility but also could be confusing.
 //
 // ToDo: Get rid of this ugly hack for reading image files, using a
 //       slave SoTexture2 node.  Ideally the image file read utility
 //       functions would be statically available somewhere so all the
 //       nodes that need this service could share them.

#include <Inventor/nodes/SoTexture2.h>
#include <Inventor/actions/SoGLRenderAction.h>
#include <Inventor/elements/SoGLCacheContextElement.h>
#include <Inventor/elements/SoTextureQualityElement.h>
#include <Inventor/elements/SoGLTextureEnabledElement.h>
#include <Inventor/elements/SoGLTextureImageElement.h>
#include <Inventor/sensors/SoFieldSensor.h>

// Make sure the OpenGL libraries are linked in
#ifdef WIN32
#pragma comment(lib,"GLU32.lib")
#pragma comment(lib,"OpenGL32.lib")
#endif

#include <GL/glu.h> // for gluScaleImage
#include <GL/gl.h>

#include "SoBackground.h"

SO_NODE_SOURCE(SoBackgroundImage);

#ifdef _DEBUG
#include <crtdbg.h>
#define CLEAR_GL_ERROR glGetError();
#define CHECK_DRAWPIXELS_ERROR { \
        GLenum gldrawpixelserror = glGetError(); \
        _ASSERT( gldrawpixelserror == GL_NO_ERROR ); }
#else
#define CLEAR_GL_ERROR
#define CHECK_DRAWPIXELS_ERROR
#endif

////////////////////////////////////////////////////////////////////////
//
// Initializes the SoBackgroundImage class. This is a one-time thing that is
// done after database initialization and before any instance of
// this class is constructed.
//
void
SoBackgroundImage::initClass()
{
   // Initialize type id variables. The arguments to the macro
   // are: the name of the node class, the class this is derived
   // from, and the name registered with the type of the parent
   // class.
   SO_NODE_INIT_CLASS(SoBackgroundImage, SoNode, "Node");
}


////////////////////////////////////////////////////////////////////////
//
// Constructor
//
SoBackgroundImage::SoBackgroundImage()
{
   // Do standard constructor stuff
   SO_NODE_CONSTRUCTOR(SoBackgroundImage);

   // Setup fields
   SO_NODE_ADD_FIELD(filename, (""));
   SO_NODE_ADD_FIELD(style, (CENTER));
   SO_NODE_ADD_FIELD(image, (SbVec2s(0,0),0,NULL));

   SO_NODE_ADD_FIELD(rememberFilename, (""));   //MMH Oct-98 Hack for Chrome

   // Set up enumerations for texture model
   SO_NODE_DEFINE_ENUM_VALUE(Style, NONE);
   SO_NODE_DEFINE_ENUM_VALUE(Style, CENTER);
   SO_NODE_DEFINE_ENUM_VALUE(Style, LOWER_LEFT);
   SO_NODE_DEFINE_ENUM_VALUE(Style, UPPER_LEFT);
   SO_NODE_DEFINE_ENUM_VALUE(Style, UPPER_RIGHT);
   SO_NODE_DEFINE_ENUM_VALUE(Style, LOWER_RIGHT);
   SO_NODE_DEFINE_ENUM_VALUE(Style, STRETCH);
   SO_NODE_DEFINE_ENUM_VALUE(Style, TILE);

   SO_NODE_SET_SF_ENUM_TYPE(style, Style);

   // Initialize miscellaneous members
   texNode = NULL;
   
   imWidth = imHeight = imComps = imFormat = 0;
   imPixels = NULL;   
   im2Width = im2Height = 0;
   im2Pixels = NULL;   

   renderList = NULL;  // Display list used for rendering

    // Set up sensors to keep the image/filename fields agreeing.
    // Sensors are used instead of field to field connections or raw
    // notification so that the fields can still be attached to/from
    // other fields.
    imageSensor = new SoFieldSensor(imageChangedCB, this);
    imageSensor->setPriority(0);
    imageSensor->attach(&image);
    filenameSensor = new SoFieldSensor(filenameChangedCB, this);
    filenameSensor->setPriority(0);
    filenameSensor->attach(&filename);
}


////////////////////////////////////////////////////////////////////////
//
// Destructor
//
SoBackgroundImage::~SoBackgroundImage()
{
    // Cleanup
    if (renderList) {
        renderList->unref();
        renderList = NULL;
    }

    // Note: imPixels points to the image held in the SoTexture2 node,
    //       so it would be a bad idea to explicitly delete it.
    if (texNode != NULL) {
        texNode->unref();
        texNode = NULL;
    }
    delete [] im2Pixels;

    delete imageSensor;
    delete filenameSensor;
}


////////////////////////////////////////////////////////////////////////
void
SoBackgroundImage::imageChangedCB(void *data, SoSensor *)
{
    SoBackgroundImage *bkgd = (SoBackgroundImage *)data;

    if (bkgd->image.isIgnored()) return;

    bkgd->filenameSensor->detach();
    bkgd->filename.setValue("");
    bkgd->filename.setDefault(TRUE);
    bkgd->filenameSensor->attach(&bkgd->filename);

    // Freeing the renderList signals render action to process new image
    bkgd->imPixels = NULL;
    if (bkgd->im2Pixels) {
        delete [] bkgd->im2Pixels;
        bkgd->im2Pixels = NULL;
    }
    if (bkgd->renderList) {
    	bkgd->renderList->unref();
	    bkgd->renderList = NULL;
    }
}


////////////////////////////////////////////////////////////////////////
void
SoBackgroundImage::filenameChangedCB(void *data, SoSensor *)
{
    SoBackgroundImage *bkgd = (SoBackgroundImage *)data;

    bkgd->imPixels = NULL;
    if (bkgd->im2Pixels) {
        delete [] bkgd->im2Pixels;
        bkgd->im2Pixels = NULL;
    }
    if (bkgd->renderList) {
        bkgd->renderList->unref();
        bkgd->renderList = NULL;
    }
}


/////////////////////////////////////////////////////////////////////////
//
// Implement GL render action.
//
// Note: Currently we don't pay attention to SoGLTextureEnabledElement.
//       Even though we now use texturing in the stretched and tiled
//       cases, we think end users would be surprised if disabling
//       texturing turned off the background image.  Possibly, disable
//       texturing should mean to go back to using glDrawPixels?

void
SoBackgroundImage::GLRender(SoGLRenderAction *action)
{
    int rc;

    // Don't auto-cache above background nodes
    //
    // This ensures we can detect when the window is resized and
    // adjust the Stretch and Tile behavior.
    //
    // Experiments showed that (somewhat surprisingly) caching
    // the background bitmap did not noticeably improve performance.
    SoGLCacheContextElement::shouldAutoCache(action->getState(), 
        SoGLCacheContextElement::DONT_AUTO_CACHE);

    // Get the current image filename
    const SbString fname = filename.getValue();

    // Get current texture quality setting
    SoState *state = action->getState();
    float texQuality = SoTextureQualityElement::get( state );

    // If zero quality or no image file or bkgd image turned off, nothing to do
    //
    // Note: Doing the texQuality check here means that setting MoveAs to
    //       NoTextures in the viewer will also disable the backgd image.
    //       This is a questionable overriding of functionality, but it's
    //       very useful to be able to turn off the background image for
    //       better performance.
    //
    // Note: If bkgd image is turned off (style NONE), we free the cached
    //       image (display list or texture object) in OpenGL to save
    //       memory.  We still have a copy of the image in our memory.
    int styleVal = style.getValue();
    if (styleVal == NONE) {
        if (renderList) {
	    renderList->unref();
	    renderList = NULL;
        }
        return;
    }
    if (texQuality == 0.0)
        return;

    // Check if image filename has changed
    if (imPixels == NULL && fname.getLength()) {

        // Create texture node to use for loading image files
        if (texNode == NULL) {
            texNode = new SoTexture2;
            texNode->ref();
        }

        // Load image
        texNode->filename = fname;

        rememberFilename = fname; //MMH Oct-98 Hack for ExportAsChrome

        // Use SoTexture2 node to actually load texture image.
        // Copy texture image and release memory in SoTexture2 node.
        // Note: Setting image field clears *our* filename field!
        image = texNode->image;
        texNode->filename.setValue("");
    }

    // Get ptr to image bits in the image field
    SbVec2s imSize;
    imPixels = (unsigned char*)image.getValue( imSize, imComps );
    if (imPixels != NULL) {
        imWidth  = imSize[0];
        imHeight = imSize[1];
    }
    if (imComps == 1)
        imFormat = GL_LUMINANCE;
    else if (imComps == 2)
        imFormat = GL_LUMINANCE_ALPHA;
    else if (imComps == 4)
        imFormat = GL_RGBA;
    else
        imFormat = GL_RGB;  // Default

    // Bail if no image to display
    if (imPixels == NULL)
        return;

    if (im2Pixels == NULL) {
        im2Width = im2Height = 0;
    }

    // Kind of a hack...
    // For now, always use texture mapping for Tiled and Stretch modes.
    // (previously was always set to 1, which skipped some setup which
    // might be relevant to performance) -- mmh 05/98
    static int useTextures = (styleVal == TILE || styleVal == STRETCH);

    SoGLRenderAction *pGLRA = (SoGLRenderAction*)action;
    const SbViewportRegion vport = pGLRA->getViewportRegion();
    const SbVec2s vpSize = vport.getViewportSizePixels();
    short vpWidth, vpHeight;
    vpSize.getValue( vpWidth, vpHeight );
#ifdef _DEBUG
    int glvp[4];
    glGetIntegerv( GL_VIEWPORT, glvp ); //test
#endif

    // Save OpenGL state and setup for bitmap rendering
    glPushAttrib( GL_ENABLE_BIT | GL_DEPTH_BUFFER_BIT | GL_PIXEL_MODE_BIT );

    // Setup optimal OpenGL state for drawing
    glDisable(GL_ALPHA_TEST);
    glDisable(GL_BLEND);
    glDisable(GL_DEPTH_TEST);
    glDisable(GL_DITHER);
    glDisable(GL_FOG);
    glDisable(GL_LIGHTING);
    glDisable(GL_LOGIC_OP);
    glDisable(GL_POLYGON_SMOOTH);
    glDisable(GL_POLYGON_STIPPLE);
    glDisable(GL_STENCIL_TEST);
    glDisable(GL_TEXTURE_1D);

    // Setup optimal OpenGL state for glDrawPixels (if needed)
    if (!useTextures) {
        glDisable(GL_TEXTURE_2D);
        glPixelTransferi(GL_MAP_COLOR, GL_FALSE);
        glPixelTransferi(GL_RED_SCALE, 1);
        glPixelTransferi(GL_RED_BIAS, 0);
        glPixelTransferi(GL_GREEN_SCALE, 1);
        glPixelTransferi(GL_GREEN_BIAS, 0);
        glPixelTransferi(GL_BLUE_SCALE, 1);
        glPixelTransferi(GL_BLUE_BIAS, 0);
        glPixelTransferi(GL_ALPHA_SCALE, 1);
        glPixelTransferi(GL_ALPHA_BIAS, 0);
        glPixelStorei( GL_UNPACK_ALIGNMENT, 1 );
    }
    glDepthMask( 0 );           // don't update Z buffer

    glMatrixMode( GL_MODELVIEW );
    glPushMatrix();
    glLoadIdentity();
    glMatrixMode( GL_PROJECTION );
    glPushMatrix();
    glLoadIdentity();

    // TODO?
    // Possibly we should also set GL_DRAW_BUFFER to be safe...

    // Even using glRasterPos we need this if we want 0,0 to be the
    // lower left corner of the window (otherwise it's at the center)
    glOrtho( 0., vpWidth, 0., vpHeight, 0., 1. );

    int xpos = 0;
    int ypos = 0;

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    // First check for the easy "justified" cases where image is
    // simply blt'd into the window.  These are always done with
    // glDrawPixels and will generally be small images.

    if (styleVal == CENTER) {    // Center
        xpos = (vpWidth / 2) - (imWidth / 2);
        ypos = (vpHeight / 2) - (imHeight / 2);
        if (xpos < 0) xpos = 0; // Don't allow invalid raster pos
        if (ypos < 0) ypos = 0;
        glRasterPos2i( xpos, ypos );
#ifdef _DEBUG
        int glcrp[4];
        glGetIntegerv( GL_CURRENT_RASTER_POSITION, glcrp ); //test
#endif
        CLEAR_GL_ERROR;
        glDrawPixels( imWidth, imHeight, imFormat,
                      GL_UNSIGNED_BYTE, imPixels );
        CHECK_DRAWPIXELS_ERROR;
    }

    else if (styleVal == LOWER_LEFT) {
        glRasterPos2i( 0, 0 );
        CLEAR_GL_ERROR;
        glDrawPixels( imWidth, imHeight, imFormat,
                      GL_UNSIGNED_BYTE, imPixels );
        CHECK_DRAWPIXELS_ERROR;
    }

    else if (styleVal == UPPER_LEFT) {
        ypos = vpHeight - imHeight;
        if (ypos < 0) ypos = 0;
        glRasterPos2i( 0, ypos );
        CLEAR_GL_ERROR;
        glDrawPixels( imWidth, imHeight, imFormat,
                      GL_UNSIGNED_BYTE, imPixels );
        CHECK_DRAWPIXELS_ERROR;
    }

    else if (styleVal == UPPER_RIGHT) {
        xpos = vpWidth - imWidth;
        ypos = vpHeight - imHeight;
        if (xpos < 0) xpos = 0;
        if (ypos < 0) ypos = 0;
        glRasterPos2i( xpos, ypos );
        CLEAR_GL_ERROR;
        glDrawPixels( imWidth, imHeight, imFormat,
                      GL_UNSIGNED_BYTE, imPixels );
        CHECK_DRAWPIXELS_ERROR;
    }

    else if (styleVal == LOWER_RIGHT) {
        xpos = vpWidth - imWidth;
        if (xpos < 0) xpos = 0;
        glRasterPos2i( xpos, 0 );
        CLEAR_GL_ERROR;
        glDrawPixels( imWidth, imHeight, imFormat,
                      GL_UNSIGNED_BYTE, imPixels );
        CHECK_DRAWPIXELS_ERROR;
    }

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    // Else mode must be STRETCH or TILE
    else if (styleVal == STRETCH || styleVal == TILE) {

        // Setup to use image as texture map
        //
        // We want to use all the nifty machinery built into the
        // SoGLTextureImageElement, including automatic adjustment
        // of image size, caching image as a texture object, etc.
        //
        // We are already pushing and popping a bunch of OpenGL state,
        // but we need to manage the OIV state also, so primitives
        // downstream don't end up textured with our image.

        state->push();

        if (useTextures) {
            // Enable texture mapping
            SoGLTextureEnabledElement::set( state, TRUE );

            // Adjust quality setting
            // For Tiled mode it doesn't even make sense to switch from
            // point sampling (GL_NEAREST) to interpolation (GL_LINEAR)
            // because the whole point is draw copies of the texture
            // image "actual size".
            //
            // For Stretch mode, interpolation looks much nicer (altho
            // it is much slower with software rendering).
            float texQuality = SoTextureQualityElement::get( state );
            if (styleVal == TILE && texQuality > 0.5) {
                SoTextureQualityElement::set( state, 0.5 ); // don't interpolate
                texQuality = 0.5;
            }

            // See if renderList is valid (in the right context, with
            // the right texture quality):
            int context = SoGLCacheContextElement::get(state);
            if (renderList &&
                renderList->getContext() == context &&
	            texQuality == renderListQuality       ) {

	            SoGLTextureImageElement::set(
                    state, this, SbVec2s(imWidth,imHeight),
                    imComps, imPixels, texQuality,
                    SoTexture2::REPEAT, SoTexture2::REPEAT, 
                    SoTexture2::MODULATE, SbColor(1,1,1), renderList );
            }  // Not valid, try to build
            else {
	            // Free up old list, if necessary:
	            if (renderList) {
                    renderList->unref(state);
                    renderList = NULL;
	            }
	            renderList = SoGLTextureImageElement::set(
                    state, this, SbVec2s(imWidth,imHeight),
                    imComps, imPixels, texQuality,
                    SoTexture2::REPEAT, SoTexture2::REPEAT, 
                    SoTexture2::MODULATE, SbColor(1,1,1), NULL);
	            if (renderList)
                    renderList->ref();
	            renderListQuality = texQuality;
            }
        }

        // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
        if (styleVal == STRETCH) {   // Stretch
            if (!useTextures) {
                if (texQuality <= 0.5) {
                    // To save memory, scale the pixels every time
                    //
                    // Oddly enough, at least on a P90 with SW rendering, this is
                    // faster than bliting a larger image without pixel zoom. Hmm.
                    //
                    // This technique produces a lower quality image than scaling
                    // up the image with glu.
                    float xfact = (float)vpWidth / (float)imWidth;
                    float yfact = (float)vpHeight / (float)imHeight;
                    glRasterPos2i( 0, 0 );
                    glPixelZoom( xfact, yfact );
                    glDrawPixels( imWidth, imHeight, imFormat, GL_UNSIGNED_BYTE, imPixels );
                }
                else {  // Higher image quality
                    // For better performance (?), generate a scaled image
                    //
                    // This technique appears to actually be slower, but it does
                    // produce a much nicer looking image.
                    glPixelStorei( GL_PACK_ALIGNMENT, 0 );

                    // Throw away current image if viewport size changed
                    if (vpWidth != im2Width || vpHeight != im2Height) {
                        if (im2Pixels) {
                            delete [] im2Pixels;
                            im2Pixels = NULL;
                        }
                    }

                    // Create scaled image if necessary
                    if (im2Pixels == NULL) {
                        int numBytes = vpWidth * vpHeight * imComps * sizeof(GLubyte);
                        im2Pixels = new unsigned char[numBytes];
                        im2Width  = vpWidth;
                        im2Height = vpHeight;

                        rc = gluScaleImage( imFormat,
                             imWidth, imHeight, GL_UNSIGNED_BYTE, imPixels,
                             vpWidth, vpHeight, GL_UNSIGNED_BYTE, (void*)im2Pixels );
                    }
                    glRasterPos2i( 0, 0 );
                    glDrawPixels( vpWidth, vpHeight, imFormat,
                                  GL_UNSIGNED_BYTE, im2Pixels );
                }
            } //endif !useTextures
            else {
                // Draw some geometry to be textured
                int xmax = vpWidth;
                int ymax = vpHeight;
                glBegin( GL_TRIANGLE_STRIP );
                    glColor3f   ( 1, 1, 1 );
                    glTexCoord2i( 0, 0 );
                    glVertex2i  ( 0, 0 );
                    glTexCoord2i( 1, 0 );
                    glVertex2i  ( xmax, 0 );
                    glTexCoord2i( 0, 1 );
                    glVertex2i  ( 0, ymax );
                    glTexCoord2i( 1, 1 );
                    glVertex2i  ( xmax, ymax );
                glEnd();
            } //endif useTextures
        } //endif StretchMode

        // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
        else {  // Tile
            if (!useTextures) {
                glRasterPos2i( 0, 0 );
    #ifdef _DEBUG
                int glcrp[4];
                glGetIntegerv( GL_CURRENT_RASTER_POSITION, glcrp ); //test
    #endif
                glDrawPixels( imWidth, imHeight, imFormat,
                              GL_UNSIGNED_BYTE, imPixels );
                for (int iy = 0; iy < vpHeight; iy += imHeight) {
                    for (int ix = 0; ix < vpWidth; ix += imWidth) {
                        if (ix + iy == 0) continue;
                        glRasterPos2i( ix, iy );
    #ifdef _DEBUG
                        glGetIntegerv( GL_CURRENT_RASTER_POSITION, glcrp ); //test
    #endif
                        glCopyPixels( 0,0, imWidth, imHeight, GL_COLOR );
                    }
                }
            } //endif !useTextures
            else {
                // Compute right/top texture coords s,t
                // Note: Should be <1 if image wider than viewport.  MMH Dec-98
                float smax, tmax;
                smax = (float)vpWidth / (float)imWidth;
                tmax = (float)vpHeight / (float)imHeight;

                // Draw some geometry to be textured
                float xmax = vpWidth;
                float ymax = vpHeight;
                glBegin( GL_TRIANGLE_STRIP );
                    glColor3f   ( 1, 1, 1 );
                    glTexCoord2f( 0, 0 );
                    glVertex3f  ( 0, 0, 0 );
                    glTexCoord2f( smax, 0 );
                    glVertex3f  ( xmax, 0, 0 );
                    glTexCoord2f( 0, tmax );
                    glVertex3f  ( 0, ymax, 0 );
                    glTexCoord2f( smax, tmax );
                    glVertex3f  ( xmax, ymax, 0 );
                glEnd();
            } //endif useTextures
        } //endif STRETCH mode

        // Restore OIV traversal state to previous texture info
        state->pop();

    } //endif STRETCH or TILE mode
    
    else {
        // Some mode we didn't recognize!
    }

    // Restore OpenGL state
    glPopMatrix();
    glMatrixMode( GL_MODELVIEW );
    glPopMatrix();
    glPopAttrib();
}
