public class Menu
{
	// Pixel-Grid toggle
	public static boolean GRID_ENABLED = false;
	// Dark/Light Background toggle
	public static boolean DARK_BACKGROUND = false;
	
	public static WebMenuBar createMenuBar()
	{
		// M.E.I. MenuBar
		WebMenuBar menuBar = new WebMenuBar();
		//menuBar.setMenuBarStyle(MenuBarStyle.standalone);
		
		// Main Menus
		menuBar.add(createFileMenu());
		menuBar.add(createEditMenu());
		menuBar.add(createViewMenu());
		
		// Editing Menus
		menuBar.add((Spade.main.effects.operations = new WebMenu("Image")));
		menuBar.add((Spade.main.tools.toolsMenu = new WebMenu("Tools")));
		menuBar.add((Spade.main.effects.effects = new WebMenu("Effects")));
		menuBar.add((Spade.main.effects.generators = new WebMenu("Generators")));
		
		// Info Menus
		menuBar.add(createWindowMenu());
		menuBar.add(createHelpMenu());
		
		return menuBar;
	}
	
	private static WebMenu createHelpMenu()
	{
		WebMenu help = new WebMenu("Help/Info");
		
		WebMenuItem pluginManager = new WebMenuItem("Plugin Viewer", GUIManager.getIcon("plugin_viewer"));
		pluginManager.addActionListener(new ActionListener()
		{
			@Override
			public void actionPerformed(ActionEvent e)
			{
				PluginManager.instance.showPluginViewer();
			}
		});
		
		WebMenuItem about = new WebMenuItem("About...", GUIManager.getIcon("about"));
		about.addActionListener(new ActionListener()
		{
			@Override
			public void actionPerformed(ActionEvent e)
			{
				Spade.main.gui.about.setVisible(true);
				Spade.main.gui.about.setLocationRelativeTo(Spade.main.gui.frame);
			}
		});
		
		help.add(pluginManager);
		help.add(about);
		
		return help;
	}
	
	private static WebMenu createDialogsMenu()
	{
		WebMenu dialogs = new WebMenu("Manage Dialogs");
		
		WebMenuItem colourChooser = new WebMenuItem("Colour Chooser", GUIManager.getIcon("colour_chooser"));
		colourChooser.setAccelerator(Hotkey.F5);
		
		colourChooser.addActionListener(new ActionListener()
		{
			@Override
			public void actionPerformed(ActionEvent e)
			{
				Spade.main.gui.chooser.toggle();
			}
		});
		
		WebMenuItem layerManager = new WebMenuItem("Layer Manager", GUIManager.getIcon("layer_manager"));
		layerManager.setAccelerator(Hotkey.F6);
		
		layerManager.addActionListener(new ActionListener()
		{
			@Override
			public void actionPerformed(ActionEvent e)
			{
				Spade.main.gui.layers.toggle();
			}
		});
		
		dialogs.add(colourChooser);
		dialogs.add(layerManager);
		
		return dialogs;
	}
	
	private static WebMenu createWindowMenu()
	{
		WebMenu window = new WebMenu("Window");
		
		window.add(createDialogsMenu());
		
		return window;
	}
	
	private static WebMenu createFileMenu()
	{
		WebMenu file = new WebMenu("File");
		
		WebMenuItem newFile = new WebMenuItem("New", GUIManager.getIcon("new"));
		newFile.setAccelerator(Hotkey.CTRL_N);
		WebMenuItem load = new WebMenuItem("Open", GUIManager.getIcon("open"));
		load.setAccelerator(Hotkey.CTRL_O);
		WebMenuItem save = new WebMenuItem("Save", GUIManager.getIcon("save"));
		save.setAccelerator(Hotkey.CTRL_S);
		final WebMenuItem saveAs = new WebMenuItem("Save As", GUIManager.getIcon("save_as"));
		saveAs.setAccelerator(new HotkeyData(true, true, false, KeyEvent.VK_S));
		WebMenuItem exit = new WebMenuItem("Exit", GUIManager.getIcon("exit"));
		
		file.add(newFile);
		file.add(load);
		file.add(new WebSeparator());
		file.add(save);
		file.add(saveAs);
		file.add(new WebSeparator());
		file.add(exit);
		
		newFile.addActionListener(new ActionListener()
		{
			@Override
			public void actionPerformed(ActionEvent e)
			{
				showNewDialog();
			}
		});
		
		load.addActionListener(new ActionListener()
		{
			@Override
			public void actionPerformed(ActionEvent e)
			{
				showOpenMenu();
			}
		});
		
		save.addActionListener(new ActionListener()
		{
			@Override
			public void actionPerformed(ActionEvent e)
			{
				new Thread(new Runnable()
				{
					public void run()
					{
						Spade.save(Spade.getDocument());
					}
				}).start();
			}
		});
		
		saveAs.addActionListener(new ActionListener()
		{
			@Override
			public void actionPerformed(ActionEvent e)
			{
				new Thread(new Runnable()
				{
					public void run()
					{
						Spade.saveAs(Spade.getDocument());
					}
				}).start();
			}
		});
		
		exit.addActionListener(new ActionListener()
		{
			@Override
			public void actionPerformed(ActionEvent e)
			{
				Spade.closeAllDocuments();
			}
		});
		
		return file;
	}
	
	public static void showOpenMenu()
	{
		final WebFileChooser chooser = new WebFileChooser(Spade.getDir());
		chooser.setFileSelectionMode(WebFileChooser.FILES_ONLY);
		chooser.setAcceptAllFileFilterUsed(false);
		chooser.setFileFilter(new ReadableFileFilter());
		
		// Add ALL the custom image-importers!
		ImageImporter.addAllImporters(chooser);
		
		int returned = chooser.showOpenDialog(new WebDialog(Spade.main.gui.frame, "Load Image"));
		
		if(returned == WebFileChooser.APPROVE_OPTION)
		{
			// If a Image takes too long, the application might crash.
			// By running the actual loading process in another thread, the AWT-Event Thread can continue working while the image is being loaded.
			new Thread(new Runnable()
			{
				@Override
				public void run()
				{
					Document doc = Document.loadFromFile(chooser.getSelectedFile());
					if(doc != null)
					{
						Spade.addDocument(doc);
						Spade.setDocument(doc);
					}
				}
			}).start();
		}
	}
	
