/* Pathogen Warrior
 * Copyright 2004 Jetro Lauha - http://iki.fi/jetro/
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 *
 * $Id: BuildState.cpp,v 1.14 2004/07/14 15:37:09 tonic Exp $
 * $Revision: 1.14 $
 */

#include <assert.h>
#include <math.h>
#include "main.h"
#include "SDL_image.h"


enum QUADRIC_OBJ_ENUM
{
    SPHERE_SLICES = 20,
    SPHERE_STACKS = 10,
    CYLINDER_SLICES = 5,
    CYLINDER_STACKS = 1
};

static GLUquadric *sQObj = NULL;
static GLuint sSphereList = 0;
static GLuint sCylinderLists = 0;


static ProblemConfiguration sProblemConfigurations[] =
{
    //{ 3, { 4, 3 }, 0.04f },
    { 2, { 1, 1 }, 0 },
    { 2, { 2, 1 }, 0 },
    { 3, { 1, 1 }, 0.01f },
    { 2, { 3, 1 }, 0.01f },
    { 2, { 4, 1 }, 0 },
    { 3, { 2, 1 }, 0 },
    { 3, { 3, 1 }, 0 },
    { 3, { 3, 1 }, 0.01f },
    { 2, { 5, 1 }, 0.01f },
    { 3, { 2, 2 }, 0 },
    { 3, { 2, 2 }, 0.01f },
    { 2, { 6, 1 }, 0.02f },
    { 3, { 3, 2 }, 0 },
    { 3, { 2, 3 }, 0 },
    { 3, { 2, 3 }, 0.01f },
    { 3, { 1, 4 }, 0 },
    { 3, { 3, 3 }, 0 },
    { 3, { 4, 1 }, 0 },
    { 3, { 4, 2 }, 0 },
    { 3, { 3, 3 }, 0.01f },
    { 3, { 4, 1 }, 0.01f },
    { 3, { 4, 2 }, 0.01f },
    { 3, { 4, 3 }, 0.01f },
    { 3, { 3, 3 }, 0.02f },
    { 3, { 4, 1 }, 0.02f },
    { 3, { 4, 2 }, 0.02f },
    { 3, { 4, 3 }, 0.02f },
    { 3, { 3, 3 }, 0.04f },
    { 3, { 4, 1 }, 0.04f },
    { 3, { 4, 2 }, 0.04f },
    { 3, { 4, 3 }, 0.04f },
    { 3, { 3, 3 }, 0.06f },
    { 3, { 4, 1 }, 0.06f },
    { 3, { 4, 2 }, 0.06f },
    { 3, { 4, 3 }, 0.06f },
};
#define SPROBLEMCONFIGURATIONS_COUNT (sizeof(sProblemConfigurations) / sizeof(sProblemConfigurations[0]))
static INT sCurrentProblemConfiguration = -1;


static FLOAT getLevelTranslation(INT level)
{
    return (MAX_PROBLEM_STRUCTURE_LEVELS + 1 - level) * 0.1f;
}


static FLOAT smoothStep(FLOAT x, FLOAT a, FLOAT b)
{
    if (x < a)
        return 0.f;
    if (x >= b)
        return 1.f;
    x = (x - a) / (b - a);      // normalize to [0..1]
    return x * x * (3 - 2 * x);
}

static FLOAT smoothPulse(FLOAT a1, FLOAT a2, FLOAT b1, FLOAT b2, FLOAT x)
{
    return smoothStep(x, a1, a2) - smoothStep(x, b1, b2);
}


