// main.cpp
// ========
// example of edge detection with laplacian filter (2nd-order derivative).
// The result images will be displayed on the screen using OpenGL.
//
//  AUTHOR: Song Ho Ahn (song.ahn@gmail.com)
// CREATED: 2005-08-31
// UPDATED: 2025-08-13
///////////////////////////////////////////////////////////////////////////////

#ifdef __APPLE__
#include <GLUT/glut.h>
#else
#include <GL/glut.h>
#endif

#include <cstdio>
#include <cstdlib>
#include <iostream>
#include <cmath>
#include <algorithm>        // for clamp()
#include "convolution.h"
#include "Timer.h"
#include "Bmp.h"


// GLUT CALLBACK functions
void displayCB();
void displaySubWin1CB();
void displaySubWin2CB();
void displaySubWin3CB();
void reshapeCB(int w, int h);
void reshapeSubWin1CB(int w, int h);
void reshapeSubWin2CB(int w, int h);
void reshapeSubWin3CB(int w, int h);
void keyboardHandlerCB(unsigned char key, int x, int y);

void initGL();
int  initGLUT(int argc, char **argv);
bool loadRawImage(char *fileName, int x, int y, unsigned char *data);
void drawString(const char *str, int x, int y, void *font);
void clearSharedMem();
void edgeDetect(int mode);

// global variables ////////////////
Image::Bmp bmp;
unsigned char   *inBuf1;
int             *inBuf2;
unsigned char   *outBuf1;
int             *outBuf2;
unsigned char   *outBuf3;
unsigned char   *outBuf4;
int             imageX;
int             imageY;
void            *font = GLUT_BITMAP_8_BY_13;
int             fontWidth = 8;
int             fontHeight = 13;
int             mainWin, subWin1, subWin2, subWin3, subWin4;
double          time1, time2;
int mode = 1;

// kernels
float kernelAvg1[] = {      // avg filter
    1/9.0f, 1/9.0f, 1/9.0f,
    1/9.0f, 1/9.0f, 1/9.0f,
    1/9.0f, 1/9.0f, 1/9.0f
};
float kernelAvg2[] = {      // avg filter separable
    1/3.0f, 1/3.0f, 1/3.0f
};

float kernelGauss1[] = {    // gaussian filter
    1/16.0f, 2/16.0f, 1/16.0f,
    2/16.0f, 4/16.0f, 2/16.0f,
    1/16.0f, 2/16.0f, 1/16.0f
};
float kernelGauss2[] = {    // gaussian filter separable
    1/4.0f, 2/4.0f, 1/4.0f
};

float kernelLaplace1[] = {  // laplacian filter
    0.0f, 1.0f, 0.0f,
    1.0f,-4.0f, 1.0f,
    0.0f, 1.0f, 0.0f
};
float kernelLaplace2[] = {  // negated laplacian filter
    0.0f,-1.0f, 0.0f,
   -1.0f, 4.0f,-1.0f,
    0.0f,-1.0f, 0.0f
};
float kernelLaplace3[] = {  // laplacian filter with diagonal
    1.0f, 1.0f, 1.0f,
    1.0f,-8.0f, 1.0f,
    1.0f, 1.0f, 1.0f
};