	public static void showNewDialog()
	{
		final GridEffectDialog dialog = new GridEffectDialog(1, 1, "Create Image", null);
		
		Dimension dim = ClipboardHandler.getImageSize();
		if(dim == null)
		{
			Document current = Spade.getDocument();
			if(current != null)
			{
				dim = new Dimension(current.getWidth(), current.getHeight());
			}
			else
			{
				dim = new Dimension(512, 512);
			}
		}
		
		final JTextField width = new JTextField("" + dim.width);
		final JTextField height = new JTextField("" + dim.height);
		//width.setHorizontalAlignment(JTextField.RIGHT);
		//height.setHorizontalAlignment(JTextField.RIGHT);
		
		{
			JPanel panel = dialog.getPanel(0);
			panel.setLayout(new GridLayout(0, 2));
			
			((AbstractDocument) width.getDocument()).setDocumentFilter(new NumberFilter());
			((AbstractDocument) height.getDocument()).setDocumentFilter(new NumberFilter());
			
			width.setColumns(4);
			height.setColumns(4);
			
			JLabel wl = WeblafWrapper.createLabel("Width: ");
			wl.setHorizontalAlignment(SwingConstants.CENTER);
			
			JLabel hl = WeblafWrapper.createLabel("Height: ");
			hl.setHorizontalAlignment(SwingConstants.CENTER);
			
			panel.add(wl);
			panel.add(width);
			
			panel.add(hl);
			panel.add(height);
		}
		
		JPanel panel = dialog.getBottomPanel();
		panel.setLayout(new GridLayout(0, 2));
		JButton create = WeblafWrapper.createButton("Create");
		JButton cancel = WeblafWrapper.createButton("Cancel");
		
		panel.add(create);
		panel.add(cancel);
		
		create.addActionListener(new ActionListener()
		{
			@Override
			public void actionPerformed(ActionEvent e)
			{
				dialog.close();
				if(width.getText().equals("") || height.getText().equals(""))
					return;
				int w = Integer.parseInt(width.getText());
				int h = Integer.parseInt(height.getText());
				if(w > Document.MAX_DIMENSION || h > Document.MAX_DIMENSION || w == 0 || h == 0)
				{
					Popup.show("Invalid Image Dimensions", "Image must have dimensions higher than 0 and no more than " + Document.MAX_DIMENSION);
				}
				else
				{
					Spade.newImage(w, h);
				}
			}
		});
		
		cancel.addActionListener(new ActionListener()
		{
			@Override
			public void actionPerformed(ActionEvent e)
			{
				dialog.close();
			}
		});
		
		dialog.display();
	}
	
	private static WebMenu createEditMenu()
	{
		WebMenu edit = new WebMenu("Edit");
		
		WebMenuItem undo = new WebMenuItem("Undo", GUIManager.getIcon("undo"));
		undo.setAccelerator(Hotkey.CTRL_Z);
		WebMenuItem redo = new WebMenuItem("Redo", GUIManager.getIcon("redo"));
		redo.setAccelerator(Hotkey.CTRL_Y);
		
		undo.addActionListener(new ActionListener()
		{
			@Override
			public void actionPerformed(ActionEvent arg0)
			{
				if(Spade.getDocument() != null)
					Spade.getDocument().getHistory().revertChange();
			}
		});
		
		redo.addActionListener(new ActionListener()
		{
			@Override
			public void actionPerformed(ActionEvent arg0)
			{
				if(Spade.getDocument() != null)
					Spade.getDocument().getHistory().repeatChange();
			}
		});
		
		edit.add(undo);
		edit.add(redo);
		
		return edit;
	}
	
	private static WebMenu createViewMenu()
	{
		WebMenu view = new WebMenu("View");
		
		WebMenuItem zoomIn = new WebMenuItem("Zoom In", GUIManager.getIcon("zoom_inc"));
		zoomIn.setAccelerator(new HotkeyData(true, false, false, KeyEvent.VK_EQUALS));
		WebMenuItem zoomOut = new WebMenuItem("Zoom Out", GUIManager.getIcon("zoom_dec"));
		zoomOut.setAccelerator(new HotkeyData(true, false, false, KeyEvent.VK_MINUS));
		WebMenuItem zoomReset = new WebMenuItem("Reset Zoom", GUIManager.getIcon("zoom_reset"));
		zoomReset.setAccelerator(new HotkeyData(true, false, false, KeyEvent.VK_0));
		
		WebMenuItem grid = new WebMenuItem("Toggle Grid", GUIManager.getIcon("toggle_grid"));
		grid.setAccelerator(Hotkey.CTRL_G);
		WebMenuItem darkDraw = new WebMenuItem("Toggle Dark Background", GUIManager.getIcon("toggle_dark_bg"));
		darkDraw.setAccelerator(Hotkey.CTRL_B);
		
		zoomIn.addActionListener(new ActionListener()
		{
			@Override
			public void actionPerformed(ActionEvent e)
			{
				float zoom = Spade.main.gui.canvas.getScale();
				if(zoom < 1f)
				{
					Spade.main.gui.canvas.setScale(Math.min(zoom * 2f, 1f));
				}
				else
				{
					Spade.main.gui.canvas.setScale(Math.min(zoom + 2f, 64f));
				}
			}
		});
		
		zoomOut.addActionListener(new ActionListener()
		{
			@Override
			public void actionPerformed(ActionEvent e)
			{
				float zoom = Spade.main.gui.canvas.getScale();
				if(zoom <= 1f)
				{
					Spade.main.gui.canvas.setScale(Math.max(zoom / 2f, 1 / 32f));
				}
				else
				{
					Spade.main.gui.canvas.setScale(Math.max(zoom - 2f, 1f));
				}
			}
		});
		
		zoomReset.addActionListener(new ActionListener()
		{
			@Override
			public void actionPerformed(ActionEvent e)
			{
				Spade.main.gui.canvas.setScale(1f);
			}
		});
		
		grid.addActionListener(new ActionListener()
		{
			@Override
			public void actionPerformed(ActionEvent e)
			{
				GRID_ENABLED = !GRID_ENABLED;
				Spade.main.gui.repaint();
			}
		});
		
		darkDraw.addActionListener(new ActionListener()
		{
			@Override
			public void actionPerformed(ActionEvent e)
			{
				DARK_BACKGROUND = !DARK_BACKGROUND;
				Spade.main.gui.repaint();
			}
		});
		
		view.add(zoomIn);
		view.add(zoomOut);
		view.add(zoomReset);
		view.add(new WebSeparator());
		view.add(grid);
		view.add(darkDraw);
		
		return view;
	}
	
	public static class NumberTextField extends JTextField
	{
		private static final long serialVersionUID = -8289311655265709303L;
		
		public NumberTextField(String text)
		{
			super(text);
			
			setColumns(8);
			((AbstractDocument) getDocument()).setDocumentFilter(new NumberFilter());
		}
		