BuildState::BuildState() :
    mScore(0), mPathogenCount(0),
    mProblemRoot(MHT_BALL),
    mMouseX(0), mMouseY(0),
    mHighlight(FALSE), mDragging(FALSE),
    mMapMask(NULL)
{
    mFPS = 60;

    sQObj = gluNewQuadric();
    assert(sQObj != NULL);
    gluQuadricNormals(sQObj, GLU_FLAT);
    gluQuadricTexture(sQObj, GL_FALSE);

    sSphereList = glGenLists(1);
    glNewList(sSphereList, GL_COMPILE);
    gluSphere(sQObj, 0.1, SPHERE_SLICES, SPHERE_STACKS);
    glEndList();

    gluQuadricNormals(sQObj, GLU_NONE);
    sCylinderLists = glGenLists(MAX_PROBLEM_STRUCTURE_LEVELS);
    INT a;
    for (a = 0; a < MAX_PROBLEM_STRUCTURE_LEVELS; ++a)
    {
        const GLdouble radius = 0.02;
        glNewList(sCylinderLists + a, GL_COMPILE);
        gluCylinder(sQObj, radius, radius, getLevelTranslation(a),
                    CYLINDER_SLICES, CYLINDER_STACKS);
        glEndList();
    }
}


BuildState::~BuildState()
{
    gluDeleteQuadric(sQObj);
    sQObj = NULL;

    glDeleteLists(sSphereList, 1);
}


BOOL BuildState::init()
{
    mHexImage = loadImage("data/hex.png");
    mBallImage = loadImage("data/ball.png");
    mShineImage = loadImage("data/shine.png");
    mLinkImages[0] = loadImage("data/link1.png");
    mLinkImages[1] = loadImage("data/link2.png");
    mLinkImages[2] = loadImage("data/link3.png");
    mMapImage = loadImage("data/map.png");
    mCircleImage = loadImage("data/circle.png");
    mBlockImage = loadImage("data/block.png");
    mSquareImage = loadImage("data/square.png");
    mMapMask = IMG_Load("data/mapmask.png");

    return TRUE;
}


void BuildState::deinit()
{
    freeImage(mHexImage);
    freeImage(mBallImage);
    freeImage(mShineImage);
    freeImage(mLinkImages[0]);
    freeImage(mLinkImages[1]);
    freeImage(mLinkImages[2]);
    freeImage(mMapImage);
    freeImage(mCircleImage);
    freeImage(mBlockImage);
    freeImage(mSquareImage);
    SDL_FreeSurface(mMapMask);
}


BOOL BuildState::update(UINT32 time, App::KeyEventList &keyEvents)
{
    mTime = time;

    mScrollX = (mScrollX * 9 + mScrollXDest) * 0.1f;
    mScrollY = (mScrollY * 9 + mScrollYDest) * 0.1f;

    // all key presses are handled in onKey* event methods
    keyEvents.clear();

    if (mNormalizedProblemTime > 1 && !mMutationTriggerNewProblem && !mGameOver)
    {
        mGameOverTime = mTime;
        mGameOver = TRUE;
        TextRow tr("Game Over!", mTime, 4000);
        mInfoText.push_back(tr);
        char text[256];
        sprintf(text, "Pathogen %u defeated you.", mPathogenCount);
        tr.str = text;
        tr.age = 4500;
        mInfoText.push_back(tr);
    }

    if (!mGameOver && verifyProblem() && !mMutationTriggerNewProblem)
    {
        //createProblem();
        INT32 points = (mProblemStartTime + mProblemTime - mTime) / 10;
        if (points > 0)
            mScore += points;
        mutateProblem();
        mMutationTriggerNewProblem = TRUE;
    }

    if (mMutationTriggerTime > 0)
    {
        if (mTime - mMutationTriggerTime >= 1000 &&
            !mMutationTriggerMutated)
        {
            if (mMutationTriggerNewProblem)
                createProblem();
            else
                mutateNodes(&mProblemRoot);
            mMutationTriggerMutated = TRUE;
        }
        if (mTime - mMutationTriggerTime >= 2000)
            mMutationTriggerTime = 0;
    }

    mNormalizedProblemTime = (FLOAT)(mTime - mProblemStartTime) / mProblemTime;

    TextList::iterator it = mInfoText.begin();
    for (; it != mInfoText.end(); ++it)
    {
        TextRow &row = *it;
        if (row.startTime + row.age < mTime)
        {
            mInfoText.erase(it);
            break;
        }
    }

    return TRUE;
}


void BuildState::onActivate(UINT32 tick)
{
    mTime = tick;

    mScore = 0;
    mPathogenCount = 0;
    sCurrentProblemConfiguration = -1;
    mGameOverTime = 0;
    mGameOver = FALSE;

    createProblem();
}


