/*
    win32 Ray Caster
    (C) 2006 Jason Hunt
    nulluser@gmail.com

    Free software - GPL
*/


#include <windows.h>
#include <stdio.h>
#include <math.h>
#include "resource.h"


#define SECOND_TIMER    1           // ID for frame timer
#define UPDATE_TIMER    2           // ID for frame timer
#define INPUT_TIMER     3           // ID for frame timer
#define TARGET_FPS      80          // FSP system is trying to hit
#define FRAME_TIME     (1000/TARGET_FPS)// Time between frame start in mS
#define INPUT_TIME      20


#define MAX_X   500
#define MAX_Y   500
#define DEG_TO_RAD(a) (((a) * M_PI / 180.0))    // Macro to convert degrees to radians

#define BLOCK_X_SIZE    128
#define BLOCK_Y_SIZE    128

#define MAX_TEXTURES    10
#define TEXTURE_SIZE    128

// Struct for each map location
typedef struct
{
    unsigned int t_index;   // Index into textures
} map_block_type;


// Map structure 
typedef struct 
{
    map_block_type *data;           // Pointer to map data
    
    unsigned long x_size;           // Size in blocks
    unsigned long y_size;
    double max_x;                   // Site in world coords
    double max_y;
    
    COLORREF *textures[MAX_TEXTURES];   // Texture data
} map_type;


// Player data
typedef struct 
{
    double x, y, dir;
    double move_speed, turn_speed;
    
} player_type;


map_type map;           // Map data
player_type player;     // Player data


// Set inital fiend of view
double field_of_view = DEG_TO_RAD(60);


int display_x = 640;             // Initial screen size
int display_y = 600;

bool clear_screen = true;        // True of full redraw is needed

HDC memdc = NULL;	           	 // Memory device context
HBITMAP h_mem_bitmap = NULL;     // Offscreen bitmap


unsigned int fps = 0;           // Frames per second
unsigned int updates = 0;       // Updates since last second

bool display_data = true;       // True to display debug data

bool keys[256] = {false};       // Keyboard table


COLORREF *frame_buffer;         // Offscreen draw buffer
BITMAPINFO buffer_info;         // Bitmap into for buffer



/* Return a random number in the specified range */
double f_rand(double s, double e)
{
    return((e - s)*(rand() / (double)RAND_MAX) + s);
}
/* End of f_rand */


/* Set a texture indec value in the map */
void set_map_t_index(unsigned int x, unsigned int y, unsigned int v)
{
    map.data[x + y * map.x_size].t_index = v;
}
/* End of set_map_t_index */


/* Reset game to inital */
void reset_game( void )
{
    map.x_size = 50;
    map.y_size = 50;

    map.data = (map_block_type *) malloc(map.x_size * map.y_size * sizeof(map_block_type));
    
    if (map.data == NULL) return;

    memset(map.data, 0, map.x_size * map.y_size * sizeof(unsigned long));


    map.max_x = map.x_size * BLOCK_X_SIZE;
    map.max_y = map.y_size * BLOCK_Y_SIZE;
        
    
    for (int i = 0; i < map.x_size; i++)
    {
        set_map_t_index(i, 0, 1);
        set_map_t_index(i, map.y_size-1, 1);
    }

    for (int i = 0; i < map.y_size; i++)
    {
        set_map_t_index(0, i, 1);
        set_map_t_index(map.x_size-1, i, 1);
    }

    
    
    for (int i = 0; i < map.x_size * map.y_size / 10; i++)
      set_map_t_index(rand() % map.x_size, rand() % map.y_size, 1+ rand() % 2);


    player.x = 2 * BLOCK_X_SIZE;
    player.y = 2 * BLOCK_X_SIZE;
    
    player.dir = DEG_TO_RAD(45.0);
    
    player.turn_speed = 0.02;
    player.move_speed = 16;
       
}




/* Get data from real map cords */
unsigned long get_map( double x, double y)
{
    int map_x = (int)(x / BLOCK_X_SIZE);
    int map_y = (int)(y / BLOCK_Y_SIZE);
    
    if (map_x < 0 || map_y < 0 || map_x >= map.x_size || map_y >= map.y_size)
        return(0);

    return(map.data[map_x + map_y * map.x_size].t_index);
}
/* End of get_map */