		public int get()
		{
			if(this.getText().isEmpty())
				return -1;
			
			return Integer.valueOf(this.getText());
		}
	}
} public class Document
{
	public static int MAX_DIMENSION = 4096;
	
	private LinkedList<IDocChange> changes = new LinkedList<IDocChange>();
	private LinkedList<IDocChange> reverted = new LinkedList<IDocChange>();
	
	private int width, height;
	private File file;
	private Metadata info;
	private Layer root, current;
	private History history;
	
	private IChange previewChange;
	public int lowestChange;
	
	private boolean initialised;
	public boolean repaint;
	
	private ArrayList<Layer> flatmap = new ArrayList<Layer>();
	
	private Document()
	{
		this.info = new Metadata();
		this.history = new History(this);
	}
	
	public Document(int width, int height)
	{
		this();
		this.width = width;
		this.height = height;
		
		this.current = this.root = new Layer(this, new RawImage(width, height), new Metadata());
		this.flatmap.clear();
		root.constructFlatMap(flatmap);
		initialised = true;
	}
	
	public static Document loadFromFile(File f)
	{
		Document doc = new Document();
		doc.file = f;
		if(!ImageImporter.loadImage(f.getAbsolutePath(), doc))
			return null;
		doc.flatmap.clear();
		doc.root.constructFlatMap(doc.flatmap);
		doc.initialised = true;
		return doc;
	}
	
	public void reconstructFlatmap()
	{
		if(!initialised)
			return;
		Spade.main.gui.layers.redrawTree();
		this.flatmap.clear();
		root.constructFlatMap(flatmap);
	}
	
	public History getHistory()
	{
		return history;
	}
	
	public Layer getRoot()
	{
		return root;
	}
	
	public Layer getCurrent()
	{
		return current;
	}
	
	public void setRoot(Layer root)
	{
		this.current = this.root = root;
		this.reconstructFlatmap();
	}
	
	public boolean setCurrent(Layer current)
	{
		if(this.current == current)
		{
			return false;
		}
		this.current = current;
		Spade.main.gui.layers.select(current);
		return true;
	}
	
	public Metadata getMetadata()
	{
		return info;
	}
	
	public void resize(int width, int height)
	{
		this.width = width;
		this.height = height;
		this.allChanged();
		Spade.main.gui.canvas.resized(width, height);
	}
	
	public void save()
	{
		save(false);
	}
	
	public void save(boolean panic)
	{
		final String fileName = this.file.getAbsolutePath();
		
		String extension = "";
		
		int i = fileName.lastIndexOf('.');
		
		if(i > 0)
		{
			extension = fileName.substring(i + 1);
		}
		
		final ImageExporter exporter = ImageExporter.get(extension);
		
		final Document doc = this;
		
		try
		{
			exporter.save(doc, new File(fileName));
			history.save();
			Spade.main.gui.checkButtonNames();
		}
		catch(Exception e)
		{
			if(!panic)
				Popup.showException("Error Saving Document", e, "This error occured while saving the file " + file
						+ ". It may work if you try again, but if not, we apologise. Report the bug to get it fixed as soon as possible");
			else
				Popup.showException("Error Saving Document", e, "This error occured while saving the file " + file
						+ " after a crash. Sadly, we cannot recover from this.");
			return;
		}
	}
	
	public String getDir()
	{
		if(file != null)
			return this.file.getParent();
		else
			return System.getProperty("user.dir");
	}
	
	public int getWidth()
	{
		return width;
	}
	
	public int getHeight()
	{
		return height;
	}
	
	public ArrayList<Layer> getFlatMap()
	{
		return flatmap;
	}
	
	/*
	 * This should only be called when loading an image. I need to find another way to do this.
	 */
	public void setDimensions(int width, int height)
	{
		this.width = width;
		this.height = height;
	}
	
	public BufferedImage getRenderedImage()
	{
		BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
		{
			Graphics2D g = image.createGraphics();
			for(Layer l : flatmap)
			{
				l.render(g);
			}
			g.dispose();
		}
		return image;
	}
	
	public void preview(IChange change)
	{
		this.previewChange = change;
		this.repaint();
	}
	
	public void applyPreview()
	{
		this.getCurrent().addChange(previewChange);
		this.previewChange = null;
		this.repaint();
	}
	
	public void changed(Layer layer)
	{
		if(!initialised)
			return;
		Spade.main.gui.checkButtonNames();
		lowestChange = Math.min(lowestChange, flatmap.indexOf(layer));
		this.repaint();
	}
	
	public void allChanged()
	{
		lowestChange = 0;
		this.repaint();
	}
	
	public IChange getPreview()
	{
		return previewChange;
	}
	
	public void setFile(File file)
	{
		this.file = file;
	}
	
	public File getFile()
	{
		return file;
	}
	
	public void addChange(IDocChange change)
	{
		history.addChange(-1);
		changes.push(change);
		change.apply(this);
		this.allChanged();
	}
	
	public void addChangeSilent(IDocChange change)
	{
		changes.push(change);
		change.apply(this);
		this.allChanged();
	}
	
	public void revertChange()
	{
		if(changes.isEmpty())
		{
			return;
		}
		IDocChange change = changes.pop();
		reverted.push(change);
		change.revert(this);
		this.allChanged();
	}
	
	public void repeatChange()
	{
		if(reverted.isEmpty())
		{
			return;
		}
		IDocChange change = reverted.pop();
		changes.push(change);
		change.repeat(this);
		this.allChanged();
	}
	
	public boolean saved()
	{
		return this.file != null && this.history.isSaved();
	}
	
	public void repaint()
	{
		repaint = true;
		Spade.main.gui.repaint();
	}
	
	public void setMetadata(Metadata info)
	{
		this.info = info;
	}
} @SuppressWarnings("serial")
public class Layer extends DefaultMutableTreeNode
{
	private Document doc;
	private FreezeBuffer buffer;
	private Metadata info;
	private BlendMode blend;
	private boolean floating, visible;
	
	public Layer(Document doc, Metadata info)
	{
		this(doc, new RawImage(doc.getWidth(), doc.getHeight()), info);
	}
	
	public Layer(Document doc, RawImage image, Metadata info)
	{
		super(info.getOrSet("name", "New Layer"));
		this.doc = doc;
		this.buffer = new FreezeBuffer(image);
		this.info = info;
		this.blend = BlendMode.getBlendMode(info.getOrSet("blend", "Normal"));
	}
	
	public void updateMetadata()
	{
		BlendMode newMode = BlendMode.getBlendMode(info.get("blend"));
		if(newMode != blend)
		{
			blend = newMode;
			doc.repaint = true;
		}
		String newName = info.get("name");
		if(!newName.equals(this.getUserObject()))
		{
			this.setUserObject(newName);
			Spade.main.gui.layers.redrawTree();
		}
	}
	
	public Layer getParentLayer()
	{
		return (Layer) super.getParent();
	}
	
	public void setVisible(boolean visible)
	{
		this.visible = visible;
	}
	
	public void addLayer(Layer l)
	{
		super.add(l);
		doc.reconstructFlatmap();
		doc.changed(this);
	}
	
	public void addLayer(Layer l, int index)
	{
		super.insert(l, index);
		doc.reconstructFlatmap();
		doc.changed(this);
	}
	
	public int removeLayer(Layer l)
	{
		int index = super.getIndex(l);
		super.remove(l);
		doc.reconstructFlatmap();
		doc.changed(this);
		return index;
	}
	
	public int getWidth()
	{
		return doc.getWidth();
	}
	
	public int getHeight()
	{
		return doc.getHeight();
	}
	
	public Metadata getMetadata()
	{
		return info;
	}
	
	public void render(Graphics2D g)
	{
		if(!visible)
			return;
		g.setComposite(blend);
		g.drawImage(this.buffer.getFront(), 0, 0, null);
	}
	