void BuildState::onDeactivate()
{
}


void BuildState::onKeyDown(const SDLKey &key)
{
    switch (key)
    {
        /*
    case SDLK_RETURN:
        //createProblem();
        mutateProblem();
        mMutationTriggerNewProblem = TRUE;
        break;
        */
    case SDLK_SPACE:
        if (mGameOver)
            App::getInstance()->setState(GS_TITLE);
        else
            mutateProblem();
        break;
    case SDLK_DOWN: case SDLK_KP2:
        mScrollYDest -= 1;
        break;
    case SDLK_UP: case SDLK_KP8:
        mScrollYDest += 1;
        break;
    case SDLK_LEFT: case SDLK_KP4:
        mScrollXDest += 1;
        break;
    case SDLK_RIGHT: case SDLK_KP6:
        mScrollXDest -= 1;
        break;
    }
}


void BuildState::onKeyUp(const SDLKey &key)
{
}


void BuildState::onMouseDown(const SDL_MouseButtonEvent &event)
{
    if (event.button == SDL_BUTTON_LEFT)
    {
        if (mHighLightMapCircle)
            mutateProblem();
        else
        {
            // highlight node
            mHighlight = TRUE;
            mMouseDownNode = mHighlightNode;
            // start fiddling with the hexagon map
            mDragStartNode = mHighlightNode;
            mDragging = TRUE;
        }
    }
}


void BuildState::onMouseUp(const SDL_MouseButtonEvent &event)
{
    if (event.button == SDL_BUTTON_LEFT && mHighlight)
    {
        if (mHighlight)
        {
            if (mDragging && mDragStartNode != mHighlightNode)
            {
                HexagonMap::iterator it;
                MapNode *n1 = NULL, *n2 = NULL;
                it = mMap.find(mDragStartNode);
                if (it != mMap.end())
                {
                    n1 = (*it).second;
                    mMap.erase(it);
                }
                it = mMap.find(mHighlightNode);
                if (it != mMap.end())
                {
                    n2 = (*it).second;
                    mMap.erase(it);
                }
                if (n1 != NULL)
                {
                    mMap[mHighlightNode] = n1;
                    n1->position = mHighlightNode;
                }
                if (n2 != NULL)
                {
                    mMap[mDragStartNode] = n2;
                    n2->position = mDragStartNode;
                }
            }
            mDragging = FALSE;
            mHighlight = FALSE;
        }
        if (mDragStartNode == mHighlightNode)
        {
            HexagonMap::iterator it = mMap.find(mHighlightNode);
            if (it == mMap.end())
            {
                // current node is empty - make a new link there
                LinkNode *node = new LinkNode(MHT_LINK);
                mMap[mHighlightNode] = node;
            }
            else
            {
                MapNode *node = (*it).second;
                // if current node is a link, rotate or erase it
                if (mHighlightNode == mMouseDownNode &&
                    node->type == MHT_LINK &&
                    ++((LinkNode *)node)->link >= 3)
                {
                    delete node;
                    mMap.erase(it);
                }
            }
        }
    }
}


void BuildState::onMouseMotion(const SDL_MouseMotionEvent &event)
{
    mMouseX = event.x;
    mMouseY = event.y;

    if ((event.state & SDL_BUTTON(1)) && !(event.state & SDL_BUTTON(3)))
    {
        /*
        // drag with left button
        if (mDragStartNode != mHighlightNode)
        {
            HexagonMap::iterator it;
            MapNode *n1 = NULL, *n2 = NULL;
            it = mMap.find(mDragStartNode);
            if (it != mMap.end())
            {
                n1 = (*it).second;
                mMap.erase(it);
            }
            it = mMap.find(mHighlightNode);
            if (it != mMap.end())
            {
                n2 = (*it).second;
                mMap.erase(it);
            }
            if (n1 != NULL)
            {
                mMap[mHighlightNode] = n1;
                n1->position = mHighlightNode;
            }
            if (n2 != NULL)
            {
                mMap[mDragStartNode] = n2;
                n2->position = mDragStartNode;
            }

            mDragStartNode = mHighlightNode;
        }
        */
    }
    else if (!(event.state & SDL_BUTTON(1)) && (event.state & SDL_BUTTON(3)))
    {
        // drag with right button
        mScrollXDest -= event.xrel / ((FLOAT)mHexImage.w * 2 / 3);
        mScrollYDest -= event.yrel / (FLOAT)mHexImage.h;
    }
}