/* Draws a texture mapped line slice */
void  render_line(HDC memdc, unsigned int x, unsigned int size, int offset, unsigned int t)
{
    double texture_y = 0;
    double texture_dy = 64.0  / (double)size;   // Calculate texture delta

    // If the slice is clipped by the size, 
    // advance into the texture to compensate
    if (size > display_y / 2) 
    {
        texture_y += (size -  display_y / 2) * texture_dy;
        size = display_y / 2;   // clip the size to the display        
    }
                             
    // Setup starting pointer of slice
    COLORREF *p = frame_buffer + (display_y / 2 - size) * display_x + x;
    
    // Dump the line to the frame buffer
    for (int i = 0; i < size * 2; i++)
    {
        unsigned int cur_y = (int)(texture_y);

        // Clamp y_index in case of round off error
        if (cur_y > TEXTURE_SIZE - 1) cur_y = 0;
    
        // Indec into the testure
        *p = map.textures[t][offset + (cur_y << 7)];

        p += display_x;  // Advance index into frame buffer
        
        texture_y += texture_dy; // Advance index into texture
    }
}
/* End of render line */




/* Draw game display */
void render_frame( HWND hwnd, map_type map, player_type player )
{
    // Calculate the starting ray angle
    double ray_angle = player.dir + field_of_view / 2.0;
   
    // Clear the frame buffer
    memset(frame_buffer, 255, display_x * display_y * sizeof(COLORREF));
    
    // Render a slice for every x
    for (unsigned int i = 0; i < display_x; i++)
    {                
        bool vert_in_range = true,      // True while the ray is in the map
             horz_in_range = true;

        unsigned long vert_map = 0, horz_map = 0;   // Map value at current ray

        double cur_vert_x = 0, cur_vert_y = 0;      // Ray position for vertical check
        double vert_x_add = 0, vert_y_add = 0;      // Ray delta for check

        double cur_horz_y = 0, cur_horz_x = 0;      // Ray position for vertical check
        double horz_x_add = 0, horz_y_add = 0;      // Ray delta for check

        double tan_angle = tan(ray_angle);          // Tangent for the angle

        // Setup inital positions 
        cur_vert_x = (((int)player.x / BLOCK_X_SIZE) +1) * BLOCK_X_SIZE; 
        cur_horz_y = (((int)player.y / BLOCK_Y_SIZE) +1) * BLOCK_Y_SIZE;  
        
        // Facing right special case
        if (ray_angle == DEG_TO_RAD(0))
        {
            vert_x_add = BLOCK_X_SIZE;
            vert_y_add = 0;
            horz_in_range = false;      // Ignore horz
            
        } else
        // Facing up special case
        if (ray_angle == DEG_TO_RAD(90))
        {
            horz_x_add = 0;
            horz_y_add = BLOCK_Y_SIZE;

            vert_in_range = false;      // Ignore vert
        } else
        // Facing left special case
        if (ray_angle == DEG_TO_RAD(180))
        {
            cur_vert_x -= BLOCK_X_SIZE + 1;

            vert_x_add = -BLOCK_X_SIZE;
            vert_y_add = 0;
            horz_in_range = false;      // Ignore horz
        } else
        // Facing down special case
        if (ray_angle == DEG_TO_RAD(270))
        {
            cur_horz_y  -= BLOCK_X_SIZE + 1;
            horz_x_add = 0;
            horz_y_add = -BLOCK_Y_SIZE;

            vert_in_range = false;      // Ignore vert
        } else
        // Normal case
        {
            // Setup for facing right
            vert_x_add = BLOCK_X_SIZE;
            vert_y_add = BLOCK_X_SIZE * tan_angle;

            // Modify if facing left
            if (cos(ray_angle) < 0)
            {
                cur_vert_x -= BLOCK_X_SIZE + 1;
                vert_x_add *= -1;
                vert_y_add *= -1;
            }
            
            // Setup for facing Up
            horz_x_add = BLOCK_X_SIZE / tan_angle;
            horz_y_add = BLOCK_Y_SIZE;
            
            // Modify for facing down
            if (sin(ray_angle) < 0)
            {
                cur_horz_y -= BLOCK_X_SIZE + 1;
                horz_x_add *= -1;
                horz_y_add *= -1;
            }

            // Calculate starting positions
            cur_vert_y = player.y + (cur_vert_x - player.x) * tan_angle;            
            cur_horz_x = player.x + (cur_horz_y - player.y) / tan_angle;
        }

        // Check for verticle intersections
        if (!(vert_map = get_map(cur_vert_x, cur_vert_y)))
        {
            while(!vert_map && vert_in_range)            
            {
                cur_vert_x += vert_x_add;
                cur_vert_y += vert_y_add;

                // Range check
                if  (cur_vert_x > map.max_x || cur_vert_y > map.max_y || cur_vert_x < 0 || cur_vert_y < 0)
                    vert_in_range = false;
                else
                    vert_map = get_map(cur_vert_x, cur_vert_y);
            }
        }

        // Determine distance
        double vert_dist = (player.x - cur_vert_x) * (player.x - cur_vert_x) + 
                           (player.y - cur_vert_y) * (player.y - cur_vert_y);

        // Check for horizontal intersections
        if (!(horz_map = get_map(cur_horz_x, cur_horz_y)))
        {
            while(!horz_map && horz_in_range)            
            {
                cur_horz_x += horz_x_add;
                cur_horz_y += horz_y_add;

                // Range check
                if  (cur_horz_x > map.max_x || cur_horz_y > map.max_y || cur_horz_x < 0 || cur_horz_y < 0)
                    horz_in_range = false;
                else
                    horz_map = get_map(cur_horz_x, cur_horz_y);
            }
        }

        // Determine distance
        double horz_dist = ((player.x - cur_horz_x) * (player.x - cur_horz_x) + 
                            (player.y - cur_horz_y) * (player.y - cur_horz_y));

        // Set max distance
        double dist =  2 * (map.max_x + map.max_y);
        
        int color;         
        
        int offset = 0;
        
        unsigned int texture = 0;
        
        // Find closest wall
        if (horz_dist < vert_dist)
        {
            color = horz_map;        
            dist = sqrt(horz_dist); 
            offset = (int)cur_horz_x % 128;
            texture = horz_map;
        }
        else
        {
            color = vert_map;        
            dist = sqrt(vert_dist); 
            offset = (int)cur_vert_y % 128;
            texture = vert_map;
        }

        dist *= cos(player.dir - ray_angle);    // Correct for fisheye effect

        if (dist > 0)
        {
            // Calculate slice height
            int size = (unsigned int )(((BLOCK_X_SIZE + BLOCK_Y_SIZE) / (4*dist)) * display_y);

            // Convert texture number to an index from [0..]
            render_line(memdc, i, size, offset, texture - 1); 
        }
        
        ray_angle -= field_of_view / (double)display_x;
    }      
                      
    // Copy frame buffer to memory device context
    SetDIBitsToDevice(memdc, 0, 0, display_x, display_y, 0, 0, 0, display_y, 
                      frame_buffer, &buffer_info, DIB_RGB_COLORS);   
           
    // Debugging data
    if (display_data)
    {
        char buff[300];
        sprintf(buff, "FPS: %d  ", fps);
        TextOut(memdc, 1, 1, buff, strlen(buff));
    }
}
/* End of draw_game */



