// ---------------------------------------------------------------------------
// 
//   Lily Engine alpha
// 
//   Copyright (c) 2010 Clifford Jolly
//     http://cliffjolly.com
//     cliff@cliffjolly.com
// 
// ---------------------------------------------------------------------------
// 
//   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
// -------------------------- END HEADER -------------------------------------

#include "../hacks/winhacks.h"

#include <sstream>
#include <iostream>
#include <fstream>
using namespace std;

#include "../console/console.h"
using namespace Console;

#include <boost/function.hpp>

#include <GL/glew.h>
#include <GL/gl.h>

#include "../texturemanager/texture.h"
#include "../texturemanager/texturemanager.h"
#include "../filesystem/filesystem.h"
#include "../string/string.h"
#include "../image/image.h"

#include "entity.h"
#include "editorui.h"
#include "boundingbox.h"
#include "world.h"
#include "quadtree.h"
#include "terrainblock.h"
#include "scriptentity.h"
#include "scenerychunk.h"
#include "renderer.h"

#include "../uijunk/widget.h"

// -------------------------------------------------------------------
//  EditorUI undo actions
// -------------------------------------------------------------------

// Basic undo action class. Doesn't do anything.
class EditorUndoAction {

    public:

        EditorUndoAction(World *world, EditorUI *editor) {

            this->world = world;
            this->editor = editor;
            desc = "???";
        }

        virtual void execute(void) {
            
        }

        std::string getDesc(void) {
            return desc;
        }

    protected:

        EditorUI *editor;
        World *world;
        std::string desc;
};

// Tile drawing undo.
class UndoTileDrawing : public EditorUndoAction {

    public:

        UndoTileDrawing(World *world, EditorUI *editor) : EditorUndoAction(world, editor) {
            desc = "Tile drawing";
        }

        // We want tile drawing undo to undo everything from the last
        // time the mouse button was pressed to when it was released,
        // so what we're really going to do it push one on the stack
        // and add old tiles with this as we draw.
        void addOldTile(int x, int y, int layer, int tile) {

            OldTile ot;
            ot.x = x;
            ot.y = y;
            ot.layer = layer;
            ot.tile = tile;

            tileActions.push_back(ot);
        }

        void execute(void) {

            int i = unsigned(tileActions.size());
            bool alreadyPrintedLayerError = false;
            
            while(i) {
                i--;

                if(tileActions[i].layer >= world->getNumLayers()) {
                    
                    if(!alreadyPrintedLayerError) {
                        alreadyPrintedLayerError = true;

                        out("error") << "FIXME: Trying to undo tiles on a layer that doesn't exist anymore!" << endl;
                    }
                    
                } else {

                    world->setTile(
                        tileActions[i].x,
                        tileActions[i].y,
                        tileActions[i].layer,
                        tileActions[i].tile);
                }
            }
        }

    private:

        struct OldTile {
                int x;
                int y;
                int layer;
                int tile;
        };

        vector<OldTile> tileActions;

};

// Generic undo - For whatever function we want to set up as an undo.
class UndoGeneric : public EditorUndoAction {
        
    public:

        UndoGeneric(World *world, EditorUI *editor, const std::string &desc, boost::function<void()> func) : EditorUndoAction(world, editor) {
            this->desc = string("Function: ") + desc;
            this->func = func;
        }

        void execute(void) {

            func();

        }

    private:

        boost::function<void()> func;

};

// -------------------------------------------------------------------
//  EditorUI implementation
// -------------------------------------------------------------------

void EditorUI::undo(void) {

    if(undoActions.size()) {

        int num = undoActions.size() - 1;

        out("system") << "Undo: " << undoActions[num]->getDesc() << endl;

        undoActions[num]->execute();
        delete undoActions[num];
        undoActions.erase(undoActions.end() - 1);

    } else {

        out("system") << "Nothing left to undo." << endl;

    }
}

void EditorUI::clearUndoActions(void) {
    for(unsigned int i = 0; i < undoActions.size(); i++) {
        delete undoActions[i];
    }
    undoActions.clear();
}


void EditorUI::addUndoAction(const std::string &desc, boost::function<void()> action) {
    UndoGeneric *newAction = new UndoGeneric(world, this, desc, action);
    undoActions.push_back(newAction);
}

EditorUI::EditorUI(World *world, bool debugOnly) : junkInterface(world, !debugOnly) {
	this->world = world;
	world->editor = this;
	
	visible = false;
	dragObject = NULL;
    linkObject = NULL;
	dragPoint = NULL;
	dragOffset = Vector2D(0, 0);
	gridSpacing = 16;
	gridSnapping = true;
	screenDragging = false;
	tileDrawing = false;
	tileDrawingHeld = false;
	lastActiveLayer = -1;
	lastMousePos = Vector2D(0, 0);
	overrideEditorOnlyVisibility = false;
	ignoreNextMouseUp = false;
	
	populateSpawnList();
}

EditorUI::~EditorUI(void) {
}

void EditorUI::populateSpawnList(void) {
	
	// Clear out anything already there.
	junkInterface.clearEntityPaletteList();
	
	// Add built-in terrain blocks.
	junkInterface.addEntityPaletteItem("Terrain_Triangle");
	junkInterface.addEntityPaletteItem("Terrain_Quad");
	
	// Get the new spawn list.
	vector<string> spawnList = world->getSpawnList();
	
	// Add them.
	for(unsigned int i = 0; i < spawnList.size(); i++) {
		junkInterface.addEntityPaletteItem(spawnList[i]);
	}
}

std::string EditorUI::getLastSelected(void) const {
	
	if(selection.size()) {
		
		// Return the last object in the list.
		return selection[selection.size() - 1];
	}
	
	// Nothing selected?
	return "";
}

void EditorUI::getAllSelected(std::vector<std::string> &selection) const {
	selection = this->selection;
}