void BuildState::createProblem()
{
    clearProblem();

    ++sCurrentProblemConfiguration;
    if (sCurrentProblemConfiguration >= SPROBLEMCONFIGURATIONS_COUNT)
        sCurrentProblemConfiguration = SPROBLEMCONFIGURATIONS_COUNT - 1;

    INT balls = buildNodes(&mProblemRoot, NULL, 0,
                           sProblemConfigurations[sCurrentProblemConfiguration].levels);
    mutateNodes(&mProblemRoot);

    mProblemTime = balls * TIME_PER_COMPOUND;
    mProblemStartTime = mTime;
    mNormalizedProblemTime = 0;

    ++mPathogenCount;

    char text[256];
    TextRow tr("", mTime, 3000);
    sprintf(text, "Pathogen %u", mPathogenCount);
    tr.str = text;
    mInfoText.push_back(tr);
    sprintf(text, "%d compounds", balls);
    tr.str = text;
    mInfoText.push_back(tr);
    sprintf(text, "Time: %d seconds", mProblemTime / 1000);
    tr.str = text;
    mInfoText.push_back(tr);


    mMutationTriggerNewProblem = FALSE;

    // find a position from the map image

    int lockResult = SDL_LockSurface(mMapMask);
    if (lockResult < 0)
        return;
    assert(mMapMask->format->BitsPerPixel == 32);

    BOOL found = FALSE;
    do {
        INT x = rand() % mMapMask->w;
        INT y = rand() % mMapMask->h;
        UINT32 pix = *((UINT32 *)((UINT8 *)mMapMask->pixels + y * mMapMask->pitch) + x);
        INT r = (pix >> 16) & 0xff;
        INT g = (pix >> 8) & 0xff;
        INT b = pix & 0xff;
        if (r > 128 && g > 128 && b > 128)
        {
            found = TRUE;
            mMapImagePosition.x = x;
            mMapImagePosition.y = y;
        }
    } while(!found);

    SDL_UnlockSurface(mMapMask);
}


BOOL BuildState::verifyProblem()
{
    return verifyNode(&mProblemRoot) == mProblemRoot.children.size();
}


void BuildState::mutateProblem()
{
    if (mMutationTriggerTime > 0)
        return;
    mMutationTriggerTime = mTime;
    mMutationTriggerMutated = FALSE;
}


INT BuildState::verifyNode(BallNode *node)
{
    if (node->children.size() == 0)
        return 0;

    const INT dirX[6] = { -1, -1, 0, 0, 1, 1 };
    const INT dirY[2][6] = { { -1, 0, -1, 1, -1, 0 }, { 0, 1, -1, 1, 0, 1 } };
    const INT dirLink[6] = { 2, 1, 0, 0, 1, 2 };
    UINT dir;
    INT x = node->position.x;
    INT y = node->position.y;
    INT childrenFound = 0;

    //INT gx = mProblemRoot.position.x + dirX[aa];
    //INT gy = mProblemRoot.position.y + dirY[xi][aa];

    for (dir = 0; dir < 6; ++dir)
    {
        UINT xi = node->position.x & 1;
        Position p(x + dirX[dir], y + dirY[xi][dir]);
        HexagonMap::iterator it = mMap.find(p);
        if (it == mMap.end())
            continue;

        MapNode *testNode = (*it).second;
        if (testNode->type != MHT_LINK)
            continue;
        LinkNode *linkNode = (LinkNode *)testNode;
        while (it != mMap.end() && testNode->type == MHT_LINK &&
               linkNode->link == dirLink[dir])
        {
            xi = 1 - xi;
            p.x += dirX[dir];
            p.y += dirY[xi][dir];
            it = mMap.find(p);
            if (it != mMap.end())
            {
                testNode = (*it).second;
                if (testNode->type == MHT_LINK)
                    linkNode = (LinkNode *)testNode;
            }
        }
        /*
        //BallNode::BallNodeSet parentChildren = node->parent->children;
        if (it == mMap.end() || testNode.type != MHT_BALL ||
        //parentChildren.find((BallNode *)&testNode) == parentChildren.end())
        node->children.find((BallNode *)&testNode) == node->children.end())
        return childrenFound;
        else
        {
        if (verifyNode((BallNode *)&testNode) == node->children.size())
        ++childrenFound;
        }
        */
        if (it != mMap.end() && testNode->type == MHT_BALL)
        {
            BallNode *ballNode = (BallNode *)testNode;
            BOOL foundChild = node->children.find(ballNode) != node->children.end();
            if (foundChild)
            {
                INT children = verifyNode(ballNode);
                if (children == ballNode->children.size())
                    ++childrenFound;
            }
        }
    }

    return childrenFound;
}