	public void constructFlatMap(ArrayList<Layer> flatmap)
	{
		flatmap.add(this);
		
		@SuppressWarnings("unchecked")
		Enumeration<Layer> children = this.children();
		while(children.hasMoreElements())
		{
			((Layer) children.nextElement()).constructFlatMap(flatmap);
		}
	}
	
	public void addChange(IChange change)
	{
		doc.getHistory().addChange(doc.getFlatMap().indexOf(this));
		buffer.addChange(change);
		doc.changed(this);
		if(change instanceof IMaskChange)
		{
			Spade.main.gui.canvas.maskChanged();
		}
	}
	
	public Document getDocument()
	{
		return doc;
	}
	
	public void revertChange()
	{
		if(buffer.revertChange() instanceof IMaskChange)
		{
			Spade.main.gui.canvas.maskChanged();
		}
		doc.changed(this);
	}
	
	public void repeatChange()
	{
		if(buffer.repeatChange() instanceof IMaskChange)
		{
			Spade.main.gui.canvas.maskChanged();
		}
		doc.changed(this);
	}
	
	public void addChangeSilent(IChange change)
	{
		buffer.addChange(change);
		doc.changed(this);
		if(change instanceof IMaskChange)
		{
			Spade.main.gui.canvas.maskChanged();
		}
	}
	
	public BlendMode getBlendMode()
	{
		return blend;
	}
	
	public RawImage getImage()
	{
		return buffer.getImage();
	}
	
	public BufferedImage getBufferedImage()
	{
		return buffer.getFront();
	}
	
	public Layer floating()
	{
		this.floating = true;
		return this;
	}
	