void EditorUI::drawTiles(int x, int y, int tile) {
	
    UndoTileDrawing *undoAction = NULL;
    if(undoActions.size()) {
        undoAction = dynamic_cast<UndoTileDrawing*>(undoActions[undoActions.size() - 1]);
    }

	int tileSize = world->layers[junkInterface.getActiveLayer()]->tilePane.getTileSize();
	
	// We need to fix some stuff due to rounding.
	if(x < 0) x -= tileSize;
	if(y < 0) y -= tileSize;

    int rx = x / tileSize;
    int ry = y / tileSize;
    int layer = junkInterface.getActiveLayer();

    // Add this to our current drawing undo action, if any.
    if(undoAction) {

        int oldTile = world->getTile(rx, ry, layer);

        if(oldTile != tile) {
            undoAction->addOldTile(rx, ry, layer, oldTile);
        }
    }

    world->layers[layer]->tilePane.setTile(rx, ry, tile);

}

int EditorUI::pickTile(int x, int y) {
	
	int tileSize =  world->layers[junkInterface.getActiveLayer()]->tilePane.getTileSize();
	
	// We need to fix some stuff due to rounding.
	if(x < 0) x -= tileSize;
	if(y < 0) y -= tileSize;
	
	return world->layers[junkInterface.getActiveLayer()]->tilePane.getTile(x / tileSize, y / tileSize);
}