INT BuildState::buildNodes(BallNode *node, BallNode *parent, INT level, INT depth)
{
    INT balls = 1;

    node->parent = parent;
    /*
    node->color.red = (float)rand() / RAND_MAX;
    node->color.green = (float)rand() / RAND_MAX;
    node->color.blue = (float)rand() / RAND_MAX;
    */
    FLOAT r, g, b;
    FLOAT h, l, s;
    BOOL added = FALSE;
    //FLOAT threshold = 0.15f;
    FLOAT threshold = 0.05f;
    UINT32 tries = 0;
    do {
        h = 360 * ((float)rand() / RAND_MAX);
        l = ((float)rand() / RAND_MAX) * 0.7f + 0.3f;
        s = ((float)rand() / RAND_MAX) * 0.6f + 0.4f;

        hls2rgb(&r, &g, &b, h, l, s);

        FLOAT minDistSq = 1000000;
        int a;
        for (a = 0; a < mColors.size(); ++a)
        {
            FLOAT rd = mColors[a].red - r;
            FLOAT gd = mColors[a].green - g;
            FLOAT bd = mColors[a].blue - b;
            rd *= 0.3f;
            gd *= 0.59f;
            bd *= 0.11f;
            FLOAT distSq = rd * rd + gd * gd + bd * bd;
            if (distSq < minDistSq)
                minDistSq = distSq;
        }

        if (minDistSq > threshold)
        {
            Color c(r, g, b);
            mColors.push_back(c);

            node->mapColor.red = r;
            node->mapColor.green = g;
            node->mapColor.blue = b;
            hls2rgb(&r, &g, &b, h, l * 0.9f, s);    // make 3D ball a bit darker
            node->color.red = r;
            node->color.green = g;
            node->color.blue = b;
            added = TRUE;
        }
        if (++tries > 1000)
        {
            // fallback just to be safe..
            tries = 0;
            threshold -= 0.01f;
        }
    } while (!added);

    if (parent == NULL)
    {
        node->position.x = 0;
        node->position.y = 0;
        mMap[node->position] = node;
    }
    else
    {
        Position p;
        p.x = parent->position.x;
        p.y = parent->position.y;
        BOOL foundPos = FALSE;
        do {
            if (mMap.find(p) == mMap.end())
                foundPos = TRUE;
            else
            {
                p.x += rand() % 3 - 1;
                p.y += rand() % 3 - 1;
            }
        } while (!foundPos);
        assert(mMap.find(p) == mMap.end());
        node->position = p;
        mMap[p] = node;
    }


    /*
    --depth;
    assert(depth >= 0);
    if (depth <= 0)
        return balls;
    */
    ++level;
    assert(level <= depth);
    if (level >= depth)
        return balls;

    const INT newNodeCount = sProblemConfigurations[sCurrentProblemConfiguration].branches[level - 1];
    INT a;
    for (a = 0; a < newNodeCount; ++a)
    {
        BallNode *newNode = new BallNode(MHT_BALL);
        node->children.insert(newNode);
        balls += buildNodes(newNode, node, level, depth);
    }

    return balls;
}