/* Load map images into memory */
void load_images(HINSTANCE h_inst, HWND hwnd)
{
    //HBITMAP b  = LoadBitmap(h_inst, "TEXTURE_1");
    
    HDC hdc = GetDC(hwnd);

    BITMAPINFO binfo;

    memset(&binfo, 0, sizeof(BITMAPINFO));
    
    binfo.bmiHeader.biSize = sizeof(BITMAPINFO);
    binfo.bmiHeader.biWidth = 128;
    binfo.bmiHeader.biHeight = 128;
    binfo.bmiHeader.biPlanes = 1;
    binfo.bmiHeader.biBitCount = 32;
    binfo.bmiHeader.biCompression = BI_RGB;
        
    // Get texture memory
    map.textures[0] = (COLORREF *)malloc(128 * 128 * sizeof(COLORREF));
    
    if (map.textures[0] == NULL) return;

    HBITMAP b  = LoadBitmap(h_inst, "TEXTURE_1");
    
    // Extract data
    GetDIBits(hdc, b, 0, 128, map.textures[0], &binfo, DIB_RGB_COLORS);    
    
    // Get texture memory
    map.textures[1] = (COLORREF *)malloc(128 * 128 * sizeof(COLORREF));
    
    if (map.textures[1] == NULL) return;

    b  = LoadBitmap(h_inst, "TEXTURE_2");

    // Extract data    
    GetDIBits(hdc, b, 0, 128, map.textures[1], &binfo, DIB_RGB_COLORS);    

    ReleaseDC(hwnd, hdc);    
}
/* End of load_images */


