// simple Sobel Filter example
// Author: Michel Steuwer (michel.steuwer@wwu.de)
// The hole code is based on the Sobel Filter example of the NVIDIA CUDA SDK

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <GL/glew.h>
#if defined(__APPLE__) || defined(MACOSX)
#include <GLUT/glut.h>
#else
#include <GL/glut.h>
#endif

#include <cuda_runtime.h>
#include <cutil_inline.h>
#include <cuda_gl_interop.h>

#include "SobelFilter_kernels.h"

static int wWidth   = 512; // Window width
static int wHeight  = 512; // Window height
static int imWidth  = 0;   // Image width
static int imHeight = 0;   // Image height

unsigned int Bpp;				// Bits per pixel
static GLuint buffer = 0;		// Buffer to write in
static GLuint texture = 0;		// Texture for display (bind to the buffer)
unsigned char * pixels = NULL;	// Image pixel data on the host

float brightness = 1.f;			// Image exposure
int sobelWidth = 256;			// specifies the columns effected by the filter
enum SobelDisplayMode sobelDisplayMode;

#define OFFSET(i) ((char *)NULL + (i))

// display function
void display(void) 
{
	// Sobel operation
    Pixel *data = NULL;
    cutilSafeCall(cudaGLMapBufferObject((void**)&data, buffer));
    sobelFilter(data, imWidth, imHeight, sobelDisplayMode, brightness, sobelWidth );
    cutilSafeCall(cudaGLUnmapBufferObject(buffer));

	// bind buffer to texture and draw texture
	
    glClear(GL_COLOR_BUFFER_BIT);
	
    glBindTexture(GL_TEXTURE_2D, texture);
    glBindBuffer(GL_PIXEL_UNPACK_BUFFER, buffer);
    glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, imWidth, imHeight, 
                   GL_LUMINANCE, GL_UNSIGNED_BYTE, OFFSET(0));
    glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
	
    glDisable(GL_DEPTH_TEST);
    glEnable(GL_TEXTURE_2D);
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
	
    glBegin(GL_QUADS);
    glVertex2f(0, 0); glTexCoord2f(0, 0);
    glVertex2f(0, 1); glTexCoord2f(1, 0);
    glVertex2f(1, 1); glTexCoord2f(1, 1);
    glVertex2f(1, 0); glTexCoord2f(0, 1);
    glEnd();
    glBindTexture(GL_TEXTURE_2D, 0);
	
    glutSwapBuffers();
	
    glutPostRedisplay();
}

void idle(void) {
	
}

// keyboar function
void keyboard( unsigned char key, int /*x*/, int /*y*/) 
{
	char temp[256];
	
    switch( key) {
	// press 'ESC' to exit
	case 27:
		exit (0);
	    break;
	// '-' and '+' to adjust brightness of the output image
	case '-':
	    brightness -= 0.1f;
	    break;
	case '+':
	    brightness += 0.1f;
	    break;
	// ',' and '.' to adjust the area, with is affected by the filter
	case ',':
		if (sobelWidth >= 4)
	    	sobelWidth -= 4;
	    break;
	case '.':
		if (sobelWidth <= imWidth-4)
	    	sobelWidth += 4;
	    break;
	// use 'i', 's' and 'f' to chage the display mode
	case 'i': 
    case 'I':
	    sobelDisplayMode = SOBELDISPLAY_IMAGE;
	    sprintf(temp, "Cuda Edge Detection (%s)", filterMode[sobelDisplayMode]);  
	    glutSetWindowTitle(temp);
	    break;
	case 's': 
    case 'S':
	    sobelDisplayMode = SOBELDISPLAY_SOBEL;
	    sprintf(temp, "Cuda Edge Detection (%s)", filterMode[sobelDisplayMode]);  
	    glutSetWindowTitle(temp);
		break;
	case 'f': 
	case 'F':
	    sobelDisplayMode = SOBELDISPLAY_SOBEL_FULL;
	    sprintf(temp, "Cuda Edge Detection (%s)", filterMode[sobelDisplayMode]);  
	    glutSetWindowTitle(temp);
	    default: break;
    }
    glutPostRedisplay();
}

// reshape function
void reshape(int x, int y) {
    glViewport(0, 0, x, y);
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    glOrtho(0, 1, 0, 1, 0, 1); 
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
    glutPostRedisplay();
}

// cleaup function
void cleanup(void) {	
    cutilSafeCall(cudaGLUnregisterBufferObject(buffer));

    glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
    glDeleteBuffers(1, &buffer);
    glDeleteTextures(1, &texture);
    deleteTexture();
}

// loads the given file as a image
void loadImage(char *file) {
    unsigned int w, h;
    size_t file_length= strlen(file);

    if (!strcmp(&file[file_length-3], "pgm")) {
        if (cutLoadPGMub(file, &pixels, &w, &h) != CUTTrue) {
            printf("Failed to load image file: %s\n", file);
            exit(-1);
        }
        Bpp = 1;
    } else if (!strcmp(&file[file_length-3], "ppm")) {
        if (cutLoadPPM4ub(file, &pixels, &w, &h) != CUTTrue) {
            printf("Failed to load image file: %s\n", file);
            exit(-1);
        }
        Bpp = 4;
    } else {
        cudaThreadExit();
        exit(-1);
    }
    imWidth = (int)w;
	imHeight = (int)h;
}