void BuildState::mutateNodes(BallNode *node)
{
    node->direction.angle = 360 * (float)rand() / RAND_MAX;
    node->direction.x = 2 * (float)rand() / RAND_MAX - 1;
    node->direction.y = 2 * (float)rand() / RAND_MAX - 1;
    node->direction.z = 2 * (float)rand() / RAND_MAX - 1;

    BallNode::BallNodeSet::iterator it = node->children.begin();
    BallNode::BallNodeSet::iterator itend = node->children.end();

    for (; it != itend; ++it)
        mutateNodes(*it);
}


void BuildState::clearProblem()
{
    mScrollX = mScrollXDest = 1;
    mScrollY = mScrollYDest = 1;

    mColors.clear();
    clearNode(&mProblemRoot);
    mMap.clear();
}


void BuildState::clearNode(BallNode *node)
{
    BallNode::BallNodeSet::iterator it = node->children.begin();
    BallNode::BallNodeSet::iterator itend = node->children.end();

    for (; it != itend; ++it)
    {
        clearNode(*it);
        delete *it;
    }
    node->children.clear();
}


void BuildState::render(SDL_Surface * /*screen*/)
{
    FLOAT r = 0;
    if (mGameOver)
        r += 0.3f * smoothStep(mTime, mGameOverTime, mGameOverTime + 2000);
    clear(0.3f + r, 0.3f, 0.4f);

    renderModel();
    render2D();
}


void BuildState::render2D()
{
    glDisable(GL_LIGHTING);
    enter2DMode(WIDTH, HEIGHT);

    // draw map image and the pulsating circle
    INT mapX = WIDTH - mMapImage.w;
    INT mapY = (HEIGHT / 3 - mMapImage.h) / 3;
    drawImage(mMapImage, mapX, mapY);
    FLOAT scale = 0.02f + sin(mTime * 0.004f) * 0.02f + 1.5f * mNormalizedProblemTime;
    FLOAT radius = scale * mCircleImage.w / 2;
    INT circlePosX = mapX + mMapImagePosition.x;
    INT circlePosY = mapY + mMapImagePosition.y;
    BOOL inside = (mMouseX - circlePosX) * (mMouseX - circlePosX) +
                  (mMouseY - circlePosY) * (mMouseY - circlePosY) <
                  radius * radius;
    mHighLightMapCircle = inside;
    drawImageRotatedCenteredScaledAdditive(mCircleImage,
                                           circlePosX, circlePosY,
                                           0, scale, 1, inside ? 0.5f : 0, 0);

    // draw time bar below the map
    FLOAT timeBarWidth = (1 - mNormalizedProblemTime) * mMapImage.w;
    if (timeBarWidth < 1) timeBarWidth = 1;
    if (timeBarWidth > mMapImage.w) timeBarWidth = mMapImage.w;
    drawImageScaled(mBlockImage,
                    WIDTH - timeBarWidth, mapY + mMapImage.h + 8,
                    timeBarWidth, mBlockImage.h);

    renderHexagonMap();

    // draw text rows
    GameApp *app = (GameApp *)App::getInstance();
    Font *font = app->getFont();
    INT y = HEIGHT / 2 - mInfoText.size() * font->getFontHeight() / 2;
    TextList::iterator it = mInfoText.begin();
    for (; it != mInfoText.end(); ++it)
    {
        TextRow &row = *it;
        const char *text = row.str.c_str();
        FLOAT alpha = 1 - smoothStep(mTime,
                                     row.startTime + row.age * 0.9f,
                                     row.startTime + row.age);
        font->drawString(WIDTH / 3, y,
                         FONT_ALIGN_HORIZ_CENTER, text,
                         row.color.red, row.color.green, row.color.blue,
                         alpha);
        y += font->getFontHeight();
    }

    // score
    char scoreStr[32];
    sprintf(scoreStr, "Score: %u", mScore);
    font->drawString(WIDTH - mMapImage.w, HEIGHT / 3,
                     FONT_ALIGN_VERT_CENTER, scoreStr);

    // mutation changing
    if (mMutationTriggerTime > 0)
    {
        FLOAT alpha = smoothPulse(0, 1000, 1000, 2000, mTime - mMutationTriggerTime);
        font->drawString(WIDTH - 1, HEIGHT / 3 + font->getFontHeight() * 4,
                         FONT_ALIGN_RIGHT | FONT_ALIGN_VERT_CENTER,
                         "Fetching a pathogen sample...", 0.5f, 0.75f, 1, alpha);
    }

    // game over anim
    if (mGameOver && mTime > mGameOverTime + 5000)
    {
        UINT32 startTime = mGameOverTime + 5000;
        FLOAT angle = mTime * 0.001f;
        FLOAT scale = (mTime - startTime) * 0.01f;
        drawImageRotatedCenteredScaled(mSquareImage, WIDTH / 2, HEIGHT / 2,
                                       angle, scale);
        if (scale > 35)
            App::getInstance()->setState(GS_TITLE);
    }

    leave2DMode();
}


