#include "pch.c"
#pragma hdrstop

#include <time.h>
#include "mazedlg.h"
#include <assert.h>

MazeView vw;
MazeSolution sol;

MazeOptions maze_options =
{
    FALSE,          // depth_test
    RENDER_FLAT,    // render mode (WALLS)
    RENDER_FLAT,    // render mode (FLOOR)
    RENDER_FLAT,    // render mode (CEILING)
    FALSE,           // frame_count
    FALSE,          // top_view
    TRUE,           // eye_view
    FALSE,          // single_step
    FALSE,          // all_alpha
    FALSE,          // bDither
    1               // nrats
};

float gfAspect = 1.0f;  // aspect ratio

#define SST_NEW_MAZE    0
#define SST_MAZE_GROW   1
#define SST_SOLVING     2
#define SST_MAZE_SHRINK 3
#define SST_ROTATE      4

int solve_state = SST_NEW_MAZE;

// Table of textures used.  The first entries contain 1 texture for each main
// surface, followed by textures used for various objects
TEXTURE all_textures[NUM_TEXTURES];

// Environment associated with the texture (repetition, palette rotation,..) 
// For now, have one for each of the textures in all_textures.  But, this 
// could be a per-object thing.
TEX_ENV gTexEnv[NUM_TEXTURES];

// texture environment mode
int gTexEnvMode;

// Texture resources for the main surfaces
TEX_RES gTexResSurf[NUM_DEF_SURFACE_TEXTURES] = {
    {TEX_BMP, IDB_BRICK},       // default textures
    {TEX_BMP, IDB_WOOD},
    {TEX_BMP, IDB_CASTLE},
    {TEX_A8,  IDB_CURL4},       // mandelbrot textures
    {TEX_A8,  IDB_BHOLE4},
    {TEX_A8,  IDB_SNOWFLAK},
    {TEX_A8,  IDB_SWIRLX4}
};
    
// Texture resources for objects
TEX_RES gTexResObject[NUM_OBJECT_TEXTURES] = {
    {TEX_A8,  IDB_START},
    {TEX_A8,  IDB_END},
    {TEX_A8,  IDB_RAT},
    {TEX_A8,  IDB_AD},
    {TEX_BMP, IDB_COVER}
};
SSContext gssc;

void maze_Init( void *data );
void RotateTexturePalettes( TEX_ENV *pTexEnv, int count, int iRot );

enum {
    TIMER_START = 0,
    TIMER_STOP
};

void Draw(void)
{
    GLbitfield mask;

    mask = 0;
    if (maze_options.depth_test)
    {
        mask |= GL_DEPTH_BUFFER_BIT;
    }
    if (maze_options.render[FLOOR] == RENDER_NONE ||
        maze_options.render[CEILING] == RENDER_NONE ||
        maze_options.render[WALLS] == RENDER_NONE ||
        maze_height < 1.0f)
    {
        mask |= GL_COLOR_BUFFER_BIT;
    }
    if (mask != 0)
    {
        glClear(mask);
    }

    // Rotate the palettes of any paletted texures by 1
    RotateTexturePalettes( gTexEnv, NUM_TEXTURES, 1 );

    if (maze_options.eye_view)
    {
        DrawMaze(&vw);
    }

    if (maze_options.top_view)
    {
        DrawTopView(&vw);
    }
    
    glFlush();
}

MazeGoal maze_goals[MAX_GOALS];
int ngoals;

Object special_obj[MAX_SPECIALS];

#define MAX_LETTERS 3
Object letters_obj[MAX_LETTERS];

typedef struct _Rat
{
    Object obj;
    MazeView vw;
    MazeSolution sol;
} Rat;
Rat rats[MAX_RATS];

void NewMazeList(void)
{
    glNewList(maze_walls_list, GL_COMPILE);
    DrawMazeWalls();
    glEndList();
}