bool EditorUI::handleEvent(SDL_Event *event) {
	
	// If we aren't visible, we don't do any editor interaction at all.
	if(!visible) return false;
	
	Widget *rootWidget = getRootWidget();
	WidgetEvent convertedEvent;
	Vector2D screenSize = world->getRenderer()->getScreenSize();
	widgetPushTransforms(screenSize.x, screenSize.y);
	widgetConvertSDLEvent(event, &convertedEvent);
	
    Uint8 *keys = SDL_GetKeyState(NULL);

   	if(convertedEvent.type != WIDGETEVENT_IDUNNO) {
		
		// Actually handle the event.
		bool ret = rootWidget->handleEvent(&convertedEvent);
		
		// If we're on a modal dialog (anything above the main UI) then
		// it doesn't let anything through.
		ret |= (junkInterface.getWidgetStackSize() > 1);
		
		bool uglyMouseUpHack = false;
		
		if(convertedEvent.type == WIDGETEVENT_MOUSEUP && ignoreNextMouseUp) {
			uglyMouseUpHack = true;
			ignoreNextMouseUp = false;
		}
		
		// Note: This gets all mouse events except mouse up events
		// because they don't do anything bad if extra events are sent
		// and to properly handle drag events in the main editor.
		if(	convertedEvent.type == WIDGETEVENT_MOUSEDOWN || 
			convertedEvent.type == WIDGETEVENT_MOUSEMOTION ||
			
			// FIXME: Don't mix SDL and Widget events.
			((event->button.button == SDL_BUTTON_WHEELUP ||
			event->button.button == SDL_BUTTON_WHEELDOWN) &&
			convertedEvent.type == WIDGETEVENT_MOUSEUP) ||
			
			uglyMouseUpHack) {
			
			if(junkInterface.mouseOverAWidget) {
				
				if(convertedEvent.type == WIDGETEVENT_MOUSEDOWN) {
					ignoreNextMouseUp = true;
				}
				
				// Whatever this mouse event was, it happened over one
				// of the UIJunk widgets.
				ret = true;
			}
		}
		
		if(ret) {
			glEnable(GL_TEXTURE_2D);
			widgetPopTransforms();
			
			//cout << "UI ate event" << endl;
			
			return true;
			
		} else {
			
			//cout << "UI didn't eat that one" << endl;
		}
		
	}
	
	widgetPopTransforms();
	
	// Some stuff the junkUI might leave dirty...
	glEnable(GL_TEXTURE_2D);

	// Mouse click events.
	if(event->type == SDL_MOUSEBUTTONDOWN) {
		
		// If it's a mouse wheel thing, we don't care.
		if(event->button.button == SDL_BUTTON_WHEELUP || event->button.button == SDL_BUTTON_WHEELDOWN) {
			return true;
		}
		
		// Use the middle mouse button for screen dragging.
		if(event->button.button == SDL_BUTTON_MIDDLE) {
			screenDragging = true;
			return true;
		}
		
		// Get the mouse location in world space.
		Vector2D mouseLoc = world->mouseEventToWorld(event, world->getCamScaleForLayer(junkInterface.getActiveLayer()));

		// First, see if we're in the special tile drawing mode and should react to that.		
		if(event->button.button == SDL_BUTTON_LEFT && tileDrawing) {
			
            // FIXME: We already do this up above. Do we need to again?
			Uint8 *keys = SDL_GetKeyState(NULL);
			
			// Handle tile picking if CTRL is held. Otherwise draw.
			if(keys[SDLK_LCTRL]) {

				int tile = pickTile(int(mouseLoc.x), int(mouseLoc.y));
				
				if(keys[SDLK_LSHIFT]) {
					
					// Multi-select picker.
					junkInterface.addPickedTile(tile);
					
				} else {
					
					//  Single-select picker.
					junkInterface.clearPickedTiles();
					junkInterface.addPickedTile(tile);
					
				}
				
			} else {
				
                // Push a new tile drawing undo action.
                UndoTileDrawing *undoAction = new UndoTileDrawing(world, this);
                undoActions.push_back(undoAction);

				drawTiles(int(mouseLoc.x), int(mouseLoc.y), junkInterface.getCurrentTile());
				tileDrawingHeld = true;
				return true;
			}
		}
		
		// The selection area is actually bigger than a single point.
		// This lets us easily select things that are infinitely small
		// or one pixel without having to be exactly on it.
		float selectionRadius = 2 / world->zoomLevel;
		
		// Create a bounding box of the appropriate size.
		BoundingBox selectionBox(mouseLoc.x - selectionRadius, mouseLoc.y + selectionRadius, mouseLoc.x + selectionRadius, mouseLoc.y - selectionRadius);
		
		// Check to see if we're clicking on a point on an already selected TerrainBlock
		//   If yes, initiate editing drag (modify vertices of the polygon),
		//   then return
		if(selection.size() == 1) {
			
			// Is it a TerrainBlock?
			TerrainBlock *tb = dynamic_cast<TerrainBlock*>(world->entitiesByName[selection[0]]);
			if(tb) {
				
				// Yes. Get the polygon.
				Polygon *poly = &(tb->poly);
				
				// Iterate through the points on the polygon and see
				// which we're selecting, if any.
				for(unsigned int i = 0; i < poly->points.size(); i++) {
					
					// Make a bounding box for the handles of the point.
					BoundingBox bb(
						poly->points[i] + poly->offset + Vector2D(-4, 4) / world->zoomLevel,
						poly->points[i] + poly->offset + Vector2D(4, -4) / world->zoomLevel);
					
					// If it overlaps with the selection box, then we're
					// going to initiate the editing drag.	
					if(selectionBox.overlap(bb)) {
						dragObject = tb;
						dragPoint = &(poly->points[i]);
						
						// Save the original point location for the
						// duration of the drag.
						originalDragPos = *dragPoint;
						
						return true;
					}
				}
			}
		}
		
		// Check to see if we're clicking inside an already selected
		// object's bounds. If yes, initiate movement drag, then
		// return.
		for(unsigned int i = 0; i < selection.size(); i++) {

            if(!world->entitiesByName[selection[i]]) continue;

            // Check for the selection's bounding box overlapping the
			// selectionBox. If yes, initiate movement drag.
			if(selectionBox.overlap(world->entitiesByName[selection[i]]->getBoundingBox())) {

                // Save the values we have before screwing everything up.
                updateFromInspector();

                if(keys[SDLK_LCTRL]) {

                    // Control held down, initiate link mode.
                    linkObject = world->entitiesByName[selection[i]];

                } else {

                    dragObject = world->entitiesByName[selection[i]];
                    dragOffset = mouseLoc;
				
                    // Save the original location of the object.
                    originalDragPos = dragObject->getPos();

                }
				
				return true;
			}
		}
		
		// At this point, we're pretty sure the user isn't trying to
		// drag anything. So they're probably trying to select a new
		// object.
		
		// Figure out what objects overlap the selection box.
		vector<Entity *> ents;
		world->getQuadTree(junkInterface.getActiveLayer())->getEntsTouchingBox(selectionBox, ents);
		
		// We're going to save whatever was last selected, so we don't
		// just select it again.
		string lastSelection = "";
		if(selection.size()) {
			lastSelection = selection[0];
		}
		
		// Clear the current selection.
        updateFromInspector();
		selection.clear();
		
		// Iterate through the objects that have overlapping bounding
		// boxes with the cursor.
		for(unsigned int i = 0; i < ents.size(); i++) {
			
			// Find the first object that isn't the last object we 
			// selected and select that.
			if(ents[i]->getName() != lastSelection) {
				selection.push_back(ents[i]->getName());
				out("console") << "Selecting " << ents[i]->getName() << "." << endl;
				break;
			}
		}
		
		// Bring up the new parameter list in the inspector.
		updateInspector();
		
		// TODO: Drag selection box?
	}
	
	// Mouse movement events.
	if(event->type == SDL_MOUSEMOTION) {
		
		// Save the mouse location.
		lastMousePos = world->mouseEventToWorld(event, world->getCamScaleForLayer(junkInterface.getActiveLayer()));
		
		// Screen dragging movement.
		if(screenDragging) {
			Vector2D dragAmount = world->mouseRelToWorld(event);
			world->cameraLoc -= dragAmount;
			return true;
		}
		
		// Tile drawing.
		if(tileDrawingHeld) {
			drawTiles(int(lastMousePos.x), int(lastMousePos.y), junkInterface.getCurrentTile());
		}
		
		// If dragObject and dragPoint are not NULL, then we're
		// modifying individual points on an object like a TerrainBlock.
		if(dragObject && dragPoint) {
			
			// Get the new position, offset from the object's own position.
			Vector2D newPoint = world->mouseEventToWorld(event, world->getCamScaleForLayer(dragObject->getLayer())) - dragObject->getPos();
			
			// Apply snapping to the nearest grid point.
			if(gridSnapping) {
				float xfudge = (newPoint.x > 0) ? 0.5 : -0.5;
				float yfudge = (newPoint.y > 0) ? 0.5 : -0.5;
				newPoint.x = int(newPoint.x / float(gridSpacing) + xfudge) * gridSpacing;
				newPoint.y = int(newPoint.y / float(gridSpacing) + yfudge) * gridSpacing;
			}
			
			// Change the position.
			*dragPoint = newPoint;
			
			// If we've got a TerrainBlock that we're dealing with,
			// we're going to deal with rebuilding the bounding box and
			// making sure it's convex. I'm sure there's a better way
			// to deal with this that adheres to proper OO doctrine, but
			// this works.
			TerrainBlock *tb = dynamic_cast<TerrainBlock*>(dragObject);
			if(tb) {
				for(unsigned int i = 0; i < tb->poly.points.size(); i++) {
					tb->poly.fixConcave(i);
					tb->buildBoundingBox();
				}
			}
			
			// The object's bounding box has possibly changed.
			// Reinsert it into the quadtree for consistency.
			world->getQuadTree(dragObject->getLayer())->reinsert(dragObject);
			
			return true;
		}
		
		// Lastly, if we just have something selected, drag it.
		if(dragObject) {
			// Figure out the new position based on the mouse movement
			// and its original position.
			Vector2D newPoint = world->mouseEventToWorld(event, world->getCamScaleForLayer(dragObject->getLayer())) - (dragOffset - originalDragPos);
			
			// Snap position.
			if(gridSnapping) {
				float xfudge = (newPoint.x > 0) ? 0.5 : -0.5;
				float yfudge = (newPoint.y > 0) ? 0.5 : -0.5;
				newPoint.x = int(newPoint.x / float(gridSpacing) + xfudge) * gridSpacing;
				newPoint.y = int(newPoint.y / float(gridSpacing) + yfudge) * gridSpacing;
			}
			
			// Set the position.
			dragObject->setPos(newPoint);
			
			return true;
		}
	}
	
	if(event->type == SDL_MOUSEBUTTONUP) {
		
		Vector2D mouseLoc = world->mouseEventToWorld(event);
		
		// Stop drawing, if we're drawing.
		if(event->button.button == SDL_BUTTON_LEFT && tileDrawing) {
			tileDrawingHeld = false;
			return true;
		}
		
		// Screen dragging.
		if(event->button.button == SDL_BUTTON_MIDDLE) {
			screenDragging = false;
			return true;
		}
		
		// Mouse zoom in/out
		if(event->button.button == SDL_BUTTON_WHEELUP) {
			world->zoomLevel *= 2.0;
            if(world->zoomLevel > 128) world->zoomLevel = 128;
			out("console") << "Zoom level increased: " << world->zoomLevel << "x" << endl;
			return true;
		}
		
		if(event->button.button == SDL_BUTTON_WHEELDOWN) {
			world->zoomLevel /= 2.0;
            if(world->zoomLevel < 0.0625) world->zoomLevel = 0.0625;
			out("console") << "Zoom level decreased: " << world->zoomLevel << "x" << endl;
			return true;
		}
		
		float selectionRadius = 2 / world->zoomLevel;
		BoundingBox selectionBox(mouseLoc.x - selectionRadius, mouseLoc.y + selectionRadius, mouseLoc.x + selectionRadius, mouseLoc.y - selectionRadius);

		// If the mouse didn't move, but we have something that we were
		// going to drag, that probably means that we want to select the
		// other thing in that place.
		if((mouseLoc - dragOffset).isZero() && dragObject) {
			vector<Entity *> ents;
			world->getQuadTree(junkInterface.getActiveLayer())->getEntsTouchingBox(selectionBox, ents);
			string lastSelection = "";
			if(selection.size()) {
				lastSelection = selection[0];
			}
			selection.clear();
			for(unsigned int i = 0; i < ents.size(); i++) {
				if(ents[i]->getName() != lastSelection && (ents[i]->getLayer() == junkInterface.getActiveLayer() || (ents[i]->getLayer() < 0 || ents[i]->getLayer() > 5))) {
					selection.push_back(ents[i]->getName());
					break;
				}
			}
		
		}

        // Handle object linking.
		if(event->button.button == SDL_BUTTON_LEFT && linkObject) {

			vector<Entity *> ents;
            world->getQuadTree(junkInterface.getActiveLayer())->getEntsTouchingBox(selectionBox, ents);

            bool hasHit = ents.size() && ents[0] != linkObject;
            
            if(keys[SDLK_LSHIFT]) {

                if(hasHit) {
                    linkObject->addLinkTarget(ents[0]->getName());
                }

            } else {

                if(hasHit) {
                    linkObject->setLinkTarget(ents[0]->getName());
                } else {
                    linkObject->setLinkTarget("");
                }
            }

            linkObject = NULL;
        }
		
		// If something's been selected, or something's been moved, we
		// need to update the inspector with whatever.
		updateInspector();
		
		// Stop dragging.
		dragObject = NULL;
		dragPoint  = NULL;
        linkObject = NULL;
		
		return true;
	}
	

	// Handle some of our keyboard editor commands.
	if(event->type == SDL_KEYDOWN) {
		
		// TODO: Document all these keyboard shortcuts somewhere.
		
		// Delete - Deletes an object.
		if(event->key.keysym.sym == SDLK_DELETE) {
			if(!dragObject && !linkObject) {
				for(unsigned int i = 0; i < selection.size(); i++) {
					world->entitiesByName[selection[i]]->dead = true;
				}
				selection.clear();
				updateInspector();
				world->cleanUpDeadEntities();
				return true;
			}
		}
		
		// +/- on the keypad - Zoom in and out.
		if(event->key.keysym.sym == SDLK_KP_PLUS) {
			world->zoomLevel *= 2.0;
			out("console") << "Zoom level increased: " << world->zoomLevel << "x" << endl;
		}
		if(event->key.keysym.sym == SDLK_KP_MINUS) {
			world->zoomLevel /= 2.0;
			out("console") << "Zoom level decreased: " << world->zoomLevel << "x" << endl;
		}
		
		// [/] (brackets) - Change the size of the grid.
		if(event->key.keysym.sym == SDLK_LEFTBRACKET) {
			gridSpacing /= 2;
			if(gridSpacing < 1) gridSpacing = 1;
			out("console") << "Grid spacing decreased: " << gridSpacing << " pixel" << ((gridSpacing > 1) ? "s" : "") << endl;
		}
		if(event->key.keysym.sym == SDLK_RIGHTBRACKET) {
			gridSpacing *= 2;
			out("console") << "Grid spacing increased: " << gridSpacing << " pixels" << endl;
		}
		
		// g - Disable/enable grid snapping.
		if(event->key.keysym.sym == SDLK_g) {
			gridSnapping = !gridSnapping;
			out("console") << "Grid snapping: " << (gridSnapping ? "enabled" : "disabled") << endl;
		}
		
        // Numpad keys. Shift tiles and entities on the active layer.
		if(event->key.keysym.sym >= SDLK_KP0 && event->key.keysym.sym <= SDLK_KP9) {

			int sx = 0;
			int sy = 0;
			
			if(event->key.keysym.sym == SDLK_KP4) {
				sx = -1;
			}
			if(event->key.keysym.sym == SDLK_KP6) {
				sx = 1;
			}
			if(event->key.keysym.sym == SDLK_KP8) {
				sy = 1;
			}
			if(event->key.keysym.sym == SDLK_KP2) {
				sy = -1;
			}
			
            shiftLayer(junkInterface.getActiveLayer(), sx, sy);

            addUndoAction("Shift layer", boost::bind(&EditorUI::shiftLayer, this, junkInterface.getActiveLayer(), -sx, -sy));
		}

        // D key - duplicate the selected entity.
        if(event->key.keysym.sym == SDLK_d) {
            
            for(unsigned int i = 0; i < selection.size(); i++) {

                Entity *src = world->entitiesByName[selection[i]];

                if(src) {

                    TiXmlElement *entXml = src->toXml();

                    string newName = world->getUnusedName().c_str();

                    // Clobber the name so we don't re-use the name of the
                    // one we're duplicating.
                    entXml->SetAttribute("name", newName.c_str());

                    // Make a tiny version of the world with just this
                    // entity so we can add it to the world just like
                    // loading an XML file.
                    TiXmlElement *worldTmpXml = new TiXmlElement("World");
                    worldTmpXml->LinkEndChild(entXml);

                    world->addEntitiesFromXml(worldTmpXml);

                    delete worldTmpXml;

                    world->addNewEntities();

                    // Replace this slot in the selection with the new
                    // entity.
                    selection[i] = newName;
                }
            }

        }

        // Ctrl-Z - undo.
        if(keys[SDLK_LCTRL] && event->key.keysym.sym == SDLK_z) {
            undo();
        }

	}
	
	return false;
}