/* Free all world data */
void free_data( void )
{
    if (map.data) free(map.data);
    
    for (int i = 0; i < MAX_TEXTURES; i++)
        if (map.textures[i])
            free(map.textures[i]);    

    if (frame_buffer)
        free(frame_buffer);
}
/* End of free_data */


/* Create off screen buffer */
void setup_display( HWND hwnd )
{
    // Free old frame buffer
    if (frame_buffer) free(frame_buffer);    

    // Clear buffer info so we only have to set needed values
    memset(&buffer_info, 0, sizeof(BITMAPINFO));
    
    // Setup the offscreen buffer into
    buffer_info.bmiHeader.biSize = sizeof(BITMAPINFO);
    buffer_info.bmiHeader.biWidth = display_x;
    buffer_info.bmiHeader.biHeight = display_y;
    buffer_info.bmiHeader.biPlanes = 1;
    buffer_info.bmiHeader.biBitCount = 32;
    buffer_info.bmiHeader.biCompression = BI_RGB;
        
    // Get memory for the frame buffer
    frame_buffer = (COLORREF *)malloc(display_x * display_y * sizeof(COLORREF));
    
    if (frame_buffer == NULL) return;
    
    // Clear to black
    memset(frame_buffer, 0, display_x * display_y * sizeof(COLORREF));
    
    // Get main DC
    HDC hdc = GetDC(hwnd);

  	// Create Memory device contextand bitmap
    memdc = CreateCompatibleDC(hdc);	

    // Create off screen buffer
  	h_mem_bitmap = CreateCompatibleBitmap(hdc, display_x, display_y);

  	// Select the off screen buffer into the DC
   	SelectObject(memdc, h_mem_bitmap);		

    ReleaseDC(hwnd, hdc);       // Main DC is no longer needed
}
/* End of setup_display */



/* Resize the display and update buffers */
void resize_display(HWND hwnd, LPARAM l)
{
    display_x = LOWORD(l);
    display_y = HIWORD(l);

    DeleteObject(h_mem_bitmap);		//	 Delete old screen buffer
    DeleteDC(memdc);		      	// Delete old memory DC

    setup_display(hwnd);

    InvalidateRect(hwnd, NULL, true);   
}
/* End of resize_display */



/* Check current keypresses and update */
void check_keys(void)
{
    // Forward
    if (keys['W'])
    {
        player.x += player.move_speed * cos(player.dir);
        player.y += player.move_speed * sin(player.dir);
    }

    // Back
    if (keys['S'])
    {
        player.x -= player.move_speed * cos(player.dir);
        player.y -= player.move_speed * sin(player.dir);
    }

    // Left
    if (keys['A'])
    {
        player.dir += player.turn_speed;
        
        if (player.dir > 2*M_PI)
            player.dir -=  2*M_PI;
    }
    
    // Right
    if (keys['D'])
    {
        player.dir -= player.turn_speed;      
        
        if (player.dir < 0)
            player.dir +=  2*M_PI;
    }
}
/* End of check keys */