///////////////////////////////////////////////////////////////////////////////
int main(int argc, char **argv)
{
    // open bmp file
    //if(!bmp.read("shapes_black.bmp"))
    if(!bmp.read("lena_512.bmp"))
    {
        clearSharedMem();                       // exit program if failed to load image
        return 0;
    }
    inBuf1 = (unsigned char*)bmp.getData();
    imageX = bmp.getWidth();
    imageY = bmp.getHeight();
    bmp.printSelf();

    // init buffers
    inBuf2 = new int[imageX * imageY];
    outBuf1 = new unsigned char[imageX * imageY];
    outBuf2 = new int[imageX * imageY];
    outBuf3 = new unsigned char[imageX * imageY];
    outBuf4 = new unsigned char[imageX * imageY];

    // smooth input before applying filter
    Timer t;
    t.start();
    convolve2DSeparable(inBuf1, outBuf1, imageX, imageY, kernelGauss2, 3, kernelGauss2, 3);
    //convolve2DSeparable(inBuf1, outBuf1, imageX, imageY, kernelAvg2, 3, kernelAvg2, 3);
    t.stop();
    time1 = t.getElapsedTimeInMilliSec();
    printf("Smoothing: %f ms\n", time1);

    // copy input as int
    int count = imageX * imageY;
    for(int i = 0; i < count; ++i)
        inBuf2[i] = (int)outBuf1[i];

    // apply laplacian
    edgeDetect(mode);

    // drawing graphics from here /////////////////////////////////////////////
    // init GLUT
    mainWin = initGLUT(argc, argv);

    // register GLUT callback functions
    glutDisplayFunc(displayCB);
    glutReshapeFunc(reshapeCB);                 // subwindows do not need reshape call
    glutKeyboardFunc(keyboardHandlerCB);

    // 3 sub-windows
    // each sub-windows has its own openGL context, callbacks
    subWin1 = glutCreateSubWindow(mainWin, 0, 0, imageX, imageY);
    glutDisplayFunc(displaySubWin1CB);
    glutKeyboardFunc(keyboardHandlerCB);

    subWin2 = glutCreateSubWindow(mainWin, imageX, 0, imageX, imageY);
    glutDisplayFunc(displaySubWin2CB);
    glutKeyboardFunc(keyboardHandlerCB);

    subWin3 = glutCreateSubWindow(mainWin, 0, imageX*2, imageX, imageY);
    glutDisplayFunc(displaySubWin3CB);
    glutKeyboardFunc(keyboardHandlerCB);

    // turn off unused features
    initGL();

    // the last GLUT call (LOOP)
    // window will be shown and display callback is triggered by events
    // NOTE: this call never return main().
    glutMainLoop(); /* Start GLUT event-processing loop */

    return 0;
}



///////////////////////////////////////////////////////////////////////////////
// edge detect with laplacian filter
///////////////////////////////////////////////////////////////////////////////
void edgeDetect(int mode = 1)
{
    Timer t;
    t.start();

    float* kernel = kernelLaplace1;
    if(mode == 2) kernel = kernelLaplace2;
    else if(mode == 3) kernel = kernelLaplace3;

    convolve2D(inBuf2, outBuf2, imageX, imageY, kernel, 3, 3);
    // convert int to char
    int count = imageX * imageY;
    int max = 0, min = 0;
    for(int i = 0; i < count; ++i)
    {
        if(outBuf2[i] > max) max = outBuf2[i];
        if(outBuf2[i] < min) min = outBuf2[i];
        outBuf3[i] = (unsigned char)std::clamp(outBuf2[i] + 127, 0, 255);   // shift
        outBuf4[i] = (unsigned char)std::clamp(abs(outBuf2[i]), 0, 255);  // abs
    }

    t.stop();
    double time1 = t.getElapsedTimeInMilliSec();
    printf("Laplacian mode=%d: %f ms\n", mode, time1);

    printf("Min Max: %d, %d\n\n", min, max);
}



///////////////////////////////////////////////////////////////////////////////
// load 8-bit RAW image
///////////////////////////////////////////////////////////////////////////////
bool loadRawImage(char *fileName, int x, int y, unsigned char *data)
{
    // check params
    if(!fileName || !data)
        return false;

    FILE *fp;
    if((fp = fopen(fileName, "r")) == NULL)
    {
        printf("Cannot open %s.\n", fileName);
        return false;
    }

    // read pixel data
    fread(data, 1, x*y, fp);
    fclose(fp);

    return true;
}



///////////////////////////////////////////////////////////////////////////////
// initialize GLUT for windowing
///////////////////////////////////////////////////////////////////////////////
int initGLUT(int argc, char **argv)
{
    // GLUT stuff for windowing
    // initialization openGL window.
    // it is called before any other GLUT routine
    glutInit(&argc, argv);

    glutInitDisplayMode(GLUT_RGB | GLUT_DOUBLE);    // display mode

    glutInitWindowSize(3*imageX, imageY);           // window size

    glutInitWindowPosition(100, 100);               // window location

    // finally, create a window with openGL context
    // Window will not displayed until glutMainLoop() is called
    // it returns a unique ID
    int handle = glutCreateWindow(argv[0]);            // param is the title of window

    return handle;
}



///////////////////////////////////////////////////////////////////////////////
// initialize OpenGL
// disable unused features
///////////////////////////////////////////////////////////////////////////////
void initGL()
{
    glClearColor(0, 0, 0, 0);
    glShadeModel(GL_FLAT);  // shading mathod: GL_SMOOTH or GL_FLAT
    glPixelStorei(GL_UNPACK_ALIGNMENT, 1);

    // TUNNING IMAGING PERFORMANCE
    // turn off  all pixel path and per-fragment operation
    // which slow down OpenGL imaging operation (glDrawPixels glReadPixels...).
    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_STENCIL_TEST);
    glDisable(GL_TEXTURE_1D);
    glDisable(GL_TEXTURE_2D);
}