void EditorUI::shiftLayer(int layer, int x, int y) {

    if(layer < 0 || layer >= (int)world->layers.size()) {
        out("error") << "Tried to shift non-existing layer #" << layer << "." << endl;
        return;
    }

    TilePane *tp = &(world->layers[layer]->tilePane);
    
    if(x != 0 || y != 0) {

        tp->shiftAllTiles(x, y);
				
        for(unsigned int i = 0; i < world->entities.size(); i++) {
            if(world->entities[i]->getLayer() == junkInterface.getActiveLayer()) {
                world->entities[i]->setPos(world->entities[i]->getPos() + (Vector2D(x, y) * tp->getTileSize()));
            }
        }
    }

    tp->shrinkToFit();

}


void EditorUI::update(void) {
	
	Vector2D screenSize = world->getRenderer()->getScreenSize();
	junkInterface.update(screenSize.x, screenSize.y);

	if(!visible) return;
	
	if(junkInterface.attributesChanged()) {
		updateFromInspector();
	}
	
	// Camera scrolling.
	SDL_PumpEvents();
	
	/*
	// TODO: Maybe re-enable keyboard scrolling in the editor?
	
	float camSpeed = 8.0 / world->zoomLevel;
	int axis0 = world->getControlAxisState(0);
	int axis1 = world->getControlAxisState(1);
	
	if(world->getControlButtonState(0)) {
		camSpeed *= 4;
	}
	if(abs(axis0) > 32) {
		world->cameraLoc.x += camSpeed * float(axis0) / 32767.0;
	}
	if(abs(axis1) > 32) {
		world->cameraLoc.y -= camSpeed * float(axis1) / 32767.0;
	}
	*/
	
	// ---------------------------------------------------------------
	//  File save/load/new
	// ---------------------------------------------------------------
	
	// File saving.
	string saveName = junkInterface.getSave();
	if(saveName.size()) {
		
		// Make a new document.
		TiXmlDocument *doc = new TiXmlDocument();
		
		// Serialize everything in the world and link it.
		TiXmlElement *e = world->toXml();
		doc->LinkEndChild(e);
		
		// Make a directory tree to save into.
		FileSystem::makePathForSave(saveName);
		
		// Make a printer and give it the data.
		TiXmlPrinter printer;
		printer.SetIndent("\t");
		doc->Accept(&printer);
		delete doc;
		
		// Write it all out.
		ofstream outFile;
		outFile.open(saveName.c_str()); 
		outFile << printer.CStr();
		outFile.close();
		
		// Save TilePanes...
		// TODO: Remove deprecated gzip saving.
        {
            string tilePaneFile = saveName + ".tiles.gz";
            gzFile out = gzopen(tilePaneFile.c_str(), "wb+");
            if(out) {
                for(int i = 0; i < world->getNumLayers(); i++) {
                    world->layers[i]->tilePane.save(out);
                }
                gzclose(out);
            } else {
                Console::out("error") << "Couldn't open tiles file for writing." << endl;
            }
        }

        // TODO: Remove this crap after I get loading working for RLE2.
        {
            // Save TilePanes... the new (now deprecated) way!
            string tilePaneFile = saveName + ".tiles.rle";
            SimpleBuffer outBuffer;
            for(int i = 0; i < world->getNumLayers(); i++) {
                world->layers[i]->tilePane.saveSimpleBuf(&outBuffer);
            }
        
            // Save ambient lights.
            for(int i = 0; i < world->getNumLayers(); i++) {
                Vector3D ambientColor = world->layers[i]->ambientLight;
                float out;
                out = ambientColor.x; outBuffer.addData(&out, sizeof(out));
                out = ambientColor.y; outBuffer.addData(&out, sizeof(out));
                out = ambientColor.z; outBuffer.addData(&out, sizeof(out));
            }

            // Save retro flag.
            outBuffer.addData(&world->retroEffects, sizeof(world->retroEffects));

            // Write the file.
            FileSystem::saveFile(tilePaneFile, outBuffer.getData(), outBuffer.getLength());
        }

		// Save TilePanes... the newer way! Seriously, just keep the
        // stupid saving code in World from now on. Ugh.
        world->saveTiles(saveName);
	}
	
	// File loading.
	string loadName = junkInterface.getLoad();
	if(loadName.size()) {
		
		selection.clear();
		
		// First clear out anything that's already here.
		world->clearEntities();

		// TODO: Destroy and recreate Lua state here?

        world->loadTiles(loadName);

		world->loadEntities(loadName, true);
		world->addNewEntities();
		
		// Lacking dynamic resizing of the quadtree, let's just do it
		// upon loading the level.
		for(int i = 0; i < world->getNumLayers(); i++) {
			world->getQuadTree(i)->resizeToFitAll();
		}
		
		rebuildTiles();
        updateLightSliders();
		
		selection.clear();
		updateInspector();

        clearUndoActions();

	}

		
	// New file.
	if(junkInterface.getNew()) {
		selection.clear();
		world->clearEntities();

		// FIXME: Do we need to nuke the Lua state for this?
		//world->runInitLua();
		
        world->setupDefaultLayers();
		
		selection.clear();
		updateInspector();

        clearUndoActions();
	}
	
	// ---------------------------------------------------------------
	//  Entity palette
	// ---------------------------------------------------------------
	
	// New entity creation.
	string newEntName = junkInterface.getNewEntity();
	if(newEntName.size()) {
		
		Entity *newEnt = NULL;
		
		// First, we have built-in TerrainBlocks.
		if(newEntName == "Terrain_Triangle") {
			TerrainBlock *tb = new TerrainBlock(world);
			tb->poly.points.push_back(Vector2D(16, 0) / world->zoomLevel);
			tb->poly.points.push_back(Vector2D(-16, 0) / world->zoomLevel);
			tb->poly.points.push_back(Vector2D(0, 32) / world->zoomLevel);
			newEnt = tb;
		} else if(newEntName == "Terrain_Quad") {
			TerrainBlock *tb = new TerrainBlock(world);
			tb->poly.points.push_back(Vector2D(16, 16) / world->zoomLevel);
			tb->poly.points.push_back(Vector2D(-16, 16) / world->zoomLevel);
			tb->poly.points.push_back(Vector2D(-16, -16) / world->zoomLevel);
			tb->poly.points.push_back(Vector2D(16, -16) / world->zoomLevel);
			newEnt = tb;
		} else {
			
			// Check the list of dynamically defined spawns for
			// different things.
			if(world->spawnMap[newEntName] == World::SPAWNTYPE_SCENERY) {
				
				// Scenery chunks.
				out("console") << "Creating scenery \"" << newEntName << "\"" << endl;
				World::SceneryDefinition *def = &(world->sceneryDefs[newEntName]);
				SceneryChunk *chunk = new SceneryChunk(world);
				chunk->setSpriteFrame(def->area);
				chunk->setTexture(def->textureName);
				chunk->setSize(
					Vector2D(
						fabs(def->area.lowerRight.x - def->area.upperLeft.x),
						fabs(def->area.upperLeft.y - def->area.lowerRight.y)));
				
				newEnt = chunk;
				
			} else if(world->spawnMap[newEntName] == World::SPAWNTYPE_SPAWNER) {
				
				// All Lua-defined entities.
				out("console") << "Creating new \"" << newEntName << "\"" << endl;
				
				// Get the class we want to spawn in onto the
				// top of the stack.
				lua_State *L = world->getLuaState();
				lua_getfield(L, LUA_GLOBALSINDEX, newEntName.c_str());
				
				assert(!lua_isnil(L, -1));
				
				// Get the creation function onto the top.
				lua_getfield(L, -1, "create");
				
				assert(!lua_isnil(L, -1));
				
				// Swap those two.
				lua_insert(L, -2);
				
				// Call the create function with the table as self.
				lua_pcall(L, 1, 1, 0);
				
				// Now the newly created object should be on the top.
				ScriptEntity *ent = getEntityFromTable(L, -1);
				
				// And remove it from the stack.
				lua_pop(L, 1);
				
				newEnt = ent;
				
			} else {
				out("error") << "Unknown spawn type for " << newEntName << endl;
			}
		
			// TODO: Navigation nodes (when they get implemented)
		}
		
		if(newEnt) {
			// All this stuff is the same regardless of what was created.
			
			// Set its layer. This will also readjust its position to
			// be on the same location on-screen.
			newEnt->setLayer(junkInterface.getActiveLayer());
			
			// Set its position.
            newEnt->setPos(world->cameraLoc * world->getCamScaleForLayer(junkInterface.getActiveLayer()));

			// Select it.
			selectObject(newEnt->getName());
		}
		
		// Move it from the new entity list right away.
		world->addNewEntities();
	}

	// ---------------------------------------------------------------
	//   Tile stuff.
	// ---------------------------------------------------------------

	// Tile Palette stuff.
	// If we've switched layers, switch the tileset to the new layer's.
	if(lastActiveLayer != junkInterface.getActiveLayer()) {
		lastActiveLayer = junkInterface.getActiveLayer();
		rebuildTiles();

        // Also switch to the new layer's lighting.
        updateLightSliders();
	}
	
	// Tile size changed.
	if(junkInterface.tileSize != world->layers[junkInterface.getActiveLayer()]->tilePane.getTileSize()) {
		world->layers[junkInterface.getActiveLayer()]->tilePane.setTileSize(junkInterface.tileSize);
		rebuildTiles();
	}
	
	// Clear all tiles.
	// FIXME: Is this even hooked up to the UI?
	if(junkInterface.clearTiles) {
		world->layers[junkInterface.getActiveLayer()]->tilePane.clear();
		junkInterface.clearTiles = false;
	}
	
	// Load a new tileset.
	if(junkInterface.newTilesFile.size()) {
		
		//string cleanedFileName = FileSystem::stripDataPath(junkInterface.newTilesFile, string("games/") + world->getGameName() + string("/"));
		string cleanedFileName = world->getRealFile(string("tilesets/") + fixDeprecatedPath(junkInterface.newTilesFile));
		
		// My apologies for this long-ass line.
		world->layers[junkInterface.getActiveLayer()]->tilePane.setTextureNum(
			world->textureManager->getTextureIndexByName(cleanedFileName));
					
		junkInterface.newTilesFile = "";
		rebuildTiles();
	}
	
	// Switching to tile editing mode at all.
	tileDrawing = junkInterface.tilesGroupSelected;

	// ---------------------------------------------------------------
	//   Lua console stuff
	// ---------------------------------------------------------------
	
	lua_State *L = world->getLuaState();
	for(unsigned int i = 0; i < junkInterface.consoleCommands.size(); i++) {
		string command = junkInterface.consoleCommands[i];
		
		junkInterface.appendConsoleOutput(string("] ") + command + "\n");
		
		luaL_loadstring(L, command.c_str());
		
		if(lua_type(L, -1) == LUA_TSTRING) {
			
			// Got a syntax error
			junkInterface.appendConsoleOutput(string(lua_tostring(L, -1)) + "\n");
			lua_pop(L, 1);
			
		} else if(lua_type(L, -1) == LUA_TFUNCTION) {
			
			// Compiled properly (no syntax errors)
			if(lua_pcall(L, 0, LUA_MULTRET, 0)) {
				// Oops. Had an error.
				junkInterface.appendConsoleOutput(string(lua_tostring(L, -1)) + "\n");
				lua_pop(L, 1);
			} else {
				// Function executed. Check the stack for stuff to dump.
				
				while(lua_gettop(L)) {
					ostringstream str;
					
					str << lua_typename(L, lua_type(L, -1)) << ": ";
					str << lua_tostring(L, -1);
					
					junkInterface.appendConsoleOutput(str.str() + "\n");
					
					lua_pop(L, 1);
				}
			}
		}
	}

	// ---------------------------------------------------------------
    //   Lighting stuff
	// ---------------------------------------------------------------

    if(junkInterface.getLightsDirty()) {

        if(junkInterface.getLightLayersLinked()) {

            // Update ALL layers.
            for(int layer = 0; layer < world->getNumLayers(); layer++) {
                world->layers[layer]->ambientLight = junkInterface.getAmbientLights();
            }

        } else {

            // Update just this layer.
            int currentLayer = junkInterface.getActiveLayer();
            world->layers[currentLayer]->ambientLight = junkInterface.getAmbientLights();
        }

        world->lightingEnabled = !junkInterface.getLightsDisabled();
        world->retroEffects = junkInterface.getRetro();
    }
    

	// TODO: Add new Entities here?
	junkInterface.consoleCommands.clear();
}