void UpdateRatPosition(Rat *rat)
{
    MoveObject(&rat->obj, rat->vw.pos.x, rat->vw.pos.y);
    // Invert the angle because view angles move opposite to
    // object angles
    rat->obj.ang = FaAdd(0, -rat->vw.ang);
}

// Simple routines to pick cells at random while guaranteeing
// that those cells are unoccupied

static int rnd_cells[MAZE_CELLS];
static int nrnd_cells;

void InitRandomCells(void)
{
    int i;

    nrnd_cells = MAZE_CELLS;
    for (i = 0; i < nrnd_cells; i++)
    {
        rnd_cells[i] = i;
    }
}

void PickRandomCell(IntPt2 *pos)
{
    int idx, t;

#if DBG
    if (nrnd_cells == 0)
    {
        MessageBox(GetDesktopWindow(), TEXT("Out of random cells"),
                   NULL, MB_OK);
        pos->x = 0;
        pos->y = 0;
        return;
    }
#endif
    
    idx = rand() % nrnd_cells;
    nrnd_cells--;
    t = rnd_cells[idx];
    rnd_cells[idx] = rnd_cells[nrnd_cells];
    pos->x = t % MAZE_GRID;
    pos->y = t / MAZE_GRID;
}

void RemoveRandomCell(IntPt2 *pos)
{
    int i, idx;

    for (i = 0; i < nrnd_cells; i++)
    {
        idx = rnd_cells[i];
        if ((idx % MAZE_GRID) == pos->x &&
            (idx / MAZE_GRID) == pos->y)
        {
            nrnd_cells--;
            rnd_cells[i] = rnd_cells[nrnd_cells];
            return;
        }
    }
}

void NewMaze(void)
{
    IntPt2 cell, start_cell;
    int i, nspecials, nads;
    static BOOL firstMaze = TRUE;

    // If not in full screen mode, move the maze window around after it's solved
    if( !gbTurboMode && !firstMaze )
        ss_RandomWindowPos();

    InitRandomCells();
    
    if (!InitMaze(&start_cell, maze_goals, &ngoals))
    {
        printf("InitMaze failed\n");
        exit(1);
    }

    RemoveRandomCell(&start_cell);
    cell.x = maze_goals[GOAL_END].clx;
    cell.y = maze_goals[GOAL_END].cly;
    RemoveRandomCell(&cell);

    nspecials = rand() % MAX_SPECIALS;
    for (i = 0; i < nspecials; i++)
    {
        PickRandomCell(&cell);
        maze_goals[ngoals].clx = cell.x;
        maze_goals[ngoals].cly = cell.y;
        maze_goals[ngoals].user = &special_obj[i];
        
        special_obj[i].w = FMAZE_CELL_SIZE/4;
        special_obj[i].h = FxFltVal(.25);
        special_obj[i].z = special_obj[i].h;
        special_obj[i].col = 15;
        special_obj[i].draw_style = DRAW_SPECIAL;
        special_obj[i].draw_arg = rand() % SPECIAL_ARG_COUNT;
        special_obj[i].ang = FaDeg(0);
        special_obj[i].user1 = (rand() % 6)+2;
        special_obj[i].user2 = rand() % 5;
        special_obj[i].user3 = 0;
        PlaceObject(&special_obj[i],
                    CellToMfx(maze_goals[ngoals].clx)+FMAZE_CELL_SIZE/2,
                    CellToMfx(maze_goals[ngoals].cly)+FMAZE_CELL_SIZE/2);
        ngoals++;
    }
    while (i < MAX_SPECIALS)
    {
        special_obj[i].cell = NULL;
        i++;
    }

    nads = (rand() % (MAX_LETTERS*2))-MAX_LETTERS+1;
    for (i = 0; i < nads; i++)
    {
        PickRandomCell(&cell);
        
        letters_obj[i].w = FMAZE_CELL_SIZE/3;
        letters_obj[i].h = FxFltVal(.33);
        letters_obj[i].z = FxFltVal(.5);
        letters_obj[i].col = 15;
        letters_obj[i].draw_style = DRAW_POLYGON;
        letters_obj[i].pTexEnv = &gTexEnv[ TEX_AD ];
        letters_obj[i].ang = FaDeg(0);
        PlaceObject(&letters_obj[i],
                    CellToMfx(cell.x)+FMAZE_CELL_SIZE/2,
                    CellToMfx(cell.y)+FMAZE_CELL_SIZE/2);
    }
    while (i < MAX_LETTERS)
    {
        letters_obj[i].cell = NULL;
        i++;
    }

    for (i = 0; i < maze_options.nrats; i++)
    {
        PickRandomCell(&cell);
        
        rats[i].obj.w = FMAZE_CELL_SIZE/4;
        rats[i].obj.h = FxFltVal(.125);
        rats[i].obj.z = rats[i].obj.h;
        rats[i].obj.col = 16;
        rats[i].obj.draw_style = DRAW_POLYGON;
        rats[i].obj.pTexEnv = &gTexEnv[ TEX_RAT ];
        SolveMazeStart(&rats[i].vw, &maze_cells[0][0], MAZE_GRID, MAZE_GRID,
                       &cell, SOL_DIR_LEFT,
                       NULL, 0,
                       (rand() % 1000) > 500 ? SOL_TURN_LEFT : SOL_TURN_RIGHT,
                       &rats[i].sol);
        // Need to force this to NULL when a new maze is generated so
        // that moving doesn't try to use old maze data
        rats[i].obj.cell = NULL;
        UpdateRatPosition(&rats[i]);
    }
    
    NewMazeList();
    
    SolveMazeStart(&vw, &maze_cells[0][0], MAZE_GRID, MAZE_GRID,
                   &start_cell, SOL_DIR_RIGHT,
                   maze_goals, ngoals,
                   (rand() % 1000) > 500 ? SOL_TURN_LEFT : SOL_TURN_RIGHT,
                   &sol);

    solve_state = SST_MAZE_GROW;
    firstMaze = FALSE;
}