///////////////////////////////////////////////////////////////////////////////
// write 2d text using GLUT
///////////////////////////////////////////////////////////////////////////////
void drawString(const char *str, int x, int y, void *font)
{
    glRasterPos2i(x, y);

    // loop all characters in the string
    while(*str)
    {
        glutBitmapCharacter(font, *str);
        ++str;
    }
}



///////////////////////////////////////////////////////////////////////////////
// clean up shared memory
///////////////////////////////////////////////////////////////////////////////
void clearSharedMem()
{
    delete [] inBuf2;
    delete [] outBuf1;
    delete [] outBuf2;
    delete [] outBuf3;
}



//=============================================================================
// CALLBACKS
//=============================================================================

void displayCB()
{
    glutSetWindow(mainWin);
    glClear(GL_COLOR_BUFFER_BIT);
    glutSwapBuffers();
}

void displaySubWin1CB(void)
{
    glutSetWindow(subWin1);
    glClear(GL_COLOR_BUFFER_BIT); // clear canvas

    // specify current raster position(coordinate)
    glRasterPos2i(0, imageY);  // upper-left corner in openGL coordinates
    glPixelZoom(1.0, -1.0);

    glDrawPixels(imageX, imageY, GL_LUMINANCE, GL_UNSIGNED_BYTE, outBuf1);

    glColor3f(1,1,0);
    drawString("Input (Smoothed)", 1, imageY - fontHeight, font);

    glutSwapBuffers();
}

void displaySubWin2CB()
{
    glutSetWindow(subWin2);
    glClear(GL_COLOR_BUFFER_BIT); // clear canvas

    // specify current raster position(coordinate)
    glRasterPos2i(0, imageY);
    glPixelZoom(1.0, -1.0);

    glDrawPixels(imageX, imageY, GL_LUMINANCE, GL_UNSIGNED_BYTE, outBuf3);

    glColor3f(1,1,0);
    drawString("Laplacian", 1, imageY - fontHeight, font);
    if(mode == 1)
        drawString("Mode: 1", 1, imageY - (2*fontHeight), font);
    else if(mode == 2)
        drawString("Mode: 2", 1, imageY - (2*fontHeight), font);
    else if(mode == 3)
        drawString("Mode: 3", 1, imageY - (2*fontHeight), font);

   glutSwapBuffers();
}

void displaySubWin3CB(void)
{
    glutSetWindow(subWin3);
    glClear(GL_COLOR_BUFFER_BIT); // clear canvas

    // specify current raster position(coordinate)
    glRasterPos2i(0, imageY);
    glPixelZoom(1.0, -1.0);

    glDrawPixels(imageX, imageY, GL_LUMINANCE, GL_UNSIGNED_BYTE, outBuf4);

    glColor3f(1,1,0);
    drawString("Magnitude", 1, imageY - fontHeight, font);

    glutSwapBuffers();
}

void reshapeCB(int w, int h)
{
    // set viewport to be the entire window
    glViewport(0, 0, (GLsizei)w, (GLsizei)h);

    // left sub-window
    glutSetWindow(subWin1);
    glutPositionWindow(0,0);
    glutReshapeWindow(imageX, imageY);
    glViewport(0, 0, imageX, imageY);
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    gluOrtho2D(0, imageX, 0, imageY);

    // middle sub-window
    glutSetWindow(subWin2);
    glutPositionWindow(imageX, 0);
    glutReshapeWindow(imageX, imageY);
    glViewport(0, 0, imageX, imageY);
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    gluOrtho2D(0, imageX, 0, imageY);

    //right sub-window
    glutSetWindow(subWin3);
    glutPositionWindow(imageX*2, 0);
    glutReshapeWindow(imageX, imageY);
    glViewport(0, 0, imageX, imageY);
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    gluOrtho2D(0, imageX, 0, imageY);

    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
}


void keyboardHandlerCB(unsigned char key, int x, int y)
{
    switch(key)
    {
    case 27: // ESCAPE
        clearSharedMem();
        exit(0);
        break;

    case 'r':
    case 'R':
        break;

    case ' ':
        ++mode;
        if(mode > 3)
            mode = 1;
        edgeDetect(mode);
        break;

    default:
        ;
    }
    glutPostWindowRedisplay(subWin1);
    glutPostWindowRedisplay(subWin2);
    glutPostWindowRedisplay(subWin3);
}