static void drawArrow(const Vector2D &start, const Vector2D &end, float pointerSize, float scale) {

    if(start == end) return;

    Vector2D invertedNormalized = (end - start);
    invertedNormalized = 
        (invertedNormalized / -invertedNormalized.magnitude()) * pointerSize;

    Vector2D perpendicular = Vector2D(invertedNormalized.y * -1, invertedNormalized.x);
    Vector2D ar1 = invertedNormalized + end + perpendicular;
    Vector2D ar2 = invertedNormalized + end - perpendicular;
    
    Vector2D halfPoint = (start + end) / 2.0;
    Vector2D halfOffset = end - halfPoint;

    // FIXME: Why was this here?
    //glLineWidth(scale * 2);
    glLineWidth(2);

    glBegin(GL_LINES);
 
    glVertex2f(start.x, start.y);  
    glVertex2f(end.x,   end.y);

    glVertex2f(end.x, end.y);
    glVertex2f(ar1.x, ar1.y);

    glVertex2f(end.x, end.y);
    glVertex2f(ar2.x, ar2.y);

    ar1 -= halfOffset;
    ar2 -= halfOffset;
    
    glVertex2f(halfPoint.x, halfPoint.y);
    glVertex2f(ar1.x, ar1.y);

    glVertex2f(halfPoint.x, halfPoint.y);
    glVertex2f(ar2.x, ar2.y);

    glEnd();

    glLineWidth(1);
}