/**************************************************************************\
* Step
*
* Main draw proc
*
\**************************************************************************/

static MazeGoal *found_goal = NULL;
static int rot_step;

void Step(void *data)
{
    int i;
    MazeGoal *goal;

    switch(solve_state)
    {
    case SST_NEW_MAZE:
        view_rot = 0;
        maze_height = 0.0f;
        NewMaze();
        break;

    case SST_SOLVING:
        for (i = 0; i < maze_options.nrats; i++)
        {
            SolveMazeStep(&rats[i].vw, &rats[i].sol);
            UpdateRatPosition(&rats[i]);
        }
        
        goal = SolveMazeStep(&vw, &sol);
        if (goal == &maze_goals[GOAL_END])
        {
            solve_state = SST_MAZE_SHRINK;
        }
        else if (goal != NULL)
        {
            solve_state = SST_ROTATE;
            found_goal = goal;
            rot_step = 0;
        }
        break;

    case SST_MAZE_GROW:
        maze_height += .025f;
        if (maze_height >= 1.0f)
        {
            solve_state = SST_SOLVING;
        }
        break;
        
    case SST_MAZE_SHRINK:
        maze_height -= .025f;
        if (maze_height <= 0.0f)
        {
            solve_state = SST_NEW_MAZE;
        }
        break;

    case SST_ROTATE:
        view_rot += 10.0;
        if (++rot_step == 18)
        {
            Object *sp_obj;

            sp_obj = (Object *)found_goal->user;
            RemoveObject(sp_obj);
            
            solve_state = SST_SOLVING;
            ngoals--;
            if (found_goal < maze_goals+ngoals)
            {
                memmove(found_goal, found_goal+1,
                        sizeof(MazeGoal)*(ngoals-(found_goal-maze_goals)));
            }
            SolveMazeSetGoals(&sol, maze_goals, ngoals);
            found_goal = NULL;

            if (view_rot >= 360.0)
            {
                view_rot = 0.0;
            }
            else
            {
                view_rot = 180.0;
            }
        }
        break;
    }

    Draw();

    for (i = 0; i < MAX_SPECIALS; i++)
    {
        // Increment rotations of any specials still present in the maze
        if (special_obj[i].cell != NULL)
        {
            special_obj[i].ang = FaAdd(special_obj[i].ang,
                                       FaDeg(special_obj[i].user1));
            special_obj[i].user3 += special_obj[i].user2;
        }
    }
}