void BuildState::renderNode(BallNode *node, INT level)
{
    glRotatef(mTime * sProblemConfigurations[sCurrentProblemConfiguration].rotation, 1, 1, 1);

    glColor3f(node->color.red, node->color.green, node->color.blue);
    glCallList(sSphereList);

    BallNode::BallNodeSet::iterator it = node->children.begin();
    BallNode::BallNodeSet::iterator itend = node->children.end();

    for (; it != itend; ++it)
    {
        glPushMatrix();

        BallNode *node = *it;

        glRotatef(node->direction.angle,
                  node->direction.x,
                  node->direction.y,
                  node->direction.z);
        FLOAT translation = getLevelTranslation(level);
        glDisable(GL_LIGHTING);
        glColor3f(0, 0, 0);
        glCallList(sCylinderLists + level);
        glEnable(GL_LIGHTING);
        glTranslatef(0, 0, translation);

        renderNode(node, level + 1);

        glPopMatrix();
    }
}


void BuildState::renderModel()
{
    const INT viewportWidth = WIDTH / 2;
    const INT viewportHeight = HEIGHT * 2 / 3;
    glViewport(WIDTH / 2, 0, viewportWidth, viewportHeight);

    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    gluPerspective(45, (float)viewportWidth / viewportHeight, 0.01f, 5);
    glMatrixMode(GL_MODELVIEW);

    glLoadIdentity();

    glEnable(GL_DEPTH_TEST);

    glEnable(GL_LIGHTING);
    glEnable(GL_LIGHT0);
    glEnable(GL_CULL_FACE);
    glCullFace(GL_BACK);
    glShadeModel(GL_FLAT);

    GLfloat lightPosition[] = { 0, 2, 2, 0 };
    glLightfv(GL_LIGHT0, GL_POSITION, lightPosition);
    GLfloat lightAmbient[] = { 0.2f, 0.0f, 0.0f, 1 };
    glLightfv(GL_LIGHT0, GL_AMBIENT, lightAmbient);
    GLfloat lightDiffuse[] = { 1, 1, 1, 1 };
    glLightfv(GL_LIGHT0, GL_DIFFUSE, lightDiffuse);
    GLfloat lightSpecular[] = { 1, 1, 1, 1 };
    glLightfv(GL_LIGHT0, GL_SPECULAR, lightSpecular);

    GLfloat zeros[] = { 0, 0, 0, 1 };
    GLfloat ones[] = { 1, 1, 1, 1 };
    glMaterialfv(GL_FRONT, GL_AMBIENT, zeros);
    glMaterialfv(GL_FRONT, GL_DIFFUSE, ones);
    glMaterialfv(GL_FRONT, GL_SPECULAR, ones);
    glMaterialfv(GL_FRONT, GL_EMISSION, zeros);
    glMaterialf(GL_FRONT, GL_SHININESS, 60);

    glColorMaterial(GL_FRONT, GL_DIFFUSE);
    glEnable(GL_COLOR_MATERIAL);

    FLOAT yOffset = -0.1f;
    if (mMutationTriggerTime > 0)
        yOffset -= 2 * smoothPulse(0, 1000, 1000, 2000, mTime - mMutationTriggerTime);
    glTranslatef(0, yOffset, -2);

    glRotatef(mTime * 0.05f, 0, 1, 0);

    BallNode *node = &mProblemRoot;
    renderNode(node, 0);
}