// setup the defualt image_path and calls loadImage
void loadDefaultImage( char* execution_path) {
    printf("Reading image: lena.pgm\n");
    const char* image_filename = "lena.pgm";
    char* image_path = cutFindFilePath(image_filename, execution_path);
    if (image_path == 0) {
       printf( "Reading image failed.\n");
       exit(EXIT_FAILURE);
    }
    loadImage( image_path );
    cutFree( image_path );
}

// create and init buffer and texture
void initBufferAndTexture() {
	GLint bsize;
	// setup texture used by CUDA see SobelFilter_kernels.cu for details
    setupTexture(imWidth, imHeight, pixels, Bpp);
    memset(pixels, 0x0, Bpp * sizeof(Pixel) * imWidth * imHeight);
	
	// create buffer
    glGenBuffers(1, &buffer);
    glBindBuffer(GL_PIXEL_UNPACK_BUFFER, buffer); 
    glBufferData(GL_PIXEL_UNPACK_BUFFER, 
                    Bpp * sizeof(Pixel) * imWidth * imHeight, 
                    pixels, GL_STREAM_DRAW);  
    
    glGetBufferParameteriv(GL_PIXEL_UNPACK_BUFFER, GL_BUFFER_SIZE, &bsize); 
    if ((GLuint)bsize != (Bpp * sizeof(Pixel) * imWidth * imHeight)) {
        printf("Buffer object (%d) has incorrect size (%d).\n", (unsigned)buffer, (unsigned)bsize);
        cudaThreadExit();
        exit(-1);
    }
    
    glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
	// CUDA: register buffer als cuda object
    cutilSafeCall(cudaGLRegisterBufferObject(buffer));
    
	// create texture for drawing
    glGenTextures(1, &texture);
    glBindTexture(GL_TEXTURE_2D, texture);
    glTexImage2D(GL_TEXTURE_2D, 0, ((Bpp==1) ? GL_LUMINANCE : GL_BGRA), 
                imWidth, imHeight,  0, GL_LUMINANCE, GL_UNSIGNED_BYTE, NULL);
    glBindTexture(GL_TEXTURE_2D, 0);
    
    glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
    glPixelStorei(GL_PACK_ALIGNMENT, 1);
}

// init OpenGL
void initGL(int argc, char** argv)
{
    glutInit( &argc, argv);    
    glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE);
    glutInitWindowSize(wWidth, wHeight);
    glutCreateWindow("Cuda Edge Detection");

    glewInit();
	
	if (!glewIsSupported( "GL_VERSION_1_5 GL_ARB_vertex_buffer_object GL_ARB_pixel_buffer_object" )) {
		fprintf(stderr, "Error: failed to get minimal extensions for demo\n");
		fprintf(stderr, "This sample requires:\n");
		fprintf(stderr, "  OpenGL version 1.5\n");
		fprintf(stderr, "  GL_ARB_vertex_buffer_object\n");
		fprintf(stderr, "  GL_ARB_pixel_buffer_object\n");
        cudaThreadExit();
		exit(-1);
	}
}

int main(int argc, char** argv) 
{
    if (argc > 1) {
		// print help
        if (cutCheckCmdLineFlag(argc, (const char **)argv, "help")) {
            printf("\nUsage: SobelFilter <options>\n");
		    printf("\t\t-file = filename.pgm (image files for input)\n\n");
			exit(1);
        }
    }

    // First initialize OpenGL context, so we can properly set the GL for CUDA.
    // This is necessary in order to achieve optimal performance with OpenGL/CUDA interop.
    initGL( argc, argv );
    
    // use CUDA device with highest Gflops/s
	cudaGLSetGLDevice (cutGetMaxGflopsDeviceId() );
    
    int device;
    struct cudaDeviceProp prop;
    cudaGetDevice( &device );
    cudaGetDeviceProperties( &prop, device );
	// test if device is capable of OpenGL
    if( !strncmp( "Tesla", prop.name, 5 ) ){
        printf("This sample needs a card capable of OpenGL and display.\n");
        printf("Please choose a different device with the -device=x argument.\n");
        cudaThreadExit();
        cutilExit(argc, argv);
    }
    
	// set up OpenGL functions
    glutDisplayFunc(display);
    glutKeyboardFunc(keyboard);
    glutReshapeFunc(reshape);
    glutIdleFunc(idle);
    
	// load image
    if (argc > 1) {
        char *image_path;
        if (cutGetCmdLineArgumentstr(argc, (const char **)argv, "file", &image_path)) {
            loadImage(image_path);
        }
    } else {
        loadDefaultImage(argv[0]);
    }

	initBufferAndTexture();
    
    printf("I: display image\n");
    printf("S: display Sobel edge detection\n");
	printf("F: display Sobel edge detection for full image\n");
    printf("Use the '-' and '+' keys to change the brightness.\n");
	printf("Use the ',' and '.' keys to change the width of the filter.\n");
    fflush(stdout);
    atexit(cleanup);
    glutMainLoop();
    
    cudaThreadExit();
    cutilExit(argc, argv);
}