	public boolean isFloating()
	{
		return this.floating;
	}
} public final class RawImage
{
	private static int[] TMP;
	
	public static enum MaskMode
	{
		REP, ADD, SUB, XOR, AND;
		
		public boolean transform(boolean bool)
		{
			switch(this)
			{
				case REP:
				case ADD:
					return true;
				case SUB:
					return false;
				case XOR:
					return !bool;
				case AND:
					return bool;
			}
			throw new IllegalStateException("Unreachable");
		}
	}
	
	private int[] buffer;
	public final int width, height;
	
	private boolean[] mask;
	
	public RawImage(int width, int height)
	{
		this.width = width;
		this.height = height;
		this.buffer = new int[width * height];
	}
	
	public RawImage(int width, int height, int[] buffer)
	{
		this.width = width;
		this.height = height;
		if(buffer.length != width * height)
			throw new IllegalArgumentException("Buffer length must be `width*height`");
		this.buffer = buffer;
	}
	
	public RawImage(int width, int height, int[] buffer, boolean[] mask)
	{
		this.width = width;
		this.height = height;
		if(buffer.length != width * height || buffer.length != mask.length)
			throw new IllegalArgumentException("Buffer length must be `width*height`");
		this.buffer = buffer;
		this.mask = mask;
	}
	
	// Returns or allocates a temporary buffer with a size of at least width*height.
	private static int[] get_tmp(int width, int height)
	{
		if(TMP == null || TMP.length < width * height)
		{
			TMP = new int[width * height];
		}
		return TMP;
	}
	
	public void move(int dx, int dy)
	{
		final int offset = dx + dy * width;
		if(Math.abs(dx) >= width || Math.abs(dy) >= height)
		{
			this.fill(0); // Fill, not clear, so the mask is respected.
			return;
		}
		else if(offset == 0)
			return;
		
		int[] tmp = get_tmp(width, height);
		
		int src = 0;
		int dst = offset;
		if(offset < 0)
		{
			src = -offset;
			dst = 0;
		}
		final int len = buffer.length - (src + dst);
		
		// Copy whole buffer to tmp.
		System.arraycopy(buffer, 0, tmp, 0, buffer.length);
		
		// Copy back from tmp
		if(mask == null)
		{
			// Do x-axis bounds clipping.
			if(dx < 0)
			{
				for(int y = 0; y < height; y++)
				{
					Arrays.fill(tmp, y * width, y * width - dx, 0);
				}
			}
			else if(dx > 0)
			{
				for(int y = 1; y <= height; y++)
				{
					Arrays.fill(tmp, y * width - dx, y * width, 0);
				}
			}
			
			// Do the actual copying.
			System.arraycopy(tmp, src, buffer, dst, len);
			
			// Do y-axis bounds clipping.
			Arrays.fill(buffer, 0, dst, 0); // Cut out top.
			Arrays.fill(buffer, buffer.length - src, buffer.length, 0); // Cut out bottom
		}
		else
		{
			this.fill(0); // Clear the old pixels. Only necessary when mask is enabled.
			
			// First translate the mask.
			System.arraycopy(mask, src, mask, dst, len);
			// Then do bounds clipping.
			Arrays.fill(mask, 0, dst, false); // Cut out top.
			Arrays.fill(mask, buffer.length - src, buffer.length, false); // Cut out bottom
			if(dx < 0) // Cut out whatever side.
			{
				for(int y = 1; y <= height; y++)
				{
					Arrays.fill(mask, y * width + dx, y * width, false);
				}
			}
			else if(dx > 0)
			{
				for(int y = 0; y < height; y++)
				{
					Arrays.fill(mask, y * width, y * width + dx, false);
				}
			}
			
			// Now we can copy the data back, if it's not masked out.
			for(int i = 0; i < len; i++)
			{
				if(mask[dst + i])
					buffer[dst + i] = tmp[src + i];
			}
		}
	}
	
	// Drawing functions
	
	public void drawLine(int x1, int y1, final int x2, final int y2, final int c)
	{
		final int dx = Math.abs(x2 - x1);
		final int dy = Math.abs(y2 - y1);
		final int sx = (x1 < x2) ? 1 : -1;
		final int sy = (y1 < y2) ? 1 : -1;
		int err = dx - dy;
		do
		{
			drawPixelChecked(x1, y1, c);
			final int e2 = 2 * err;
			if(e2 > -dy)
			{
				err = err - dy;
				x1 = x1 + sx;
			}
			if(e2 < dx)
			{
				err = err + dx;
				y1 = y1 + sy;
			}
		}
		while(!(x1 == x2 && y1 == y2));
		drawPixelChecked(x2, y2, c);
	}
	
	public void drawRect(int x1, int y1, int x2, int y2, final int c)
	{
		// Do the clamping once at the start so we don't have to perform checks when drawing the pixel.
		x1 = MathUtils.clamp(x1, 0, width - 1);
		x2 = MathUtils.clamp(x2, 0, width - 1);
		y1 = MathUtils.clamp(y1, 0, height - 1);
		y2 = MathUtils.clamp(y2, 0, height - 1);
		
		if(mask == null)
		{
			// top
			final int ix = y1 * width;
			Arrays.fill(buffer, ix + x1, ix + x2 + 1, c);
			
			// bottom
			final int jx = y2 * width;
			Arrays.fill(buffer, jx + x1, jx + x2 + 1, c);
			
			for(int i = y1 + 1; i < y2; i++)
			{
				setPixel(x1, i, c);
				setPixel(x2, i, c);
			}
		}
		else
		{
			// top
			final int ix = y1 * width;
			for(int k = ix + x1; k < ix + x2 + 1; k++)
			{
				if(mask[k])
					buffer[k] = c;
			}
			
			// bottom
			final int jx = y2 * width;
			for(int k = jx + x1; k < jx + x2 + 1; k++)
			{
				if(mask[k])
					buffer[k] = c;
			}
			
			for(int i = y1 + 1; i < y2; i++)
			{
				drawPixel(x1, i, c);
				drawPixel(x2, i, c);
			}
		}
	}
	
	public void fillRect(int x1, int y1, int x2, int y2, int c)
	{
		x1 = MathUtils.clamp(x1, 0, width - 1);
		x2 = MathUtils.clamp(x2, 0, width - 1);
		y1 = MathUtils.clamp(y1, 0, height - 1);
		y2 = MathUtils.clamp(y2, 0, height - 1);
		
		if(mask == null)
		{
			for(; y1 <= y2; y1++)
			{
				final int k = y1 * width;
				Arrays.fill(buffer, k + x1, k + x2 + 1, c);
			}
		}
		else
		{
			for(; y1 <= y2; y1++)
			{
				final int offset = y1 * width;
				for(int k = offset + x1; k < offset + x2 + 1; k++)
				{
					if(mask[k])
						buffer[k] = c;
				}
			}
		}
	}
	
	public void drawPixel(int x, int y, int c)
	{
		final int loc = index(x, y);
		if(mask == null || mask[loc])
		{
			buffer[loc] = c;
		}
	}
	
	public void drawPixelChecked(int x, int y, int c)
	{
		if(x < 0 || y < 0 || x >= width || y >= height)
			return;
		final int loc = index(x, y);
		if(mask == null || mask[loc])
		{
			buffer[loc] = c;
		}
	}
	
	public void fill(int c)
	{
		if(mask == null)
		{
			this.clear(c);
		}
		else
		{
			for(int i = 0; i < buffer.length; i++)
			{
				if(mask[i])
					buffer[i] = c;
			}
		}
	}
	
	// Mask Manipulation
	
	public void fillMask(MaskMode mode)
	{
		if(this.mask == null)
			return;
		switch(mode)
		{
			case REP:
			case ADD:
				Arrays.fill(mask, true);
				break;
			case SUB:
				Arrays.fill(mask, false);
				break;
			case XOR:
				for(int i = 0; i < mask.length; i++)
					mask[i] = !mask[i];
				break;
			case AND:
				break;
		}
	}
	
	public void maskRect(int x1, int y1, int x2, int y2, MaskMode mode)
	{
		x1 = MathUtils.clamp(x1, 0, width - 1);
		x2 = MathUtils.clamp(x2, 0, width - 1);
		y1 = MathUtils.clamp(y1, 0, height - 1);
		y2 = MathUtils.clamp(y2, 0, height - 1);
		
		switch(mode)
		{
			case REP:
				this.fillMask(MaskMode.SUB);
			case ADD:
				for(; y1 <= y2; y1++)
				{
					final int k = y1 * width;
					Arrays.fill(mask, k + x1, k + x2 + 1, true);
				}
				break;
			case SUB:
				for(; y1 <= y2; y1++)
				{
					final int k = y1 * width;
					Arrays.fill(mask, k + x1, k + x2 + 1, false);
				}
				break;
			case XOR:
				for(; y1 <= y2; y1++)
				{
					final int offset = y1 * width;
					for(int k = offset + x1; k < offset + x2 + 1; k++)
					{
						mask[k] = !mask[k];
					}
				}
				break;
			case AND:
				// Before Rectangle
				final int offset = y1 * width + x1;
				Arrays.fill(mask, 0, offset, false);
				
				// After Rectangle
				final int offset2 = y2 * width + x2 + 1;
				Arrays.fill(mask, offset2, mask.length, false);
				
				// Within Rectangle
				final int onstep = x2 - x1 + 1;
				final int offstep = width - onstep;
				
				for(int i = offset + onstep; i < offset2; i += width)
					Arrays.fill(mask, i, i + offstep, false);
				break;
		}
	}
	
	public void setMaskEnabled(boolean enabled)
	{
		if(enabled)
		{
			if(mask == null)
				mask = new boolean[buffer.length];
		}
		else if(mask != null)
		{
			mask = null;
		}
	}
	
	public void toggleMask()
	{
		if(mask == null)
		{
			mask = new boolean[buffer.length];
		}
		else
		{
			mask = null;
		}
	}
	
	public boolean[] copyMask()
	{
		return mask == null ? null : Arrays.copyOf(mask, mask.length);
	}
	
	public boolean[] borrowMask()
	{
		return mask;
	}
	
	public void setMask(boolean[] mask)
	{
		this.mask = mask;
	}
	
	// Buffer Manipulation
	
	public void clear(int c)
	{
		Arrays.fill(buffer, c);
	}
	
	public void setPixel(int x, int y, int c)
	{
		buffer[index(x, y)] = c;
	}
	
	public int getPixel(int x, int y)
	{
		return buffer[index(x, y)];
	}
	
	public int getIndex(int x, int y)
	{
		return index(x, y);
	}
	
	private int index(int x, int y)
	{
		return y * width + x;
	}
	
	public int[] copyBuffer()
	{
		return Arrays.copyOf(buffer, buffer.length);
	}
	
	public int[] borrowBuffer()
	{
		return buffer;
	}
	
	public void setBuffer(int[] buffer)
	{
		this.buffer = buffer;
	}
	
	// Create a RawImage which has direct access to the pixels of the BufferedImage.
	// This could be quite unreliable.
	public static RawImage unwrapBufferedImage(BufferedImage image)
	{
		return new RawImage(image.getWidth(), image.getHeight(), ((DataBufferInt) image.getRaster().getDataBuffer()).getData());
	}
	
	public static RawImage fromBufferedImage(BufferedImage image)
	{
		return new RawImage(image.getWidth(), image.getHeight(), image.getRGB(0, 0, image.getWidth(), image.getHeight(), null, 0, image.getWidth()));
	}
	
	public BufferedImage toBufferedImage()
	{
		BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
		image.setRGB(0, 0, width, height, buffer, 0, width);
		return image;
	}
	
	public static RawImage copyOf(RawImage image)
	{
		if(image.mask == null)
			return new RawImage(image.width, image.height, image.copyBuffer());
		return new RawImage(image.width, image.height, image.copyBuffer(), image.copyMask());
	}
	
	public void copyRegion(RawImage image)
	{
		if(image == this)
			return;
		if(this.buffer.length != image.buffer.length)
			throw new RuntimeException("Cannot copy from a different sized RawImage");
		
		if(this.mask == null)
		{
			System.arraycopy(image.buffer, 0, buffer, 0, buffer.length);
		}
		else
		{
			for(int i = 0; i < buffer.length; i++)
			{
				if(mask[i])
					this.buffer[i] = image.buffer[i];
			}
		}
	}
	
	public void copyFrom(RawImage image, boolean withMask)
	{
		if(image == this)
			return;
		if(this.buffer.length != image.buffer.length)
			throw new RuntimeException("Cannot copy from a different sized RawImage");
		System.arraycopy(image.buffer, 0, buffer, 0, buffer.length);
		if(withMask)
		{
			if(image.mask == null)
				this.mask = null;
			else if(this.mask == null)
				this.mask = Arrays.copyOf(image.mask, image.mask.length);
			else
				System.arraycopy(image.mask, 0, mask, 0, mask.length);
		}
	}
	
	public void copyMaskFrom(RawImage image)
	{
		if(image == this)
			return;
		
		if(image.mask == null)
			this.mask = null;
		else if(this.mask == null)
			this.mask = Arrays.copyOf(image.mask, image.mask.length);
		else if(this.mask.length != image.mask.length)
			throw new RuntimeException("Cannot copy from a different sized RawImage");
		else
			System.arraycopy(image.mask, 0, mask, 0, mask.length);
	}
	
	public void dispose()
	{
		buffer = null;
		mask = null;
	}
	
	public boolean isMaskEnabled()
	{
		return mask != null;
	}
} public abstract class BlendMode implements Composite, CompositeContext
{
	private static HashMap<String, BlendMode> modes = new HashMap<String, BlendMode>();
	public static final BlendMode NORMAL = new Normal();
	
	static
	{
		addBlendMode(NORMAL);
	}
	
	public final String name;
	
	public BlendMode(String name)
	{
		this.name = name;
	}
	
	public abstract void compose(Raster src, Raster dst, WritableRaster out);
	
	public void dispose()
	{
		
	}
	
	public CompositeContext createContext(ColorModel arg0, ColorModel arg1, RenderingHints arg2)
	{
		return this;
	}
	
	public String toString()
	{
		return name;
	}
	
	public static void addBlendMode(BlendMode mode)
	{
		modes.put(mode.name, mode);
	}
	
	public static BlendMode getBlendMode(String s)
	{
		return modes.get(s);
	}
	
	public static Set<String> getBlendModeNames()
	{
		return modes.keySet();
	}
} public interface IChange
{
	public Serialised encode();
} public interface IEditChange extends IChange
{
	public void apply(RawImage image);
} public interface IImageChange extends IMarker, IChange
{
	public RawImage apply(RawImage image);
} public interface IMaskChange extends IChange
{
	
} public class Spade
{
	public static final String REPO_URL = "https://github.com/Spade-Editor/Spade";
	
	// Major.Minor + optional letter for releases. The letter is for tiny revisions, such as fixing bugs that slipped through.
	// Major.Minor-RC.# for Release Candidates (builds that may be promoted to releases).
	// Major.Minor-Beta for beta builds.
	// Major.Minor-Alpha for alpha builds.
	// Major.Minor-Dev for development builds.
	// Eg: 1.3b is the 2nd revision of version 1.3
	
	// Stable for stable post-1.0 versions
	// Beta for new completed features.
	// Development for under-development new features.
	
	public static final String VERSION_STRING = "0.17.0-dev";
	public static final Version VERSION = Version.parse(VERSION_STRING);
	public static final String RELEASED = "13-03-2015";
	
	/* Add/Remove the stars on the following lines to change the build type string.
	//*/public static final String BUILD_TYPE = "Development";
	//*/public static final String BUILD_TYPE = "Alpha"; // I don't know if we'll ever use alphas.
	//*/public static final String BUILD_TYPE = "Beta";
	//*/public static final String BUILD_TYPE = "Release Candidate";
	//*/public static final String BUILD_TYPE = "Stable";
	
	public static boolean debug, localPlugins = true, globalPlugins = true;
	public static Spade main = new Spade();
	public static URL questionMarkURL = Spade.class.getResource("/res/icons/questionmark.png");
	
	public GUIManager gui;
	public PluginManager pluginManager;
	
	private Document document;
	
	public Tool currentTool;
	
	public Tools tools;
	public Effects effects;
	
	public static int leftColour = 0xff000000;
	public static int rightColour = 0xffffffff;
	
	private static HashMap<Character, Tool> toolMap = new HashMap<Character, Tool>();
	
	private static HashMap<Character, Effect> effectMap = new HashMap<Character, Effect>();
	
	public void launch(final ArrayList<File> toOpen) throws InvocationTargetException, InterruptedException
	{
		pluginManager = PluginManager.instance;
		
		HistoryIO.init();
		
		// Order is important. Prefer local (./spade) plugins to global (~/.spade/plugins).
		if(localPlugins)
			pluginManager.addPluginDirectory(new File(IOUtils.assemblePath(IOUtils.jarDir(), "plugins")));
		if(globalPlugins)
			pluginManager.addPluginDirectory(new File(IOUtils.assemblePath(System.getProperty("user.home"), ".spade", "plugins")));
		pluginManager.loadPluginFiles();
		
		tools = new Tools();
		effects = new Effects();
		
		SwingUtilities.invokeAndWait(new Runnable()
		{
			@Override
			public void run()
			{
				// Try to catch everything and recover enough to save.
				try
				{
					gui = new GUIManager();
					gui.init();
					
					setLeftColour(0xff000000, false);
					setRightColour(0xffffffff, false);
					
					tools.init();
					effects.init(); // Doesn't actually do anything
					pluginManager.loadPlugins();
					setTool(currentTool);
					for(File f : toOpen)
					{
						if(debug)
							System.out.println("Opening File " + f.getPath());
						Document d = Document.loadFromFile(f);
						if(d != null)
							Spade.addDocument(d);
					}
					ArrayList<Document> documents = gui.getDocuments();
					if(!documents.isEmpty())
						Spade.setDocument(documents.get(0));
					
					gui.frame.requestFocus();
				}
				catch(Exception e)
				{
					panic(e);
				}
			}
		});
	}
	
	public static void newImage(final int width, final int height)
	{
		Document doc = new Document(width, height);
		Spade.addDocument(doc);
		Spade.setDocument(doc);
	}
	
	public static void addTool(Character key, Tool tool)
	{
		toolMap.put(Character.toLowerCase(key), tool);
	}
	
	public static Tool getTool(Character key)
	{
		return toolMap.get(Character.toLowerCase(key));
	}
	
	public static void addEffect(Character key, Effect op)
	{
		effectMap.put(Character.toLowerCase(key), op);
	}
	
	public static Effect getEffect(Character key)
	{
		return effectMap.get(Character.toLowerCase(key));
	}
	
	public static boolean setTool(Tool tool)
	{
		if(main.currentTool == tool)
			return false;
		Input.CTRL = false;
		Input.ALT = false;
		Input.SHIFT = false;
		main.currentTool = tool;
		main.tools.toolbox.setSelected(tool);
		return true;
	}
	
	public static boolean save(Document doc)
	{
		if(doc == null)
		{
			return false;
		}
		if(doc.getFile() != null)
		{
			doc.save();
			return true;
		}
		else
			return saveAs(doc);
	}
	
	public static boolean saveAs(Document doc)
	{
		if(doc == null)
		{
			return false;
		}
		WebFileChooser chooser = new WebFileChooser(doc.getDir());
		chooser.setFileSelectionMode(WebFileChooser.FILES_ONLY);
		chooser.setAcceptAllFileFilterUsed(false);
		
		ImageExporter.addAllExporters(chooser);
		
		FileFilter allFilter = new FileFilter()
		{
			@Override
			public boolean accept(File f)
			{
				if(f.isDirectory())
					return true;
				String fileName = f.getAbsolutePath();
				int i = fileName.lastIndexOf('.');
				if(i < 0)
					return false;
				
				return ImageExporter.get(fileName.substring(i + 1)) != null;
			}
			
			@Override
			public String getDescription()
			{
				return "All supported import formats";
			}
		};
		chooser.setFileFilter(allFilter);
		
		int returned = chooser.showSaveDialog(new WebDialog(main.gui.frame, "Save Image"));
		
		if(returned == WebFileChooser.APPROVE_OPTION)
		{
			// FIXME: The filechooser doesn't seem to change the filter to the one the user selected.
			// It always seems to fall through to the else branch.
			if(chooser.getFileFilter() instanceof ImageExporter)
			{
				ImageExporter format = (ImageExporter) chooser.getFileFilter();
				File file = chooser.getSelectedFile();
				
				String fileName = file.getAbsolutePath();
				
				if(fileName.endsWith("." + format.getFileExtension()))
				{
					// Do nothing.
					doc.setFile(file);
				}
				else
				{
					// Put the format at the end of the File-Name!
					fileName += "." + format.getFileExtension();
					doc.setFile(new File(fileName));
				}
			}
			else
			{
				File file = chooser.getSelectedFile();
				if(!allFilter.accept(file))
				{
					file = new File(file.getAbsolutePath() + ".png");
				}
				doc.setFile(file);
			}
			
			if(doc == main.document)
				main.gui.setTitle(doc.getFile().getName());
			doc.save();
			return true;
		}
		return false;
	}
	
	public void setLeftColour(int c, boolean checked)
	{
		if(!checked)
		{
			gui.chooser.setColour(c, this, true);
		}
		Spade.leftColour = c;
	}
	
	public int getLeftColour()
	{
		return Spade.leftColour;
	}
	
	public void setRightColour(int c, boolean checked)
	{
		if(!checked)
		{
			gui.chooser.setColour(c, this, false);
		}
		Spade.rightColour = c;
	}
	
	public int getRightColour()
	{
		return Spade.rightColour;
	}
	
	/**
	 * @param mouseButton The mouse-button to get the color for.
	 * @return The color assigned to the given MouseButton.
	 */
	public int getColor(int mouseButton)
	{
		// BUTTON1 (LEFT): left
		// BUTTON2 (MIDDLE): Color.BLACK
		// BUTTON3 (RIGHT): right
		return mouseButton == MouseEvent.BUTTON1 ? Spade.leftColour : (mouseButton == MouseEvent.BUTTON3 ? Spade.rightColour : 0xFF000000);
	}
	
	public static void launchWithPlugins(String[] args, Plugin... plugins)
	{
		for(Plugin p : plugins)
		{
			if(p.getInfo() == null)
			{
				try
				{
					BufferedReader in = new BufferedReader(new InputStreamReader(p.getClass().getResourceAsStream("/plugin.info")));
					p.setInfo(PluginManager.loadPluginInfo(in));
				}
				catch(IOException e)
				{
					e.printStackTrace();
				}
			}
			p.getInfo().set("location", "Class: " + p.getClass().getCanonicalName());
			PluginManager.instance.registerPlugin(p);
		}
		launch(args);
	}
	
	public static void launch(String[] args)
	{
		IOUtils.setMainClass(Spade.class);
		
		ArrayList<File> open = new ArrayList<File>();
		
		System.setProperty("DlafClassName", "");
		
		StringBuilder argsDebug = new StringBuilder();
		
		// Go through ALL the arguments and...
		for(String arg : args)
		{
			argsDebug.append("Picked up argument: " + arg + "\n");
			
			if(arg.equals("-v"))
			{
				// Print version string and exit.
				System.out.println(VERSION);
				System.exit(0);
			}
			else if(arg.equals("--version"))
			{
				// Print detailed version info and exit.
				System.out.println("Spade v" + VERSION);
				System.out.println("Version Released: " + RELEASED);
				System.out.println("Built Type: " + BUILD_TYPE);
				System.exit(0);
			}
			else if(arg.equals("--print-jar-path"))
			{
				// Print the absolute path of the jar and exit.
				System.out.println(IOUtils.jarPath());
				System.exit(0);
			}
			else if(arg.equals("--show-memory-watcher"))
			{
				// ...If the arguments contain the DmemoryWatcherFlag flag, set the property to true to enable the MemoryWatcher.
				System.setProperty("DmemoryWatcherFlag", "true");
			}
			else if(arg.startsWith("--look-and-feel="))
			{
				System.setProperty("DlafClassName", arg.substring(16));
			}
			else if(arg.equals("--debug"))
			{
				debug = true;
			}
			else if(arg.equals("--no-global-plugins"))
			{
				globalPlugins = false;
			}
			else if(arg.equals("--no-local-plugins"))
			{
				localPlugins = false;
			}
			else if(arg.equals("--help"))
			{
				System.out.println("Spade v" + VERSION);
				System.out.println("Version Released: " + RELEASED);
				System.out.println("Built Type: " + BUILD_TYPE);
				System.out.println();
				System.out.println("Copyright 2013-2014 HeroesGrave and Other Spade Developers");
				System.out.println();
				System.out.println("For more info, to report issues, make suggestions, or contribute");
				System.out.println("please visit " + REPO_URL);
				System.out.println();
				System.out.println("Command-Line Options:");
				System.out.println();
				System.out.println("    Information:");
				System.out.println("    --help:                  Print this text and exit");
				System.out.println("    -v                       Print the raw version id and exit");
				System.out.println("    --version                Print more detailed version info and exit");
				System.out.println();
				System.out.println("    Plugin Settings:");
				System.out.println("    --no-local-plugins       Disable Local Plugins (./plugins/)");
				System.out.println("    --no-global-plugins      Disable Global Plugins (~/.spade/.plugins/)");
				System.out.println();
				System.out.println("    UI Settings:");
				System.out.println("    --look-and-feel=[LAF]    Specify the 'Look And Feel' to use");
				System.out.println("    --show-memory-watcher    Enable the memory usage watcher");
				System.out.println();
				System.out.println("    Debug Switches:");
				System.out.println("    --debug                  Enable debugging messages");
				System.out.println("    --debug-timings          Print rendering times");
				System.out.println();
				System.out.println("Any other arguments will be read as files to open on startup.");
				System.exit(0);
			}
			else
			{
				File f = new File(arg);
				
				if(f.exists() && f.isFile() && !f.isHidden())
				{
					open.add(f);
				}
			}
			
			/// XXX: Expand here by adding more debugging options and system flags!
		}
		
		if(debug)
			System.out.println(argsDebug);
		
		// Try to catch all exceptions and recover enough to save
		try
		{
			// Finally Launch Spade!
			main.launch(open);
		}
		catch(Exception e)
		{
			panic(e);
		}
	}
	
	public static Version getVersion()
	{
		return VERSION;
	}
	
	public static void addDocument(Document doc)
	{
		main.gui.addDocument(doc);
	}
	
	public static void setDocument(Document doc)
	{
		main.document = doc;
		main.gui.setDocument(main.document);
	}
	
	public static Document getDocument()
	{
		return main.document;
	}
	
	public static void closeAllDocuments()
	{
		Spade.closeDocument(Spade.getDocument(), new Callback()
		{
			@Override
			public void callback()
			{
				Spade.closeAllDocuments();
			}
		});
	}
	
	public static void closeDocument(Document doc)
	{
		closeDocument(doc, new Callback()
		{
			@Override
			public void callback()
			{
			}
		});
	}
	
	public static void closeDocument(final Document doc, final Callback callback)
	{
		if(main.gui.getDocuments().isEmpty())
		{
			Spade.close();
		}
		final int index = main.gui.getDocuments().indexOf(doc);
		main.gui.tryRemove(doc, new Callback()
		{
			@Override
			public void callback()
			{
				ArrayList<Document> documents = main.gui.getDocuments();
				if(main.gui.getDocuments().isEmpty())
				{
					setDocument(null);
				}
				if(doc == main.document)
				{
					if(index == 0)
						setDocument(documents.get(0));
					else
						setDocument(documents.get(index - 1));
				}
				callback.callback();
			}
		});
	}
	
	public static void close()
	{
		UserPreferences.savePrefs(main.gui.frame, main.gui.chooser, main.gui.layers);
		main.pluginManager.dispose();
		main.gui.frame.dispose();
		System.exit(0);
	}
	
	public static String getDir()
	{
		if(main.document != null)
			return main.document.getDir();
		return System.getProperty("user.dir");
	}
	
	public static void panic(Exception e)
	{
		try {
			ArrayList<Document> documents = main.gui.getDocuments();
			for(Document doc : documents)
			{
				File f = doc.getFile();
				if(f == null)
					continue;
				String name = "~" + f.getName() + ".bck";
				int i = 0;
				while(f.exists())
				{
					f = new File(f.getParentFile(), name + "." + i++);
				}
				doc.setFile(f);
				doc.save(true);
			}
			main.pluginManager.dispose();
			main.gui.frame.dispose();
		}
		catch(Exception e2)
		{
			e2.printStackTrace();
			System.err.println("Exception encountered while handling an exception!\nYou will not go to space today.\nOriginal Error:");
			e.printStackTrace();
			System.exit(-1);
		}
		Popup.showException("Spade has crashed", e, "");
		System.exit(-1);
	}
} public class MathUtils
{
	private static final int INT = 16384;
	private static final double FLOOR = INT + 0.0000;
	private static final double CEIL = INT + 0.9999;
	private static final double ROUND = INT + 0.5000;
	
	public static final int floor(double d)
	{
		return (int) (d + FLOOR) - INT;
	}
	
	public static final int ceil(double d)
	{
		return (int) (d + CEIL) - INT;
	}
	
	public static final int round(double d)
	{
		return (int) (d + ROUND) - INT;
	}
	
	public static final int roundToPowerOf(int i, int powerOf)
	{
		int j = 0;
		while(true)
		{
			j++;
			int k = pow(powerOf, j);
			if(k >= i)
				return k;
		}
	}
	
	public static final int pow(int i, int pow)
	{
		if(pow == 0 || i == 0)
			return 1;
		int r = i;
		if(pow > 0)
		{
			for(int j = 0; j < pow; j++)
			{
				r *= i;
			}
		}
		else
		{
			for(int j = 0; j < -pow; j++)
			{
				r /= i;
			}
		}
		return r;
	}
	
	public static final double clamp(double d, double min, double max)
	{
		if(min > max)
			throw new IllegalArgumentException("min > max");
		if(d >= max)
			return max;
		if(d <= min)
			return min;
		return d;
	}
	
	public static final int clamp(int i, int min, int max)
	{
		if(min > max)
			throw new IllegalArgumentException("min > max");
		if(i >= max)
			return max;
		if(i <= min)
			return min;
		return i;
	}
	
	public static final double round(double d, int dp)
	{
		double mult = Math.pow(10, dp);
		d *= mult;
		int i = MathUtils.round(d);
		return i / mult;
	}
	
	public static final int divideAndFloor(double a, double b)
	{
		return floor(a / b);
	}
	
	public static float interp(float x, float y)
	{
		return (x - y) / 2F + y;
	}
	
	public static float difference(float x, float y)
	{
		return Math.abs(x - y);
	}
	
	public static int sign(int i)
	{
		if(i > 0)
			return 1;
		else if(i < 0)
			return -1;
		else
			return 0;
	}
}
import heroesgrave.spade.gui.menus.Menu;
import heroesgrave.spade.image.Document;
import heroesgrave.spade.image.Layer;
import heroesgrave.spade.image.RawImage;
import heroesgrave.spade.image.blend.BlendMode;
import heroesgrave.spade.image.change.IChange;
import heroesgrave.spade.image.change.IEditChange;
import heroesgrave.spade.image.change.IImageChange;
import heroesgrave.spade.image.change.IMaskChange;
import heroesgrave.spade.main.Spade;
import heroesgrave.utils.math.MathUtils;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.TexturePaint;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.ArrayList;
import javax.imageio.ImageIO;
import javax.swing.JComponent;
import javax.swing.JFrame;
// {LICENSE}
/*
 * Copyright 2013-2015 HeroesGrave and other Spade developers.
 * 
 * This file is part of Spade
 * 
 * Spade 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 3 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, see <http://www.gnu.org/licenses/>
 */

package heroesgrave.spade.gui;




/**
 * Handles:
 * - Handling of the Camera
 * - Drawing of any given Image.
 **/
@SuppressWarnings("serial")
public class PaintCanvas extends JComponent implements MouseListener, MouseMotionListener, MouseWheelListener
{
	public static final Color TRANSPARENT = new Color(255, 255, 255, 0);
	public static final int SELECTION_OVERLAY = 0x3f3f5f7f;
	public static BufferedImage backgroundLight, backgroundDark;
	
	// MISC
	final JFrame mainframe;
	BufferedImage image;
	
	// Camera
	float cam_zoom;
	float cam_positionX;
	float cam_positionY;
	
	// Controls
	int mouseX, mouseY, mouseLastDragPosX, mouseLastDragPosY;
	int lastButton;
	
	// Paint
	Rectangle2D backgroundRectangle;
	TexturePaint paintLight, paintDark;
	
	// Document rendering stuff 
	private BufferedImage frozen, unselected, preview, cachedTBG;
	private RawImage unselectedRaw, previewRaw;
	private boolean maskChanged;
	private int frozenTo;
	
	// Document
	Document document;