// This file is part of MicropolisJ.
// Copyright (C) 2013 Jason Long
// Portions Copyright (C) 1989-2007 Electronic Arts Inc.
//
// MicropolisJ is free software; you can redistribute it and/or modify
// it under the terms of the GNU GPLv3, with additional terms.
// See the README file, included in this distribution, for details.

package micropolisj.gui;

import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.net.URL;
import java.text.MessageFormat;
import java.text.NumberFormat;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.prefs.*;
import javax.sound.sampled.*;
import javax.swing.*;
import javax.swing.Timer;
import javax.swing.filechooser.FileNameExtensionFilter;

import micropolisj.engine.*;
import micropolisj.util.TranslationTool;

public class MainWindow extends JFrame
	implements Micropolis.Listener, EarthquakeListener
{
	Micropolis engine;
	MicropolisDrawingArea drawingArea;
	JScrollPane drawingAreaScroll;
	DemandIndicator demandInd;
	MessagesPane messagesPane;
	JLabel mapLegendLbl;
	OverlayMapView mapView;
	NotificationPane notificationPane;
	EvaluationPane evaluationPane;
	GraphsPane graphsPane;
	JLabel dateLbl;
	JLabel fundsLbl;
	JLabel popLbl;
	JLabel currentToolLbl;
	JLabel currentToolCostLbl;
	Map<MicropolisTool,JToggleButton> toolBtns;
	EnumMap<MapState,JMenuItem> mapStateMenuItems = new EnumMap<MapState,JMenuItem>(MapState.class);
	MicropolisTool currentTool;
	File currentFile;
	boolean doSounds = true;
	boolean dirty1 = false;  //indicates if a tool was successfully applied since last save
	boolean dirty2 = false;  //indicates if simulator took a step since last save
	long lastSavedTime = 0;  //real-time clock of when file was last saved
	boolean autoBudgetPending;

	static ImageIcon appIcon;
	static {
		appIcon = new ImageIcon(MainWindow.class.getResource("/micropolism.png"));
	}

	static ResourceBundle strings = ResourceBundle.getBundle("micropolisj.GuiStrings");
	static final String PRODUCT_NAME = strings.getString("PRODUCT");

	public MainWindow()
	{
		this(new Micropolis());
	}

	public MainWindow(Micropolis engine)
	{
		setIconImage(appIcon.getImage());

		this.engine = engine;

		JPanel mainArea = new JPanel(new BorderLayout());
		add(mainArea, BorderLayout.CENTER);

		drawingArea = new MicropolisDrawingArea(engine);
		drawingAreaScroll = new JScrollPane(drawingArea);
		mainArea.add(drawingAreaScroll);

		makeMenu();
		JToolBar tb = makeToolbar();
		mainArea.add(tb, BorderLayout.WEST);

		Box evalGraphsBox = new Box(BoxLayout.Y_AXIS);
		mainArea.add(evalGraphsBox, BorderLayout.SOUTH);

		graphsPane = new GraphsPane(engine);
		graphsPane.setVisible(false);
		evalGraphsBox.add(graphsPane);

		evaluationPane = new EvaluationPane(engine);
		evaluationPane.setVisible(false);
		evalGraphsBox.add(evaluationPane, BorderLayout.SOUTH);

		JPanel leftPane = new JPanel(new GridBagLayout());
		add(leftPane, BorderLayout.WEST);

		GridBagConstraints c = new GridBagConstraints();
		c.gridx = c.gridy = 0;
		c.anchor = GridBagConstraints.SOUTHWEST;
		c.insets = new Insets(4,4,4,4);
		c.weightx = 1.0;

		demandInd = new DemandIndicator();
		leftPane.add(demandInd, c);

		c.gridx = 1;
		c.weightx = 0.0;
		c.fill = GridBagConstraints.BOTH;
		c.insets = new Insets(4, 20, 4, 4);

		leftPane.add(makeDateFunds(), c);

		c.gridx = 0;
		c.gridy = 1;
		c.gridwidth = 2;
		c.weighty = 0.0;
		c.anchor = GridBagConstraints.NORTH;
		c.insets = new Insets(0,0,0,0);

		JPanel mapViewContainer = new JPanel(new BorderLayout());
		mapViewContainer.setBorder(BorderFactory.createLineBorder(Color.BLACK));
		leftPane.add(mapViewContainer, c);

		JMenuBar mapMenu = new JMenuBar();
		mapViewContainer.add(mapMenu, BorderLayout.NORTH);

		JMenu zonesMenu = new JMenu(strings.getString("menu.zones"));
		mapMenu.add(zonesMenu);

		zonesMenu.add(makeMapStateMenuItem(strings.getString("menu.zones.ALL"), MapState.ALL));
		zonesMenu.add(makeMapStateMenuItem(strings.getString("menu.zones.RESIDENTIAL"), MapState.RESIDENTIAL));
		zonesMenu.add(makeMapStateMenuItem(strings.getString("menu.zones.COMMERCIAL"), MapState.COMMERCIAL));
		zonesMenu.add(makeMapStateMenuItem(strings.getString("menu.zones.INDUSTRIAL"), MapState.INDUSTRIAL));
		zonesMenu.add(makeMapStateMenuItem(strings.getString("menu.zones.TRANSPORT"), MapState.TRANSPORT));

		JMenu overlaysMenu = new JMenu(strings.getString("menu.overlays"));
		mapMenu.add(overlaysMenu);

		overlaysMenu.add(makeMapStateMenuItem(strings.getString("menu.overlays.POPDEN_OVERLAY"), MapState.POPDEN_OVERLAY));
		overlaysMenu.add(makeMapStateMenuItem(strings.getString("menu.overlays.GROWTHRATE_OVERLAY"), MapState.GROWTHRATE_OVERLAY));
		overlaysMenu.add(makeMapStateMenuItem(strings.getString("menu.overlays.LANDVALUE_OVERLAY"), MapState.LANDVALUE_OVERLAY));
		overlaysMenu.add(makeMapStateMenuItem(strings.getString("menu.overlays.CRIME_OVERLAY"), MapState.CRIME_OVERLAY));
		overlaysMenu.add(makeMapStateMenuItem(strings.getString("menu.overlays.POLLUTE_OVERLAY"), MapState.POLLUTE_OVERLAY));
		overlaysMenu.add(makeMapStateMenuItem(strings.getString("menu.overlays.TRAFFIC_OVERLAY"), MapState.TRAFFIC_OVERLAY));
		overlaysMenu.add(makeMapStateMenuItem(strings.getString("menu.overlays.POWER_OVERLAY"), MapState.POWER_OVERLAY));
		overlaysMenu.add(makeMapStateMenuItem(strings.getString("menu.overlays.FIRE_OVERLAY"), MapState.FIRE_OVERLAY));
		overlaysMenu.add(makeMapStateMenuItem(strings.getString("menu.overlays.POLICE_OVERLAY"), MapState.POLICE_OVERLAY));

		mapMenu.add(Box.createHorizontalGlue());
		mapLegendLbl = new JLabel();
		mapMenu.add(mapLegendLbl);

		mapView = new OverlayMapView(engine);
		mapView.connectView(drawingArea, drawingAreaScroll);
		mapViewContainer.add(mapView, BorderLayout.CENTER);

		setMapState(MapState.ALL);

		c.gridx = 0;
		c.gridy = 2;
		c.gridwidth = 2;
		c.weighty = 1.0;
		c.fill = GridBagConstraints.BOTH;
		c.insets = new Insets(0,0,0,0);

		messagesPane = new MessagesPane();
		JScrollPane scroll2 = new JScrollPane(messagesPane);
		scroll2.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
		scroll2.setPreferredSize(new Dimension(0,0));
		scroll2.setMinimumSize(new Dimension(0,0));
		leftPane.add(scroll2, c);

		c.gridy = 3;
		c.weighty = 0.0;
		notificationPane = new NotificationPane(engine);
		leftPane.add(notificationPane, c);

		pack();
		setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
		setLocationRelativeTo(null);

		MouseAdapter mouse = new MouseAdapter() {
			public void mousePressed(MouseEvent ev)
			{
				try {
					onToolDown(ev);
				} catch (Throwable e) {
					showErrorMessage(e);
				}
			}
			public void mouseReleased(MouseEvent ev)
			{
				try {
					onToolUp(ev);
				} catch (Throwable e) {
					showErrorMessage(e);
				}
			}
			public void mouseDragged(MouseEvent ev)
			{
				try {
					onToolDrag(ev);
				} catch (Throwable e) {
					showErrorMessage(e);
				}
			}
			public void mouseMoved(MouseEvent ev)
			{
				try {
					onToolHover(ev);
				} catch (Throwable e) {
					showErrorMessage(e);
				}
			}
			public void mouseExited(MouseEvent ev)
			{
				try {
					onToolExited(ev);
				} catch (Throwable e) {
					showErrorMessage(e);
				}
			}
			};
		drawingArea.addMouseListener(mouse);
		drawingArea.addMouseMotionListener(mouse);

		addWindowListener(new WindowAdapter() {
			public void windowClosing(WindowEvent ev)
			{
				closeWindow();
			}
			public void windowClosed(WindowEvent ev)
			{
				onWindowClosed(ev);
			}
			});

		Preferences prefs = Preferences.userNodeForPackage(MainWindow.class);
		doSounds = prefs.getBoolean(SOUNDS_PREF, true);

		// start things up
		mapView.setEngine(engine);
		engine.addListener(this);
		engine.addEarthquakeListener(this);
		reloadFunds();
		reloadOptions();
		startTimer();
		makeClean();
	}

	public void setEngine(Micropolis newEngine)
	{
		if (engine != null) { // old engine
			engine.removeListener(this);
			engine.removeEarthquakeListener(this);
		}

		engine = newEngine;

		if (engine != null) { // new engine
			engine.addListener(this);
			engine.addEarthquakeListener(this);
		}

		boolean timerEnabled = isTimerActive();
		if (timerEnabled) {
			stopTimer();
		}
		stopEarthquake();

		drawingArea.setEngine(engine);
		mapView.setEngine(engine);   //must change mapView after drawingArea
		evaluationPane.setEngine(engine);
		demandInd.setEngine(engine);
		graphsPane.setEngine(engine);
		reloadFunds();
		reloadOptions();
		notificationPane.setVisible(false);

		if (timerEnabled) {
			startTimer();
		}
	}

	boolean needsSaved()
	{
		if (dirty1)    //player has built something since last save
			return true;

		if (!dirty2)   //no simulator ticks since last save
			return false;

		// simulation time has passed since last save, but the player
		// hasn't done anything. Whether we need to prompt for save
		// will depend on how much real time has elapsed.
		// The threshold is 30 seconds.

		return (System.currentTimeMillis() - lastSavedTime > 30000);
	}

	boolean maybeSaveCity()
	{
		if (needsSaved())
		{
			try {
			stopTimer();

			int rv = JOptionPane.showConfirmDialog(
				this,
				strings.getString("main.save_query"),
				PRODUCT_NAME,
				JOptionPane.YES_NO_CANCEL_OPTION,
				JOptionPane.WARNING_MESSAGE);
			if (rv == JOptionPane.CANCEL_OPTION)
				return false;

			if (rv == JOptionPane.YES_OPTION) {
				if (!onSaveCityClicked()) {
					// canceled save dialog
					return false;
				}
			}
			}
			finally {
				startTimer();
			}
		}
		return true;
	}

	void closeWindow()
	{
		if (maybeSaveCity()) {
			dispose();
		}
	}

	JComponent makeDateFunds()
	{
		JPanel pane = new JPanel(new GridBagLayout());
		GridBagConstraints c0 = new GridBagConstraints();
		GridBagConstraints c1 = new GridBagConstraints();

		c0.gridx = 0;
		c1.gridx = 1;
		c0.gridy = c1.gridy = 0;
		c0.weightx = 1.0;
		c0.weighty = c1.weighty = 1.0;
		c0.anchor = GridBagConstraints.WEST;
		c1.anchor = GridBagConstraints.EAST;

		pane.add(new JLabel(strings.getString("main.date_label")), c0);
		dateLbl = new JLabel();
		pane.add(dateLbl, c1);

		c0.gridy = c1.gridy = 1;

		pane.add(new JLabel(strings.getString("main.funds_label")), c0);
		fundsLbl = new JLabel();
		pane.add(fundsLbl, c1);

		c0.gridy = c1.gridy = 2;

		pane.add(new JLabel(strings.getString("main.population_label")), c0);
		popLbl = new JLabel();
		pane.add(popLbl, c1);

		return pane;
	}

	private void makeMenu()
	{
		JMenuBar menuBar = new JMenuBar();

		JMenu gameMenu = new JMenu(strings.getString("menu.game"));
		menuBar.add(gameMenu);

		JMenuItem menuItem;
		menuItem = new JMenuItem(strings.getString("menu.game.new"));
		menuItem.addActionListener(wrapActionListener(
			new ActionListener() {
			public void actionPerformed(ActionEvent ev)
			{
				onNewCityClicked();
			}
			}));
		gameMenu.add(menuItem);

		menuItem = new JMenuItem(strings.getString("menu.game.load"));
		menuItem.addActionListener(wrapActionListener(
			new ActionListener() {
			public void actionPerformed(ActionEvent ev)
			{
				onLoadGameClicked();
			}
			}));
		gameMenu.add(menuItem);

		menuItem = new JMenuItem(strings.getString("menu.game.save"));
		menuItem.addActionListener(wrapActionListener(
			new ActionListener() {
			public void actionPerformed(ActionEvent ev)
			{
				onSaveCityClicked();
			}
			}));
		gameMenu.add(menuItem);

		menuItem = new JMenuItem(strings.getString("menu.game.save_as"));
		menuItem.addActionListener(wrapActionListener(
			new ActionListener() {
			public void actionPerformed(ActionEvent ev)
			{
				onSaveCityAsClicked();
			}
			}));
		gameMenu.add(menuItem);

		menuItem = new JMenuItem(strings.getString("menu.game.exit"));
		menuItem.addActionListener(wrapActionListener(
			new ActionListener() {
			public void actionPerformed(ActionEvent ev)
			{
				closeWindow();
			}
			}));
		gameMenu.add(menuItem);

		JMenu optionsMenu = new JMenu(strings.getString("menu.options"));
		menuBar.add(optionsMenu);

		JMenu levelMenu = new JMenu(strings.getString("menu.difficulty"));
		optionsMenu.add(levelMenu);

		difficultyMenuItems = new HashMap<Integer,JMenuItem>();
		for (int i = GameLevel.MIN_LEVEL; i <= GameLevel.MAX_LEVEL; i++)
		{
			final int level = i;
			menuItem = new JRadioButtonMenuItem(strings.getString("menu.difficulty."+level));
			menuItem.addActionListener(wrapActionListener(
				new ActionListener() {
				public void actionPerformed(ActionEvent evt) {
					onDifficultyClicked(level);
				}
			}));
			levelMenu.add(menuItem);
			difficultyMenuItems.put(level, menuItem);
		}

		autoBudgetMenuItem = new JCheckBoxMenuItem(strings.getString("menu.options.auto_budget"));
		autoBudgetMenuItem.addActionListener(wrapActionListener(
			new ActionListener() {
			public void actionPerformed(ActionEvent ev)
			{
				onAutoBudgetClicked();
			}
			}));
		optionsMenu.add(autoBudgetMenuItem);

		autoBulldozeMenuItem = new JCheckBoxMenuItem(strings.getString("menu.options.auto_bulldoze"));
		autoBulldozeMenuItem.addActionListener(wrapActionListener(
			new ActionListener() {
			public void actionPerformed(ActionEvent ev)
			{
				onAutoBulldozeClicked();
			}
			}));
		optionsMenu.add(autoBulldozeMenuItem);

		disastersMenuItem = new JCheckBoxMenuItem(strings.getString("menu.options.disasters"));
		disastersMenuItem.addActionListener(wrapActionListener(
			new ActionListener() {
			public void actionPerformed(ActionEvent ev)
			{
				onDisastersClicked();
			}
			}));
		optionsMenu.add(disastersMenuItem);

		soundsMenuItem = new JCheckBoxMenuItem(strings.getString("menu.options.sound"));
		soundsMenuItem.addActionListener(wrapActionListener(
			new ActionListener() {
			public void actionPerformed(ActionEvent ev)
			{
				onSoundClicked();
			}
			}));
		optionsMenu.add(soundsMenuItem);

		JMenu disastersMenu = new JMenu(strings.getString("menu.disasters"));
		menuBar.add(disastersMenu);

		menuItem = new JMenuItem(strings.getString("menu.disasters.MONSTER"));
		menuItem.addActionListener(wrapActionListener(
			new ActionListener() {
			public void actionPerformed(ActionEvent ev)
			{
				onInvokeDisasterClicked(Disaster.MONSTER);
			}
			}));
		disastersMenu.add(menuItem);

		menuItem = new JMenuItem(strings.getString("menu.disasters.FIRE"));
		menuItem.addActionListener(wrapActionListener(
			new ActionListener() {
			public void actionPerformed(ActionEvent ev)
			{
				onInvokeDisasterClicked(Disaster.FIRE);
			}
			}));
		disastersMenu.add(menuItem);

		menuItem = new JMenuItem(strings.getString("menu.disasters.FLOOD"));
		menuItem.addActionListener(wrapActionListener(
			new ActionListener() {
			public void actionPerformed(ActionEvent ev)
			{
				onInvokeDisasterClicked(Disaster.FLOOD);
			}
			}));
		disastersMenu.add(menuItem);

		menuItem = new JMenuItem(strings.getString("menu.disasters.MELTDOWN"));
		menuItem.addActionListener(wrapActionListener(
			new ActionListener() {
			public void actionPerformed(ActionEvent ev)
			{
				onInvokeDisasterClicked(Disaster.MELTDOWN);
			}
			}));
		disastersMenu.add(menuItem);

		menuItem = new JMenuItem(strings.getString("menu.disasters.TORNADO"));
		menuItem.addActionListener(wrapActionListener(
			new ActionListener() {
			public void actionPerformed(ActionEvent ev)
			{
				onInvokeDisasterClicked(Disaster.TORNADO);
			}
			}));
		disastersMenu.add(menuItem);

		menuItem = new JMenuItem(strings.getString("menu.disasters.EARTHQUAKE"));
		menuItem.addActionListener(wrapActionListener(
			new ActionListener() {
			public void actionPerformed(ActionEvent ev)
			{
				onInvokeDisasterClicked(Disaster.EARTHQUAKE);
			}
			}));
		disastersMenu.add(menuItem);

		JMenu priorityMenu = new JMenu(strings.getString("menu.speed"));
		menuBar.add(priorityMenu);

		priorityMenuItems = new EnumMap<Speed,JMenuItem>(Speed.class);
		menuItem = new JRadioButtonMenuItem(strings.getString("menu.speed.SUPER_FAST"));
		menuItem.addActionListener(wrapActionListener(
			new ActionListener() {
			public void actionPerformed(ActionEvent ev)
			{
				onPriorityClicked(Speed.SUPER_FAST);
			}
			}));
		priorityMenu.add(menuItem);
		priorityMenuItems.put(Speed.SUPER_FAST, menuItem);

		menuItem = new JRadioButtonMenuItem(strings.getString("menu.speed.FAST"));
		menuItem.addActionListener(wrapActionListener(
			new ActionListener() {
			public void actionPerformed(ActionEvent ev)
			{
				onPriorityClicked(Speed.FAST);
			}
			}));
		priorityMenu.add(menuItem);
		priorityMenuItems.put(Speed.FAST, menuItem);

		menuItem = new JRadioButtonMenuItem(strings.getString("menu.speed.NORMAL"));
		menuItem.addActionListener(wrapActionListener(
			new ActionListener() {
			public void actionPerformed(ActionEvent ev)
			{
				onPriorityClicked(Speed.NORMAL);
			}
			}));
		priorityMenu.add(menuItem);
		priorityMenuItems.put(Speed.NORMAL, menuItem);

		menuItem = new JRadioButtonMenuItem(strings.getString("menu.speed.SLOW"));
		menuItem.addActionListener(wrapActionListener(
			new ActionListener() {
			public void actionPerformed(ActionEvent ev)
			{
				onPriorityClicked(Speed.SLOW);
			}
			}));
		priorityMenu.add(menuItem);
		priorityMenuItems.put(Speed.SLOW, menuItem);

		menuItem = new JRadioButtonMenuItem(strings.getString("menu.speed.PAUSED"));
		menuItem.addActionListener(wrapActionListener(
			new ActionListener() {
			public void actionPerformed(ActionEvent ev)
			{
				onPriorityClicked(Speed.PAUSED);
			}
			}));
		priorityMenu.add(menuItem);
		priorityMenuItems.put(Speed.PAUSED, menuItem);

		JMenu windowsMenu = new JMenu(strings.getString("menu.windows"));
		menuBar.add(windowsMenu);

		menuItem = new JMenuItem(strings.getString("menu.windows.budget"));
		menuItem.addActionListener(wrapActionListener(
			new ActionListener() {
			public void actionPerformed(ActionEvent ev)
			{
				onViewBudgetClicked();
			}
			}));
		windowsMenu.add(menuItem);

		menuItem = new JMenuItem(strings.getString("menu.windows.evaluation"));
		menuItem.addActionListener(wrapActionListener(
			new ActionListener() {
			public void actionPerformed(ActionEvent ev)
			{
				onViewEvaluationClicked();
			}
			}));
		windowsMenu.add(menuItem);

		menuItem = new JMenuItem(strings.getString("menu.windows.graph"));
		menuItem.addActionListener(wrapActionListener(
			new ActionListener() {
			public void actionPerformed(ActionEvent ev)
			{
				onViewGraphClicked();
			}
			}));
		windowsMenu.add(menuItem);

		JMenu helpMenu = new JMenu(strings.getString("menu.help"));
		menuBar.add(helpMenu);

		menuItem = new JMenuItem(strings.getString("menu.help.launch-translation-tool"));
		menuItem.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent evt)
			{
				onLaunchTranslationToolClicked();
			}});
		helpMenu.add(menuItem);

		menuItem = new JMenuItem(strings.getString("menu.help.about"));
		menuItem.addActionListener(wrapActionListener(
			new ActionListener() {
			public void actionPerformed(ActionEvent ev)
			{
				onAboutClicked();
			}
			}));
		helpMenu.add(menuItem);

		setJMenuBar(menuBar);
	}

	private Micropolis getEngine()
	{
		return engine;
	}

	JMenuItem autoBudgetMenuItem;
	JMenuItem autoBulldozeMenuItem;
	JMenuItem disastersMenuItem;
	JMenuItem soundsMenuItem;
	Map<Speed,JMenuItem> priorityMenuItems;
	Map<Integer,JMenuItem> difficultyMenuItems;

	private void onAutoBudgetClicked()
	{
		dirty1 = true;
		getEngine().toggleAutoBudget();
	}

	private void onAutoBulldozeClicked()
	{
		dirty1 = true;
		getEngine().toggleAutoBulldoze();
	}

	private void onDisastersClicked()
	{
		dirty1 = true;
		getEngine().toggleDisasters();
	}

	static final String SOUNDS_PREF = "enable_sounds";
	private void onSoundClicked()
	{
		doSounds = !doSounds;
		Preferences prefs = Preferences.userNodeForPackage(MainWindow.class);
		prefs.putBoolean(SOUNDS_PREF, doSounds);
		reloadOptions();
	}

	void makeClean()
	{
		dirty1 = false;
		dirty2 = false;
		lastSavedTime = System.currentTimeMillis();
		if (currentFile != null) {
			String fileName = currentFile.getName();
			if (fileName.endsWith("."+EXTENSION)) {
				fileName = fileName.substring(0, fileName.length() - 1 - EXTENSION.length());
			}
			setTitle(MessageFormat.format(strings.getString("main.caption_named_city"), fileName));
		}
		else {
			setTitle(strings.getString("main.caption_unnamed_city"));
		}
	}

	private boolean onSaveCityClicked()
	{
		if (currentFile == null)
		{
			return onSaveCityAsClicked();
		}

		try
		{
			getEngine().save(currentFile);
			makeClean();
			return true;
		}
		catch (IOException e)
		{
			e.printStackTrace(System.err);
			JOptionPane.showMessageDialog(this, e, strings.getString("main.error_caption"),
				JOptionPane.ERROR_MESSAGE);
			return false;
		}
	}

	static final String EXTENSION = "cty";
	private boolean onSaveCityAsClicked()
	{
		stopTimer();
		try
		{
			JFileChooser fc = new JFileChooser();
			FileNameExtensionFilter filter1 = new FileNameExtensionFilter(strings.getString("cty_file"), EXTENSION);
			fc.setFileFilter(filter1);
			int rv = fc.showSaveDialog(this);
			if (rv == JFileChooser.APPROVE_OPTION) {
				currentFile = fc.getSelectedFile();
				if (!currentFile.getName().endsWith("."+EXTENSION)) {
					currentFile = new File(currentFile.getPath()+"."+EXTENSION);
				}
				getEngine().save(currentFile);
				makeClean();
				return true;
			}
		}
		catch (Exception e)
		{
			e.printStackTrace(System.err);
			JOptionPane.showMessageDialog(this, e, strings.getString("main.error_caption"),
				JOptionPane.ERROR_MESSAGE);
		}
		finally
		{
			startTimer();
		}
		return false;
	}

	private void onLoadGameClicked()
	{
		// check if user wants to save their current city
		if (!maybeSaveCity()) {
			return;
		}

		try
		{
			JFileChooser fc = new JFileChooser();
			FileNameExtensionFilter filter1 = new FileNameExtensionFilter(strings.getString("cty_file"), EXTENSION);
			fc.setFileFilter(filter1);

			stopTimer();
			int rv = fc.showOpenDialog(this);
			startTimer();

			if (rv == JFileChooser.APPROVE_OPTION) {
				File file = fc.getSelectedFile();
				Micropolis newEngine = new Micropolis();
				newEngine.load(file);
				setEngine(newEngine);
				currentFile = file;
				makeClean();
			}
		}
		catch (Exception e)
		{
			e.printStackTrace(System.err);
			JOptionPane.showMessageDialog(this, e, strings.getString("main.error_caption"),
				JOptionPane.ERROR_MESSAGE);
		}
	}

	private JToggleButton makeToolBtn(final MicropolisTool tool)
	{
		JToggleButton btn = new JToggleButton();
		btn.setIcon(new ImageIcon(MainWindow.class.getResource(strings.getString("tool."+tool.name()+".icon"))));
		btn.setSelectedIcon(new ImageIcon(MainWindow.class.getResource(strings.getString("tool."+tool.name()+".selected_icon"))));
		btn.setToolTipText(strings.getString("tool."+tool.name()+".tip"));
		btn.setMargin(new Insets(0,0,0,0));
		btn.setBorderPainted(false);
		btn.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent ev)
			{
				selectTool(tool);
			}
			});
		toolBtns.put(tool, btn);
		return btn;
	}

	private JToolBar makeToolbar()
	{
		toolBtns = new EnumMap<MicropolisTool, JToggleButton>(MicropolisTool.class);

		JToolBar toolBar = new JToolBar(strings.getString("main.tools_caption"), JToolBar.VERTICAL);
		toolBar.setFloatable(false);
		toolBar.setRollover(false);

		JPanel gridBox = new JPanel(new GridBagLayout());
		toolBar.add(gridBox);

		GridBagConstraints c = new GridBagConstraints();
		c.gridx = c.gridy = 0;
		c.anchor = GridBagConstraints.NORTH;
		c.insets = new Insets(8,0,0,0);
		currentToolLbl = new JLabel(" ");
		gridBox.add(currentToolLbl, c);

		c.gridy = 1;
		c.insets = new Insets(0,0,12,0);
		currentToolCostLbl = new JLabel(" ");
		gridBox.add(currentToolCostLbl, c);

		c.gridy++;
		c.fill = GridBagConstraints.NONE;
		c.weightx = 1.0;
		c.insets = new Insets(0,0,0,0);
		Box b0 = new Box(BoxLayout.X_AXIS);
		gridBox.add(b0,c);

		b0.add(makeToolBtn(MicropolisTool.BULLDOZER));
		b0.add(makeToolBtn(MicropolisTool.WIRE));
		b0.add(makeToolBtn(MicropolisTool.PARK));

		c.gridy++;
		Box b1 = new Box(BoxLayout.X_AXIS);
		gridBox.add(b1,c);

		b1.add(makeToolBtn(MicropolisTool.ROADS));
		b1.add(makeToolBtn(MicropolisTool.RAIL));

		c.gridy++;
		Box b2 = new Box(BoxLayout.X_AXIS);
		gridBox.add(b2,c);

		b2.add(makeToolBtn(MicropolisTool.RESIDENTIAL));
		b2.add(makeToolBtn(MicropolisTool.COMMERCIAL));
		b2.add(makeToolBtn(MicropolisTool.INDUSTRIAL));

		c.gridy++;
		Box b3 = new Box(BoxLayout.X_AXIS);
		gridBox.add(b3,c);

		b3.add(makeToolBtn(MicropolisTool.FIRE));
		b3.add(makeToolBtn(MicropolisTool.QUERY));
		b3.add(makeToolBtn(MicropolisTool.POLICE));

		c.gridy++;
		Box b4 = new Box(BoxLayout.X_AXIS);
		gridBox.add(b4,c);

		b4.add(makeToolBtn(MicropolisTool.POWERPLANT));
		b4.add(makeToolBtn(MicropolisTool.NUCLEAR));

		c.gridy++;
		Box b5 = new Box(BoxLayout.X_AXIS);
		gridBox.add(b5,c);

		b5.add(makeToolBtn(MicropolisTool.STADIUM));
		b5.add(makeToolBtn(MicropolisTool.SEAPORT));

		c.gridy++;
		Box b6 = new Box(BoxLayout.X_AXIS);
		gridBox.add(b6,c);

		b6.add(makeToolBtn(MicropolisTool.AIRPORT));

		// add glue to make all elements align toward top
		c.gridy++;
		c.weighty = 1.0;
		gridBox.add(new JLabel(), c);

		return toolBar;
	}

	private void selectTool(MicropolisTool newTool)
	{
		toolBtns.get(newTool).setSelected(true);
		if (newTool == currentTool) {
			return;
		}

		if (currentTool != null) {
			toolBtns.get(currentTool).setSelected(false);
		}

		currentTool = newTool;

		currentToolLbl.setText(strings.getString("tool."+currentTool.name()+".name"));

		int cost = currentTool.getToolCost();
		currentToolCostLbl.setText(cost != 0 ? formatFunds(cost) : " ");
	}

	private void onNewCityClicked()
	{
		if (maybeSaveCity()) {
			doNewCity(false);
		}
	}

	public void doNewCity(boolean firstTime)
	{
		stopTimer();
		new NewCityDialog(this, !firstTime).setVisible(true);
		startTimer();
	}

	void doQueryTool(int xpos, int ypos)
	{
		if (!engine.testBounds(xpos, ypos))
			return;

		ZoneStatus z = engine.queryZoneStatus(xpos, ypos);
		notificationPane.showZoneStatus(engine, xpos, ypos, z);
	}

	// used when a tool is being pressed
	ToolStroke toolStroke;

	// where the tool was last applied during the current drag
	int lastX;
	int lastY;

	private void onToolDown(MouseEvent ev)
	{
		if (ev.getButton() == MouseEvent.BUTTON3) {
			doQueryTool(ev.getX() / MicropolisDrawingArea.TILE_WIDTH,
				ev.getY() / MicropolisDrawingArea.TILE_HEIGHT);
			return;
		}

		if (ev.getButton() != MouseEvent.BUTTON1)
			return;

		if (currentTool == null)
			return;

		int x = ev.getX() / MicropolisDrawingArea.TILE_WIDTH;
		int y = ev.getY() / MicropolisDrawingArea.TILE_HEIGHT;

		if (currentTool == MicropolisTool.QUERY) {
			doQueryTool(x, y);
			this.toolStroke = null;
		}
		else {
			this.toolStroke = currentTool.beginStroke(engine, x, y);
			previewTool();
		}

		this.lastX = x;
		this.lastY = y;
	}

	private void onToolUp(MouseEvent ev)
	{
		if (toolStroke != null) {
			drawingArea.setToolPreview(null);

			CityLocation loc = toolStroke.getLocation();
			ToolResult tr = toolStroke.apply();
			showToolResult(loc, tr);
			toolStroke = null;
		}

		onToolHover(ev);

		if (autoBudgetPending) {
			autoBudgetPending = false;
			showBudgetWindow(true);
		}
	}

	void previewTool()
	{
		assert this.toolStroke != null;
		assert this.currentTool != null;

		drawingArea.setToolCursor(
			toolStroke.getBounds(),
			currentTool
			);
		drawingArea.setToolPreview(
			toolStroke.getPreview()
			);
	}

	private void onToolDrag(MouseEvent ev)
	{
		if (currentTool == null)
			return;
		if ((ev.getModifiersEx() & MouseEvent.BUTTON1_DOWN_MASK) == 0)
			return;

		int x = ev.getX() / MicropolisDrawingArea.TILE_WIDTH;
		int y = ev.getY() / MicropolisDrawingArea.TILE_HEIGHT;
		if (x == lastX && y == lastY)
			return;

		if (toolStroke != null) {
			toolStroke.dragTo(x, y);
			previewTool();
		}
		else if (currentTool == MicropolisTool.QUERY) {
			doQueryTool(x, y);
		}

		lastX = x;
		lastY = y;
	}

	private void onToolHover(MouseEvent ev)
	{
		if (currentTool == null || currentTool == MicropolisTool.QUERY)
		{
			drawingArea.setToolCursor(null);
			return;
		}

		int x = ev.getX() / MicropolisDrawingArea.TILE_WIDTH;
		int y = ev.getY() / MicropolisDrawingArea.TILE_HEIGHT;
		int w = currentTool.getWidth();
		int h = currentTool.getHeight();

		if (w >= 3)
			x--;
		if (h >= 3)
			y--;

		drawingArea.setToolCursor(new Rectangle(x,y,w,h), currentTool);
	}

	private void onToolExited(MouseEvent ev)
	{
		drawingArea.setToolCursor(null);
	}

	private void showToolResult(CityLocation loc, ToolResult result)
	{
		switch (result) {
		case SUCCESS:
			citySound(currentTool == MicropolisTool.BULLDOZER ? Sound.BULLDOZE : Sound.BUILD, loc);
			dirty1 = true;
			break;

		case NONE: break;
		case UH_OH:
			messagesPane.appendCityMessage(MicropolisMessage.BULLDOZE_FIRST);
			citySound(Sound.UHUH, loc);
			break;

		case INSUFFICIENT_FUNDS:
			messagesPane.appendCityMessage(MicropolisMessage.INSUFFICIENT_FUNDS);
			citySound(Sound.SORRY, loc);
			break;

		default:
			assert false;
		}
	}

	public static String formatFunds(int funds)
	{
		return MessageFormat.format(
			strings.getString("funds"), funds
			);
	}

	public static String formatGameDate(int cityTime)
	{
		Calendar c = Calendar.getInstance();
		c.set(1900 + cityTime/48,
			(cityTime%48)/4,
			(cityTime%4)*7 + 1
			);

		return MessageFormat.format(
			strings.getString("citytime"),
			c.getTime()
			);
	}

	private void updateDateLabel()
	{
		dateLbl.setText(formatGameDate(engine.cityTime));

		NumberFormat nf = NumberFormat.getInstance();
		popLbl.setText(nf.format(getEngine().getCityPopulation()));
	}

	Timer simTimer;
	Timer shakeTimer;

	private void startTimer()
	{
		final Micropolis engine = getEngine();
		final int count = engine.simSpeed.simStepsPerUpdate;

		assert !isTimerActive();

		if (engine.simSpeed == Speed.PAUSED)
			return;

		if (currentEarthquake != null)
		{
			int interval = 3000 / MicropolisDrawingArea.SHAKE_STEPS;
			shakeTimer = new Timer(interval, new ActionListener() {
				public void actionPerformed(ActionEvent evt) {
					currentEarthquake.oneStep();
					if (currentEarthquake.count == 0) {
						stopTimer();
						currentEarthquake = null;
						startTimer();
					}
				}});
			shakeTimer.start();
			return;
		}

		ActionListener taskPerformer = new ActionListener() {
		public void actionPerformed(ActionEvent evt)
		{
			for (int i = 0; i < count; i++)
			{
				engine.animate();
				if (!engine.autoBudget && engine.isBudgetTime())
				{
					stopTimer(); //redundant
					showAutoBudget();
					return;
				}
			}
			updateDateLabel();
			dirty2 = true;
		}};
		taskPerformer = wrapActionListener(taskPerformer);

		assert simTimer == null;
		simTimer = new Timer(engine.simSpeed.animationDelay, taskPerformer);
		simTimer.start();
	}

	ActionListener wrapActionListener(final ActionListener l)
	{
		return new ActionListener() {
		public void actionPerformed(ActionEvent evt) {
			try {
				l.actionPerformed(evt);
			} catch (Throwable e) {
				showErrorMessage(e);
			}
		}};
	}

	private void showErrorMessage(Throwable e)
	{
		StringWriter w = new StringWriter();
		e.printStackTrace(new PrintWriter(w));

		JTextPane stackTracePane = new JTextPane();
		stackTracePane.setEditable(false);
		stackTracePane.setText(w.toString());

		final JScrollPane detailsPane = new JScrollPane(stackTracePane);
		detailsPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
		detailsPane.setPreferredSize(new Dimension(480,240));
		detailsPane.setMinimumSize(new Dimension(0,0));

		int rv = JOptionPane.showOptionDialog(this, e,
			strings.getString("main.error_unexpected"),
			JOptionPane.DEFAULT_OPTION,
			JOptionPane.ERROR_MESSAGE,
			null,
			new String[] {
				strings.getString("main.error_show_stacktrace"),
				strings.getString("main.error_close"),
				strings.getString("main.error_shutdown")
				},
			1
			);
		if (rv == 0)
		{
			JOptionPane.showMessageDialog(this, detailsPane,
				strings.getString("main.error_unexpected"),
				JOptionPane.ERROR_MESSAGE);
		}
		if (rv == 2)
		{
			rv = JOptionPane.showConfirmDialog(
				this,
				strings.getString("error.shutdown_query"),
				strings.getString("main.error_unexpected"),
				JOptionPane.OK_CANCEL_OPTION,
				JOptionPane.WARNING_MESSAGE);
			if (rv == JOptionPane.OK_OPTION) {
				System.exit(1);
			}
		}
	}

	class EarthquakeStepper
	{
		int count = 0;
		void oneStep()
		{
			count = (count + 1) % MicropolisDrawingArea.SHAKE_STEPS;
			drawingArea.shake(count);
		}
	}
	EarthquakeStepper currentEarthquake;

	//implements EarthquakeListener
	public void earthquakeStarted()
	{
		stopTimer();
		currentEarthquake = new EarthquakeStepper();
		currentEarthquake.oneStep();
		startTimer();
	}

	void stopEarthquake()
	{
		drawingArea.shake(0);
		currentEarthquake = null;
	}

	private void stopTimer()
	{
		if (simTimer != null) {
			simTimer.stop();
			simTimer = null;
		}
		if (shakeTimer != null) {
			shakeTimer.stop();
			shakeTimer = null;
		}
	}

	boolean isTimerActive()
	{
		return simTimer != null || shakeTimer != null;
	}

	private void onWindowClosed(WindowEvent ev)
	{
		stopTimer();
	}

	private void onDifficultyClicked(int newDifficulty)
	{
		getEngine().setGameLevel(newDifficulty);
	}

	private void onPriorityClicked(Speed newSpeed)
	{
		stopTimer();
		getEngine().setSpeed(newSpeed);
		startTimer();
	}

	private void onInvokeDisasterClicked(Disaster disaster)
	{
		dirty1 = true;
		switch (disaster) {
		case FIRE:
			getEngine().makeFire();
			break;
		case FLOOD:
			getEngine().makeFlood();
			break;
		case MONSTER:
			getEngine().makeMonster();
			break;
		case MELTDOWN:
			if (!getEngine().makeMeltdown()) {
				messagesPane.appendCityMessage(MicropolisMessage.NO_NUCLEAR_PLANTS);
			}
			break;
		case TORNADO:
			getEngine().makeTornado();
			break;
		case EARTHQUAKE:
			getEngine().makeEarthquake();
			break;
		default:
			assert false; //unknown disaster
		}
	}

	private void reloadFunds()
	{
		fundsLbl.setText(formatFunds(getEngine().budget.totalFunds));
	}

	//implements Micropolis.Listener
	public void cityMessage(MicropolisMessage m, CityLocation p, boolean pictureMessage)
	{
		messagesPane.appendCityMessage(m);

		if (pictureMessage && p != null)
		{
			notificationPane.showMessage(engine, m, p.x, p.y);
		}
	}

	//implements Micropolis.Listener
	public void fundsChanged()
	{
		reloadFunds();
	}

	//implements Micropolis.Listener
	public void optionsChanged()
	{
		reloadOptions();
	}

	private void reloadOptions()
	{
		autoBudgetMenuItem.setSelected(getEngine().autoBudget);
		autoBulldozeMenuItem.setSelected(getEngine().autoBulldoze);
		disastersMenuItem.setSelected(!getEngine().noDisasters);
		soundsMenuItem.setSelected(doSounds);
		for (Speed spd : priorityMenuItems.keySet())
		{
			priorityMenuItems.get(spd).setSelected(getEngine().simSpeed == spd);
		}
		for (int i = GameLevel.MIN_LEVEL; i <= GameLevel.MAX_LEVEL; i++)
		{
			difficultyMenuItems.get(i).setSelected(getEngine().gameLevel == i);
		}
	}

	//implements Micropolis.Listener
	public void citySound(Sound sound, CityLocation loc)
	{
		if (!doSounds)
			return;

		URL afile = sound.getAudioFile();
		if (afile == null)
			return;

		boolean isOnScreen = drawingAreaScroll.getViewport().getViewRect().contains(
				drawingArea.getTileBounds(loc.x, loc.y)
			);
		if (sound == Sound.HONKHONK_LOW && !isOnScreen)
			return;

		try
		{
			Clip clip = AudioSystem.getClip();
			clip.open(AudioSystem.getAudioInputStream(afile));
			clip.start();
		}
		catch (Exception e)
		{
			e.printStackTrace(System.err);
		}
	}

	//implements Micropolis.Listener
	public void censusChanged() { }
	public void demandChanged() { }
	public void evaluationChanged() { }

	void onViewBudgetClicked()
	{
		dirty1 = true;
		showBudgetWindow(false);
	}

	void onViewEvaluationClicked()
	{
		evaluationPane.setVisible(true);
	}

	void onViewGraphClicked()
	{
		graphsPane.setVisible(true);
	}

	private void showAutoBudget()
	{
		if (toolStroke == null) {
			showBudgetWindow(true);
		}
		else {
			autoBudgetPending = true;
		}
	}

	private void showBudgetWindow(boolean isEndOfYear)
	{
		stopTimer();
		BudgetDialog dlg = new BudgetDialog(this, getEngine());
		dlg.setModal(true);
		dlg.setVisible(true);
		startTimer();
	}

	private JMenuItem makeMapStateMenuItem(String caption, final MapState state)
	{
		JMenuItem menuItem = new JRadioButtonMenuItem(caption);
		menuItem.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent evt) {
				setMapState(state);
			}
			});
		mapStateMenuItems.put(state, menuItem);
		return menuItem;
	}

	private void setMapState(MapState state)
	{
		mapStateMenuItems.get(mapView.getMapState()).setSelected(false);
		mapStateMenuItems.get(state).setSelected(true);
		mapView.setMapState(state);
		setMapLegend(state);
	}

	private void setMapLegend(MapState state)
	{
		String k = "legend_image."+state.name();
		java.net.URL iconUrl = null;
		if (strings.containsKey(k)) {
			String iconName = strings.getString(k);
			iconUrl = MainWindow.class.getResource(iconName);
		}
		if (iconUrl != null) {
			mapLegendLbl.setIcon(new ImageIcon(iconUrl));
		}
		else {
			mapLegendLbl.setIcon(null);
		}
	}

	private void onLaunchTranslationToolClicked()
	{
		if (maybeSaveCity()) {
			dispose();
			TranslationTool tt = new TranslationTool();
			tt.setVisible(true);
		}
	}

	private void onAboutClicked()
	{
		String version = getClass().getPackage().getImplementationVersion();
		String versionStr = MessageFormat.format(strings.getString("main.version_string"), version);
		versionStr = versionStr.replace("%java.version%", System.getProperty("java.version"));
		versionStr = versionStr.replace("%java.vendor%", System.getProperty("java.vendor"));

		JLabel appNameLbl = new JLabel(versionStr);
		JLabel appDetailsLbl = new JLabel(strings.getString("main.about_text"));
		JComponent [] inputs = new JComponent[] {  appNameLbl, appDetailsLbl };
		JOptionPane.showMessageDialog(this,
			inputs,
			strings.getString("main.about_caption"),
			JOptionPane.PLAIN_MESSAGE,
			appIcon);
	}
}