/* Process key down message */
void key_down(WPARAM w)
{
    keys[w & 0xff] = true;          // Set key
}
/* End of key_down */


/* Process keyup message */
void key_up(WPARAM w)
{
    keys[w & 0xff] = false;         // Free key
}
/* End of keyup */


/* The window Function */
LRESULT CALLBACK WinProc(HWND hwnd, UINT msg, WPARAM w, LPARAM l)
{
    HDC hdc;

    switch(msg)
    {
        case WM_CREATE: 
            setup_display(hwnd);

            reset_game(); 
          
            SetTimer(hwnd, UPDATE_TIMER, FRAME_TIME, NULL);
            SetTimer(hwnd, SECOND_TIMER, 1000, NULL);
            SetTimer(hwnd, INPUT_TIMER, INPUT_TIME, NULL);
            
            return(0);

        case WM_CHAR:
            if (w == 27) DestroyWindow(hwnd);   // Escape exits
            return(0);

        case WM_PAINT:

            // Render memory device context to display
            PAINTSTRUCT ps;
            hdc = BeginPaint(hwnd, &ps);
            BitBlt(hdc, 0, 0, display_x, display_y, memdc, 0, 0, SRCCOPY);
            EndPaint(hwnd, &ps);  

           return(0);

        case WM_TIMER:        
            if (w == UPDATE_TIMER)                  // Frame update
            {
                render_frame(hwnd, map, player);
                InvalidateRect(hwnd, NULL, 0);
                updates++;
            }else
            if (w == SECOND_TIMER)                  // Fps calculation
            {               
                fps = updates;
                updates = 0;
            } else
            if (w == INPUT_TIMER)                   // Input checking
            {               
                check_keys();
            }

            return(0);  
            
        case WM_DESTROY:    
            DeleteObject(h_mem_bitmap);		// Delete off screen buffer
            DeleteDC(memdc);	       		// Delete memory DC
    
            PostQuitMessage(0);
            return(0);
            
        case WM_MOUSEMOVE:
            return(0);
            
        case WM_SIZE:
            resize_display(hwnd, l);        // Resize window
            return(0);
            
        case WM_KEYDOWN:
            key_down(w);                    // Set key
            break;

        case WM_KEYUP:
            key_up(w);                      // Free key
            break;

        default:
            return(DefWindowProc(hwnd, msg, w, l));
    }

    return(0);  
}
/* End of WinProc */


/* WinMain */
int WINAPI WinMain(HINSTANCE h_this_inst, HINSTANCE h_prev_inst, LPSTR args, int mode)
{
    WNDCLASS wnd_class;

    char program_name[] = "Ray 2006 Jason Hunt";
        
    wnd_class.hInstance = h_this_inst;
    wnd_class.lpszClassName = program_name;
    wnd_class.lpfnWndProc = WinProc;
    wnd_class.style =0;
    
    wnd_class.hIcon = LoadIcon(NULL, IDI_APPLICATION);
    wnd_class.hCursor = LoadCursor(NULL, IDC_ARROW);
    wnd_class.lpszMenuName = NULL;
    
    wnd_class.cbClsExtra = 0;
    wnd_class.cbWndExtra = 0;
    
    wnd_class.hbrBackground = NULL; // Not needed
    
    if (!RegisterClass(&wnd_class)) return(0);
 
    // Create the window
    HWND hwnd = CreateWindow(program_name, "Ray", 
                             WS_OVERLAPPEDWINDOW,         // Normal Window
                             300, 100,                    // Pos
                             display_x, display_y,        // Size
                             HWND_DESKTOP, NULL, h_this_inst, NULL);

    ShowWindow(hwnd, mode);
    UpdateWindow(hwnd);
           
    MSG msg;
    
    // Load bitmap textures
    load_images(h_this_inst, hwnd);
    
    // Windows message loop
    while(GetMessage(&msg, NULL, 0, 0))
    { 
        TranslateMessage(&msg);
        DispatchMessage(&msg);                            
    }
                        
    free_data();                        
                        
                        
    return(msg.wParam);
}
/* End of winmain */