void UpdateModes(void)
{
    if (maze_options.depth_test)
    {
        glEnable(GL_DEPTH_TEST);
    }
    else
    {
        glDisable(GL_DEPTH_TEST);
    }
}

/**************************************************************************\
* Reshape
*
*       - called on resize, expose                                      
*       - always called on app startup                                  
*
\**************************************************************************/

void 
Reshape(int width, int height, void *data )
{
    gfAspect = height == 0 ? 1.0f: (float) width / (float) height;
}


/******************************Public*Routine******************************\
* VerifyTextureFiles
*
* Check that any user specified textures are valid
* - MUST be called at ss_Init, or any error msgs will not display properly
*
\**************************************************************************/

void VerifyTextureFiles(void)
{
    int i;

    ss_DisableTextureErrorMsgs();

    for (i = 0; i < NUM_SURFACES; i++) {
        if( gTexInfo[i].bTex && !gTexInfo[i].bDefTex ) {
            if( !ss_VerifyTextureFile( &gTexInfo[i].texFile ) )
                // use default texture
                gTexInfo[i].bDefTex = TRUE;
        }
    }
}

/******************************Public*Routine******************************\
* CalcRep
*
* Figure out repetion based on size.
*
* - Use 128 pixels as a unit repetition reference
* - Size is always a power of 2
*
\**************************************************************************/

static int
CalcRep( int size )
{
    double pow2;
    int pow2ref = 8;
    int rep;

    pow2 = log((double)size) / log((double)2.0);
    rep = 1 + pow2ref - (int)pow2;
    return rep; 
}

// tradeoff
#define MAX_TEX_DIM 128
//#define MAX_TEX_DIM 256

/******************************Public*Routine******************************\
* CalcTexRep
*
* Figure out texture repetition based on texture size.
*
* - mf: ? double for walls/ceiling ?
*
\**************************************************************************/

static void
CalcTexRep( TEXTURE *pTex, IPOINT2D *pTexRep )
{
    if( pTex->width >= MAX_TEX_DIM )
        pTexRep->x = 1;
    else
        pTexRep->x = CalcRep( pTex->width );

    if( pTex->height >= MAX_TEX_DIM )
        pTexRep->y = 1;
    else
        pTexRep->y = CalcRep( pTex->height );
}

/******************************Public*Routine******************************\
* LoadTextures
*
* Load textures from .bmp files or from embedded resources, using
* global texture info structure.
*
* For now, this also sets render modes, since if texturing is off for a
* surface, we always use RENDER_FLAT
*
\**************************************************************************/