void EditorUI::draw(void) {
	if(!visible) return;

	world->getRenderer()->setColor(1, 1, 0, 1);	
	world->getRenderer()->unbindTexture();

    SDL_PumpEvents();
    Uint8 *keys = SDL_GetKeyState(NULL);
	
	// If we've only selected one object, and it's a terrain block, we
	// need to display editing handles so we can manipulate the
	// vertices.
	if(selection.size() == 1) {
		TerrainBlock *tb = dynamic_cast<TerrainBlock*>(world->entitiesByName[selection[0]]);
		if(tb) {
			world->pushCamMatrixForLayer(tb->getLayer());
			Polygon *poly = &(tb->poly);
			for(unsigned int i = 0; i < poly->points.size(); i++) {
				Vector2D halfBox(4 / world->zoomLevel, 4 / world->zoomLevel);
				BoundingBox bb(poly->points[i] - halfBox + poly->offset, poly->points[i] + halfBox + poly->offset);
				bb.draw(world->getRenderer());
			}
			world->popMatrix();
		}
	}

    // Get mouse location so we can show appropriate highlights on
    // stuff we're about to select or link to.
    float selectionRadius = 2 / world->zoomLevel;
    BoundingBox selectionBox(
        lastMousePos.x - selectionRadius,
        lastMousePos.y + selectionRadius,
        lastMousePos.x + selectionRadius,
        lastMousePos.y - selectionRadius);
    
	// Draw a box around everything on the current layer
	world->pushCamMatrixForLayer(junkInterface.getActiveLayer());
	BoundingBox visibleArea = world->getVisibleArea(junkInterface.getActiveLayer(), true);
	vector<Entity*> entsToDraw;

    world->layers[junkInterface.getActiveLayer()]->quadTree->getEntsTouchingBox(visibleArea, entsToDraw);

	for(unsigned int i = 0; i < entsToDraw.size(); i++) {

        if(!(selection.size() && world->entitiesByName[selection[0]] == entsToDraw[i])) {

            BoundingBox entBox = entsToDraw[i]->getBoundingBox();

            world->getRenderer()->setColor(0.5, 0.5, 0.5, 0.5);

            if(entsToDraw[i]->isLight) {          
                world->getRenderer()->setColor(1, 0.5, 1, 1);
            }

            if(selectionBox.overlap(entsToDraw[i]->getBoundingBox())) {
            
                if(linkObject && linkObject != entsToDraw[i]) {
                    world->getRenderer()->setColor(0, 1, 1, 1);
                }

                glLineWidth(2);
            }

            entBox.draw(world->getRenderer());

            glLineWidth(1);

            // Draw link to targets.
            vector<string> targets;
            entsToDraw[i]->getAllLinkTargets(targets);
            for(unsigned int k = 0; k < targets.size(); k++) {
                Entity *targetedEnt = world->entitiesByName[targets[k]];
                if(targetedEnt) {
                    Vector2D entPos = entsToDraw[i]->getPos();
                    Vector2D linkedEntPos = targetedEnt->getPos();
                    drawArrow(entPos, linkedEntPos, 16, world->getCamScaleForLayer(junkInterface.getActiveLayer()));
                }
            }
        }
	}
	world->popMatrix();

	// Draw the bounding box for every selected object.
	world->getRenderer()->setColor(1, 1, 0, 1);
	for(unsigned int i = 0; i < selection.size(); i++) {
		Entity *e = world->entitiesByName[selection[i]];
		if(e) {
			world->pushCamMatrixForLayer(e->getLayer());
			BoundingBox box = e->getBoundingBox();
			box.draw(world->getRenderer());

            if(e != linkObject || keys[SDLK_LSHIFT]) {

                // Draw line to/from targets.
                vector<string> targets;
                e->getAllLinkTargets(targets);
                for(unsigned int k = 0; k < targets.size(); k++) {
                    Entity *targetedEnt = world->entitiesByName[targets[k]];
                    if(targetedEnt) {
                        Vector2D entPos = e->getPos();
                        Vector2D linkedEntPos = targetedEnt->getPos();
                        drawArrow(entPos, linkedEntPos, 16, world->getCamScaleForLayer(junkInterface.getActiveLayer()));
                    }
                }

            }

			world->popMatrix();
		}
	}
	
	world->pushCamMatrix();
	
	// Draw a grid.
	world->getRenderer()->setColor(1, 1, 1, 0.1);
	
	vector<Vector2D> points;
	
	for(int x = (int(world->cameraLoc.x - (world->screenSize.x / world->zoomLevel) / 2.0) / gridSpacing) * gridSpacing; x < (int(world->cameraLoc.x + (world->screenSize.x / world->zoomLevel) / 2.0) / gridSpacing) * gridSpacing; x += gridSpacing) {
		points.push_back(Vector2D(x, world->cameraLoc.y - (world->screenSize.y / world->zoomLevel) / 2.0));
		points.push_back(Vector2D(x, world->cameraLoc.y + (world->screenSize.y / world->zoomLevel) / 2.0));
	}
	for(int y = (int(world->cameraLoc.y - (world->screenSize.y / world->zoomLevel) / 2.0) / gridSpacing) * gridSpacing; y < (int(world->cameraLoc.y + (world->screenSize.y / world->zoomLevel) / 2.0) / gridSpacing) * gridSpacing; y += gridSpacing) {
		points.push_back(Vector2D(world->cameraLoc.x - (world->screenSize.x / world->zoomLevel) / 2.0, y));
		points.push_back(Vector2D(world->cameraLoc.x + (world->screenSize.x / world->zoomLevel) / 2.0, y));
	}
	
	world->getRenderer()->drawLines(&(points[0]), points.size());
	
	world->getRenderer()->setColor(1, 0, 0, 0.2);
	points.clear();
	points.push_back(Vector2D(0, world->cameraLoc.y - (world->screenSize.y / world->zoomLevel) / 2.0));
	points.push_back(Vector2D(0, world->cameraLoc.y + (world->screenSize.y / world->zoomLevel) / 2.0));
	points.push_back(Vector2D(world->cameraLoc.x - (world->screenSize.x / world->zoomLevel) / 2.0, 0));
	points.push_back(Vector2D(world->cameraLoc.x + (world->screenSize.x / world->zoomLevel) / 2.0, 0));
	world->getRenderer()->drawLines(&(points[0]), points.size());
	
	// Draw a box showing the "actual" screen, regardless of zoom.
	world->getRenderer()->setColor(0.5, 0, 0, 1);
	world->getVisibleArea().draw(world->getRenderer());
	
	world->popMatrix();
	
	world->pushCamMatrixForLayer(junkInterface.getActiveLayer());
	
	// Draw a cursor showing where the next tile will be drawn.
	if(tileDrawing) {

		int tileSize = world->layers[junkInterface.getActiveLayer()]->tilePane.getTileSize();
		
		if(tileSize) {
			
			//float scale = world->getCamScaleForLayer(fltkInterface.getActiveLayer());
			float scale = 1;
			
			int tx = (int(lastMousePos.x * scale) / tileSize) * tileSize;
			int ty = (int(lastMousePos.y * scale) / tileSize) * tileSize;
			
			if(lastMousePos.x < 0) tx -= tileSize;
			if(lastMousePos.y < 0) ty -= tileSize;
			
			world->getRenderer()->setColor(1, 0, 0, 1);
			
			Vector2D points[] = {
				Vector2D(tx, ty),
				Vector2D(tx + tileSize, ty),
				Vector2D(tx + tileSize, ty + tileSize),
				Vector2D(tx, ty + tileSize),
				Vector2D(tx, ty)
			};
			
			world->getRenderer()->drawLineStrip(points, 5);
		}
	}
	
    if(linkObject) {
        
        // Draw an arrow indicating the link.
        Vector2D start = linkObject->getPos();
        Vector2D end = lastMousePos;

        glColor4f(1, 1, 0, 1);
        drawArrow(start, end, 16, world->getCamScaleForLayer(junkInterface.getActiveLayer()));

        // TODO: Highlight the box of the thing about to be linked.
    }

	world->popMatrix();
	
	
	// TODO: Do this for everything in the stack.
	
	Vector2D screenSize = world->getRenderer()->getScreenSize();
	widgetPushTransforms(screenSize.x, screenSize.y);
	/*Widget *w = getRootWidget();
	glClearStencil(0);
	glClear(GL_STENCIL_BUFFER_BIT);
	glMatrixMode(GL_MODELVIEW);
	glEnable(GL_STENCIL_TEST);
	w->draw();
	glDisable(GL_STENCIL_TEST);*/

	for(unsigned int i = 0; i < junkInterface.rootWidgetStack.size(); i++) {
		Widget *w = junkInterface.rootWidgetStack[i];
		glClearStencil(0);
		glClear(GL_STENCIL_BUFFER_BIT);
		glMatrixMode(GL_MODELVIEW);
		glEnable(GL_STENCIL_TEST);
		w->draw();
		glDisable(GL_STENCIL_TEST);
	}
	
	widgetPopTransforms();
	
	// Some stuff the UI might leave dirty...
	glEnable(GL_TEXTURE_2D);
}