void BuildState::renderHexagonMap()
{
    Position p;
    FLOAT columnWidth = mHexImage.w * 2 / 3;
    FLOAT cellWidth = mHexImage.w;
    FLOAT cellHeight = mHexImage.h;
    INT gx, gy;
    INT mapCenterX = (INT)mScrollX;
    INT mapCenterY = (INT)mScrollY;
    Position mouseNode;
    FLOAT mouseDistSq = 10000000000.f;

    for (gy = -8; gy < 5; ++gy)
    {
        for (gx = -6 + mapCenterX % 2; gx < 6; ++gx)
        {
            p.x = gx + mapCenterX;
            p.y = gy + mapCenterY;
            FLOAT x = (gx + 1 - fmod(mScrollX, 1)) * columnWidth;
            FLOAT y = (gy + 1 - fmod(mScrollY, 1)) * cellHeight;
            x += WIDTH / 4;
            y += HEIGHT / 2 + (p.x & 1) * cellHeight / 2;
            const FLOAT fadeStartX = WIDTH / 2 - columnWidth;
            const FLOAT fadeEndX = WIDTH / 2 + columnWidth * 2;
            FLOAT alpha = 1 - smoothStep(x, fadeStartX, fadeEndX);

            FLOAT centerX = x + cellWidth / 2;
            FLOAT centerY = y + cellHeight / 2;
            FLOAT distSq = (centerX - mMouseX) * (centerX - mMouseX) +
                           (centerY - mMouseY) * (centerY - mMouseY);
            if (distSq < mouseDistSq)
            {
                mouseNode = p;
                mouseDistSq = distSq;
            }

            drawImage(mHexImage, x, y, 1, 1, 1, alpha);
            if (mHighlight && mHighlightNode.x == p.x && mHighlightNode.y == p.y)
                drawImageAdditive(mHexImage, x, y, 0, 1, 1, 1, alpha);
            if (mDragging && mDragStartNode.x == p.x && mDragStartNode.y == p.y)
            {
                FLOAT pulse = (FLOAT)(sin(mTime * 0.02f) * 0.5f + 0.5f);
                drawImageAdditive(mHexImage, x, y, 0, 1, 1, 1, alpha * pulse);
            }
            /*
            if (p.x == 0 && p.y == 0)
                drawImageAdditive(mHexImage, x, y, 0, 1, 1, 1, alpha);
            */
            /*
            const INT dirX[6] = { -1, -1, 0, 0, 1, 1 };
            const INT dirY[2][6] = { { -1, 0, -1, 1, -1, 0 }, { 0, 1, -1, 1, 0, 1 } };
            int aa;
            for (aa = 0; aa < 6; ++aa) {
                UINT xi = mProblemRoot.position.x & 1;
                INT gx = mProblemRoot.position.x + dirX[aa];
                INT gy = mProblemRoot.position.y + dirY[xi][aa];
                if (p.x == gx && p.y == gy)
                    drawImageAdditive(mHexImage, x, y, 0, 1, 1, 1, alpha);
            }
            */


            HexagonMap::iterator it = mMap.find(p);
            if (it != mMap.end())
            {
                MapNode &node = *(*it).second;
                switch (node.type)
                {
                case MHT_BALL:
                    {
                        x += columnWidth / 4;
                        BallNode &bn = (BallNode &)node;
                        drawImage(mBallImage, x, y,
                                  bn.mapColor.red,
                                  bn.mapColor.green,
                                  bn.mapColor.blue, alpha);
                        drawImageAdditive(mShineImage, x, y, 0, 1, 1, 1, alpha);
                    }
                    break;
                case MHT_LINK:
                    {
                        LinkNode &ln = (LinkNode &)node;
                        assert(ln.link < 3);
                        drawImage(mLinkImages[ln.link], x, y, 1, 1, 1, alpha);
                    }
                    break;
                }
            }
        }
    }

    mHighlightNode = mouseNode;
}