static void 
LoadTextures(void)
{
    int i;
    TEXTURE *pTex = all_textures;
    TEX_ENV *pTexEnv = gTexEnv;
    TEX_RES *pTexRes;

    assert( (NUM_SURFACES + NUM_OBJECT_TEXTURES) == NUM_TEXTURES );

    // load surface textures (wall, floor, ceiling)

    for (i = 0; i < NUM_SURFACES; i++, pTex++, pTexEnv++) {
        maze_options.render[i] = RENDER_FLAT;
        if( gTexInfo[i].bTex ) {
            pTexEnv->pTex = pTex;
            pTexEnv->bTransp = FALSE;

            // Load user or default texture for the surface

            if( !gTexInfo[i].bDefTex && 
                ss_LoadTextureFile( &gTexInfo[i].texFile, pTex )) {
            } else
            {
                // Load default resource texture

                pTexRes = &gTexResSurf[ gTexInfo[i].iDefTex ];
                if( !ss_LoadTextureResource( pTexRes, pTex ) )
                    continue; 
    
                if( ss_PalettedTextureEnabled() &&
                    (pTexRes->type == TEX_A8) )
                {
                    pTexEnv->bPalRot = TRUE;
                    pTexEnv->iPalRot = ss_iRand( 0xff );
                } else
                    pTexEnv->bPalRot = FALSE;
            }

            maze_options.render[i] = RENDER_TEXTURED;

            // Figure out texture repetition
            CalcTexRep( pTex, &pTexEnv->texRep );
            // We would have to set texture parameters per object here,
            // but we just use default ones already set by ss_LoadTexture*
        }
    }

    // load object textures

    pTexRes = gTexResObject;
    for( i = 0; i < NUM_OBJECT_TEXTURES; i++, pTex++, pTexEnv++, pTexRes++ ) {
        if( ss_LoadTextureResource( pTexRes, pTex ) )
            pTexEnv->pTex = pTex;
        else
            pTexEnv->pTex = NULL;

        pTexEnv->bTransp = FALSE;

        // For now we don't do palrot's on any of the object textures
        pTexEnv->bPalRot = FALSE;
        // No repetition
        pTexEnv->texRep.x = pTexEnv->texRep.y = 1;
    }

    // Set transparency for some of the textures
    ss_SetTextureTransparency( gTexEnv[TEX_START].pTex, 0.42f, FALSE );
    ss_SetTextureTransparency( gTexEnv[TEX_END].pTex, 0.4f, FALSE );
    ss_SetTextureTransparency( gTexEnv[TEX_AD].pTex, 0.4f, FALSE );

    // Enable transparency for some of the texture environments
    gTexEnv[TEX_START].bTransp = TRUE;
    gTexEnv[TEX_END].bTransp = TRUE;
    gTexEnv[TEX_AD].bTransp = TRUE;
}

void UseTextureEnv( TEX_ENV *pTexEnv )
{
    static HTEXTURE hCurTex = (HTEXTURE) -1;
    HTEXTURE hTex = pTexEnv->pTex;

    // We cache the current texture for 'no texture object' case

    if( !ss_TextureObjectsEnabled() &&
        (hCurTex == hTex) )
        return; // same texture, no need to send it down

    // Make this texture current

    ss_SetTexture( hTex );

    // Set texture palette if necessary

    if( pTexEnv->bPalRot &&
        !ss_TextureObjectsEnabled() && 
        ss_PalettedTextureEnabled() ) {

        // We need to send down the (rotated) palette
        ss_SetTexturePalette( hTex, pTexEnv->iPalRot );
    }

    hCurTex = hTex;
}

/******************************Public*Routine******************************\
* RotateTexturePalettes
*
* If paletted texturing is enabled, go through the supplied list of texture
* environments, and if any have a palette, increment its rotation by the
* supplied iRot value.
*
\**************************************************************************/

void
RotateTexturePalettes( TEX_ENV *pTexEnv, int count, int iRot )
{
    if( !ss_PalettedTextureEnabled() || !pTexEnv )
        return;


    for( ; count; count--, pTexEnv++ ) {

        if( !pTexEnv->pTex || !pTexEnv->pTex->pal )
            continue;

        if( pTexEnv->bPalRot ) {
            // increment palette rotation
            pTexEnv->iPalRot += iRot;
            if( pTexEnv->iPalRot >= pTexEnv->pTex->pal_size )
                pTexEnv->iPalRot = 0;
            // Only send down the new palette if texture objects are enabled,
            // since otherwise it will be sent down when texture is 
            // 'made current'
            if( ss_TextureObjectsEnabled() ) {
                ss_SetTexturePalette( pTexEnv->pTex, pTexEnv->iPalRot );
            }
        }
    }
}

/******************************Public*Routine******************************\
* InitStretchInfo
*
\**************************************************************************/