void EditorUI::setVisible(bool visible) {

	selection.clear();
	dragObject = NULL;
    linkObject = NULL;
	dragPoint = NULL;

	this->visible = visible;
	
	rebuildTiles();
    updateLightSliders();
	updateInspector();
}

bool EditorUI::getVisible(void) const {
	return visible;
}

void EditorUI::updateInspector(void) {
	
	junkInterface.clearAttributes();
	
	if(selection.size()) {
		Entity *ent = world->entitiesByName[selection[0]];
		if(ent) {
			ent->updateEditor(this);
		}
	}
}

void EditorUI::updateFromInspector(void) {
	if(selection.size()) {
		Entity *ent = world->entitiesByName[selection[0]];
		if(ent) {
			ent->updateFromEditor(this);
		}
	}
}

void EditorUI::selectObject(const std::string &name) {

    // In case anything's been changed that hasn't been recorded
    // yet, make sure that it gets updated in the Entity.

    updateFromInspector();

	selection.clear();
	selection.push_back(name);

	updateInspector();
}

void EditorUI::rebuildTiles(void) {

	//Texture *tex = world->textureManager->getTextureByIndex(world->tilePanes[junkInterface.getActiveLayer()].getTextureNum());

    TilePane *activeLayer = &(world->layers[junkInterface.getActiveLayer()]->tilePane);
    int textureNum = activeLayer->getTextureNum();
    string textureName = world->textureManager->getTextureNameByIndex(textureNum);
	
	junkInterface.rebuildTiles(textureName, activeLayer->getTileSize());
}

Widget *EditorUI::getRootWidget(void) {
	return junkInterface.getTopWidget();
}

void EditorUI::updateLightSliders(void) {
    junkInterface.updateLightSliders(
        world->layers[junkInterface.getActiveLayer()]->ambientLight,
        world->retroEffects,
        !world->lightingEnabled);
}