static void
InitStretchInfo( STRETCH_INFO *pStretch )
{
    pStretch->baseWidth = 320;
    pStretch->baseHeight = 200;
    pStretch->bRatioMode = FALSE;
}

/******************************Public*Routine******************************\
* SetFloaterInfo
*
* Set the size and position of the floating child window
*
\**************************************************************************/


static void 
SetFloaterInfo( ISIZE *pParentSize, CHILD_INFO *pChild )
{
    float hdtvAspect = 9.0f / 16.0f;
    ISIZE *pChildSize = &pChild->size;

    // Set width according to user-specified size
    // (giSize range is 0..100)
    // set min size as 1/3 parent width
    pChildSize->width = 
            (int) ((0.333f + 2.0f*giSize/300.0f) * pParentSize->width);

    // Scale height for hdtv aspect ratio
    pChildSize->height = (int) (hdtvAspect * pChildSize->width + 0.5f);
    // Ensure height not too big
    SS_CLAMP_TO_RANGE2( pChildSize->height, 0, pParentSize->height );

    pChild->pos.x = (pParentSize->width - pChildSize->width) / 2;
    pChild->pos.y = (pParentSize->height - pChildSize->height) / 2;
}

/******************************Public*Routine******************************\
* ss_Init
*
* Initialize - called on first entry into ss.
* Called BEFORE gl is initialized!
* Just do basic stuff here, like set up callbacks, verify dialog stuff, etc.
*
* Fills global SSContext structure with required data, and returns ptr
* to it.
*
\**************************************************************************/

SSContext *
ss_Init( void )
{
    getIniSettings();

    VerifyTextureFiles();

    // Set callbacks

    ss_InitFunc( maze_Init );
    ss_UpdateFunc( Step );
    ss_ReshapeFunc( Reshape );

    gssc.depthType = (maze_options.depth_test) ? SS_DEPTH16 : SS_DEPTH_NONE;

    // Currently stretch and floater mutex
    gssc.bDoubleBuf = TRUE; // will get turned off if stretch used
    if( gbTurboMode ) {
        // We stretch out the drawing area to the full size of the main window
        gssc.bFloater = FALSE;
        gssc.bStretch = TRUE;
        InitStretchInfo( &gssc.stretchInfo );
    } else {
        // A static centered floating window is created in the main window
        FLOATER_INFO *pFloater = &gssc.floaterInfo;

        gssc.bFloater = TRUE;
        gssc.bStretch = FALSE;
        pFloater->bMotion = FALSE;
        pFloater->ChildSizeFunc = SetFloaterInfo;
    }

    return &gssc;
}

/******************************Public*Routine******************************\
* maze_Init
*
* Initializes OpenGL state
*
\**************************************************************************/
void
maze_Init( void *data )
{
    float fv4[4];

    if (!FxInitialize(FA_TABLE_SIZE, 0))
    {
        printf("FxInit failed\n");
        exit(1);
    }

    glShadeModel( GL_FLAT );
    glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);

    glClearDepth(1);
    
    glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
    glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_DECAL);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
    glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);

    if( (giImageQual == IMAGEQUAL_DEFAULT) || gbTurboMode ) {
        maze_options.bDither = FALSE;
        glDisable( GL_DITHER );
    } else {
        maze_options.bDither = TRUE;
        glEnable( GL_DITHER );
    }

    // Load textures and set render modes
    LoadTextures();
    
    fv4[0] = MAZE_SIZE/2.0f;
    fv4[1] = MAZE_SIZE/2.0f;
    fv4[2] = 10.0f;
    fv4[3] = 1.0f;
    glLightfv(GL_LIGHT0, GL_POSITION, fv4);
    glEnable(GL_LIGHT0);

    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
    
    // Check which texture environment function to use for objects
    if( ss_fOnGL11() )
        gTexEnvMode = GL_REPLACE;
    else
        gTexEnvMode = GL_MODULATE; 

    maze_walls_list = glGenLists(1);
    
    UpdateModes();
}
