/*
 * Copyright 2008-2014, David Karnok 
 * The file is part of the Open Imperium Galactica project.
 * 
 * The code should be distributed under the LGPL license.
 * See http://www.gnu.org/licenses/lgpl.html for details.
 */

package hu.openig.screen;

import hu.openig.core.Action0;
import hu.openig.core.Action1;
import hu.openig.core.Func0;
import hu.openig.core.Func1;
import hu.openig.core.ResourceType;
import hu.openig.core.SaveMode;
import hu.openig.core.SimulationSpeed;
import hu.openig.core.CursorResource;
import hu.openig.gfx.BackgroundGFX;
import hu.openig.gfx.ColonyGFX;
import hu.openig.gfx.CommonGFX;
import hu.openig.gfx.DatabaseGFX;
import hu.openig.gfx.DiplomacyGFX;
import hu.openig.gfx.EquipmentGFX;
import hu.openig.gfx.InfoGFX;
import hu.openig.gfx.ResearchGFX;
import hu.openig.gfx.SpacewarGFX;
import hu.openig.gfx.StarmapGFX;
import hu.openig.gfx.StatusbarGFX;
import hu.openig.mechanics.Allocator;
import hu.openig.mechanics.Radar;
import hu.openig.mechanics.Simulator;
import hu.openig.model.AIManager;
import hu.openig.model.Configuration;
import hu.openig.model.GameDefinition;
import hu.openig.model.GameEnvironment;
import hu.openig.model.Labels;
import hu.openig.model.MultiplayerDefinition;
import hu.openig.model.Player;
import hu.openig.model.Profile;
import hu.openig.model.ResearchType;
import hu.openig.model.ResourceLocator;
import hu.openig.model.ResourceLocator.ResourcePlace;
import hu.openig.model.Screens;
import hu.openig.model.SkirmishAIMode;
import hu.openig.model.SkirmishDefinition;
import hu.openig.model.SkirmishPlayer;
import hu.openig.model.SoundTarget;
import hu.openig.model.SoundType;
import hu.openig.model.Traits;
import hu.openig.model.World;
import hu.openig.music.Music;
import hu.openig.render.TextRenderer;
import hu.openig.screen.api.EquipmentScreenAPI;
import hu.openig.screen.api.ResearchProductionAnimation;
import hu.openig.sound.Sounds;
import hu.openig.ui.UIMouse;
import hu.openig.ui.UIMouse.Button;
import hu.openig.ui.UIMouse.Modifier;
import hu.openig.utils.Exceptions;
import hu.openig.utils.U;
import hu.openig.utils.WipPort;
import hu.openig.utils.XElement;

import java.awt.Point;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.image.BufferedImage;
import java.io.Closeable;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;

import javax.swing.SwingUtilities;
import javax.swing.Timer;



/**
 * Contains all common ang game specific graphical and textual resources.
 * @author akarnokd, 2009.12.25.
 */
public class CommonResources implements GameEnvironment {
	/** The main configuration object. */
	public Configuration config;
	/** The main resource locator object. */
	public ResourceLocator rl;
	/** The global and game specific labels. */
	private Labels labels;
	/** The status bar graphics. */
	private StatusbarGFX statusbar;
	/** The background graphics. */
	private BackgroundGFX background;
	/** The equipment graphics. */
	private EquipmentGFX equipment;
	/** The space war graphics. */
	private SpacewarGFX spacewar;
	/** The info graphics. */
	private InfoGFX info;
	/** The research graphics. */
	private ResearchGFX research;
	/** The colony graphics. */
	private ColonyGFX colony;
	/** The starmap graphics. */
	private StarmapGFX starmap;
	/** The database graphics. */
	private DatabaseGFX database;
	/** The common graphics. */
	private CommonGFX common;
	/** The diplomacy graphics. */
	private DiplomacyGFX diplomacy;
	/** The text renderer. */
	private TextRenderer text;
	/** The general control interface. */
	private GameControls control;
	/** The current player's profile. */
	public Profile profile;
	/**
	 * The queue for notifying the user about achievements.
	 */
	public final Deque<String> achievementNotifier = new LinkedList<>();
	// --------------------------------------------
	// The various screen objects
	// --------------------------------------------
	/** The game world. */
	private World world;
	/** Flag to indicate the game world is loading. */
	public volatile boolean worldLoading;
	/** The game is in battle mode. */
	public boolean battleMode;
	/** Flag indicating the statusbar screen to show a non-game statusbar. */
	public boolean nongame;
	/** The common executor service. */
	public final ScheduledExecutorService pool;
	/** The combined timer for synchronized frequency updates. */
	public Timer timer;
//	/** The periodic timer. */
//	public Future<?> timerFuture;
	/** The timer delay in milliseconds. */
	public static final int TIMER_DELAY = 25;
	/** The timer tick. */
	long tick;
	/** The registration map. */
	final Map<Closeable, TimerAction> timerHandlers = new ConcurrentHashMap<>();
	/** Caches the created cursors. */
	final Map<hu.openig.model.Cursors, java.awt.Cursor> cursorCache = new HashMap<>();
	/** The timer action. */
	static class TimerAction {
		/** The operation frequency. */
		public int delay;
		/** The action to invoke. */
		public final Action0 action;
		/** The flag to indicate the action was cancelled. */
		public boolean cancelled;
        /**
         * Constructor, initializes the action.
         * @param action the action to call after the delay
         */
        public TimerAction(Action0 action) {
            this.action = Objects.requireNonNull(action);
        }
	}
	/** The radar handler. */
	protected Closeable radarHandler;
	/** The allocator handler. */
	protected Closeable allocatorHandler;
	/** The sound objects.*/
	public Sounds sounds;
	/** The music player. */
	public Music music;
	/** The current simulation controls. */
	public SimulationTimer simulation;
	/** Map of currently running AIs. */
	public final Map<Player, Future<?>> runningAI = new HashMap<>();
	/** Indicate if an asynchronous save is in operation. */
	public final WipPort saving = new WipPort();
	/** Disable controls and force watching the video. */
	public boolean force;
	/** Counts the current simulation step and invokes the simulator on every 4th. */
	protected long simulationStep;
	/** The global traits. */
	public final Traits traits = new Traits();
	/** The pool thread factory. */
	static final class BasicThreadFactory implements ThreadFactory {
		/** Thread number counter. */
		final AtomicInteger count = new AtomicInteger();
		@Override
		public Thread newThread(Runnable r) {
			Thread t = new Thread(r, "CPU-Pool-" + count.incrementAndGet());
			t.setDaemon(true);
			return t;
		}
	}
	/**
	 * Constructor. Initializes and loads all resources.
	 * @param config the configuration object.
	 * @param control the general control
	 */
	public CommonResources(Configuration config, GameControls control) {
		this.config = config;
		this.control = control;
		this.profile = new Profile();
		this.profile.name = config.currentProfile;
		
		int ncpu = Runtime.getRuntime().availableProcessors();
		ScheduledThreadPoolExecutor scheduler = new ScheduledThreadPoolExecutor(ncpu, new BasicThreadFactory());
//		scheduler.setKeepAliveTime(1500, TimeUnit.MILLISECONDS);
//		scheduler.allowCoreThreadTimeOut(true);
		scheduler.setRemoveOnCancelPolicy(true);
		pool = scheduler;

		init();
	}
	/**
	 * Initiate the timer task.
	 */
	public void startTimer() {
		stopTimer();
		timer = new Timer(TIMER_DELAY, new ActionListener() {
			@Override
			public void actionPerformed(ActionEvent e) {
				tick++;
				doTimerTick();
			}
		});
		timer.start();
	}
	/**
	 * Stop the timer task.
	 */
	public void stopTimer() {
		if (timer != null) {
			timer.stop();
		}
	}
	/** Initialize the resources in parallel. */
	private void init() {
		final ResourceLocator rl = config.newResourceLocator();
		final ExecutorService exec = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
		try {
			final Future<Labels> labelFuture = exec.submit(new Callable<Labels>() {
				@Override
				public Labels call() throws Exception {
					return new Labels().load(rl, Collections.singleton("labels"));
				}
			});
			final Future<StatusbarGFX> statusbarFuture = exec.submit(new Callable<StatusbarGFX>() {
				@Override
				public StatusbarGFX call() throws Exception {
					return new StatusbarGFX().load(rl);
				}
			});
			final Future<BackgroundGFX> backgroundFuture = exec.submit(new Callable<BackgroundGFX>() {
				@Override
				public BackgroundGFX call() throws Exception {
					return new BackgroundGFX().load(rl);
				}
			});
			final Future<EquipmentGFX> equipmentFuture = exec.submit(new Callable<EquipmentGFX>() {
				@Override
				public EquipmentGFX call() throws Exception {
					return new EquipmentGFX().load(rl);
				}
			});
			final Future<SpacewarGFX> spacewarFuture = exec.submit(new Callable<SpacewarGFX>() {
				@Override
				public SpacewarGFX call() throws Exception {
					return new SpacewarGFX().load(rl);
				}
			});
			final Future<InfoGFX> infoFuture = exec.submit(new Callable<InfoGFX>() {
				@Override
				public InfoGFX call() throws Exception {
					return new InfoGFX().load(rl);
				}
			});
			final Future<ResearchGFX> researchFuture = exec.submit(new Callable<ResearchGFX>() {
				@Override
				public ResearchGFX call() throws Exception {
					return new ResearchGFX().load(rl);
				}
			});
			final Future<ColonyGFX> colonyFuture = exec.submit(new Callable<ColonyGFX>() {
				@Override
				public ColonyGFX call() throws Exception {
					return new ColonyGFX().load(rl);
				}
			});
			final Future<StarmapGFX> starmapFuture = exec.submit(new Callable<StarmapGFX>() {
				@Override
				public StarmapGFX call() throws Exception {
					return new StarmapGFX().load(rl);
				}
			});
			final Future<DatabaseGFX> databaseFuture = exec.submit(new Callable<DatabaseGFX>() {
				@Override
				public DatabaseGFX call() throws Exception {
					return new DatabaseGFX().load(rl);
				}
			});
			final Future<TextRenderer> textFuture = exec.submit(new Callable<TextRenderer>() {
				@Override
				public TextRenderer call() throws Exception {
					return new TextRenderer(rl, config.useStandardFonts, config.textCacheSize);
				}
			});
			final Future<DiplomacyGFX> diplomacyFuture = exec.submit(new Callable<DiplomacyGFX>() {
				@Override
				public DiplomacyGFX call() throws Exception {
					return new DiplomacyGFX().load(rl);
				}
			});
			final Future<CommonGFX> commonFuture = pool.submit(new Callable<CommonGFX>() {
				@Override
				public CommonGFX call() throws Exception {
					return new CommonGFX().load(rl);
				}
			});
			labels = get(labelFuture);
			statusbar = get(statusbarFuture);
			background = get(backgroundFuture);
			equipment = get(equipmentFuture);
			spacewar = get(spacewarFuture);
			info = get(infoFuture);
			research = get(researchFuture);
			colony = get(colonyFuture);
			starmap = get(starmapFuture);
			database = get(databaseFuture);
			text = get(textFuture);
			diplomacy = get(diplomacyFuture);
			common = get(commonFuture);

			sounds = new Sounds(rl);
			sounds.initialize(config.audioChannels, new Func0<Integer>() {
				@Override
				public Integer invoke() {
					return config.muteEffect ? 0 : config.effectVolume;
				}
			});
			
			music = new Music(rl);
			music.setVolume(config.musicVolume);
			music.setMute(config.muteMusic);

			traits.load(rl.getXML("traits"));
			
			text.setFontScaling(config.uiScale / 100d);
			
			// FIXME during translation
			
			this.rl = rl;
//			labelReloader = new Thread() {
//				@Override
//				public void run() {
//					watchLabels();
//				}
//			};
//			labelReloader.setDaemon(true);
//			labelReloader.start();
			
			setupCursors();
		} finally {
			exec.shutdown();
		}
		startTimer();
	}
	/** Setup the custom cursors. */
	void setupCursors() {
		for (Field f : common.getClass().getFields()) {
			CursorResource res = f.getAnnotation(CursorResource.class);
			if (res != null) {
				try {
					cursorCache.put(res.key(),
						Toolkit.getDefaultToolkit().createCustomCursor(
							(BufferedImage)f.get(common), new Point(res.x(), res.y()), res.key().name())
					);
				} catch (IllegalAccessException e) {
					Exceptions.add(e);
				}
			}
		}
	}
	/**
	 * Reinitialize the resources by reloading them in the new language.
	 * @param newLanguage the new language
	 */
	public void reinit(String newLanguage) {
		config.language = newLanguage;
		config.save();
		sounds.close();
		init();
	}
	/**
	 * @return the current language code
	 */
	public String language() {
		return config.language;
	}
	/**
	 * Switch to the screen named.
	 * @param to the screen name
	 */
	public void switchScreen(String to) {
		switch (to) {
		case "*bridge":
			control.displayPrimary(Screens.BRIDGE);
			break;
		case "*starmap":
			control.displayPrimary(Screens.STARMAP);
			break;
		case "*colony":
			control.displayPrimary(Screens.COLONY);
			break;
		case "*equipment":
			control.displaySecondary(Screens.EQUIPMENT);
			break;
		case "*research":
			control.displaySecondary(Screens.RESEARCH);
			break;
		case "*production":
			control.displaySecondary(Screens.PRODUCTION);
			break;
		case "*information":
			control.displaySecondary(Screens.INFORMATION_PLANETS);
			break;
		case "*database":
			control.displaySecondary(Screens.DATABASE);
			break;
		case "*bar":
			control.displaySecondary(Screens.BAR);
			break;
		case "*diplomacy":
			control.displaySecondary(Screens.DIPLOMACY);
			break;
		default:
			throw new IllegalArgumentException(to);
		}

	}
	/**
	 * Set custom mouse cursor.
	 * @param cursor the cursor name
	 */
	public void setCursor(hu.openig.model.Cursors cursor) {
		if (config.customCursors) {
			control.renderingComponent().setCursor(cursorCache.get(cursor));
		}
	}
	/**
	 * Retrieve the result of the future and convert any exception
	 * to runtime exception.
	 * @param <T> the value type
	 * @param future the future for the computation
	 * @return the value
	 */
	public static <T> T get(Future<? extends T> future) {
		try {
			return future.get();
		} catch (ExecutionException | InterruptedException ex) {
			throw new RuntimeException(ex);
		}
	}
	/** @return lazily initialize the labels or return the existing one. */
	@Override
	public Labels labels() {
		return labels;
	}
	/** @return lazily initialize the status bar or return the existing one. */
	public StatusbarGFX statusbar() {
		return statusbar;
	}
	/** @return lazily initialize the background or return the existing one. */
	public BackgroundGFX background() {
		return background;
	}
	/** @return lazily initialize the equipment or return the existing one. */
	public EquipmentGFX equipment() {
		return equipment;
	}
	/** @return lazily initialize the spacewar or return the existing one. */
	public SpacewarGFX spacewar() {
		return spacewar;
	}
	/** @return lazily initialize the info or return the existing one. */
	public InfoGFX info() {
		return info;
	}
	/** @return lazily initialize the research or return the existing one. */
	public ResearchGFX research() {
		return research;
	}
	/** @return lazily initialize the colony or return the existing one. */
	public ColonyGFX colony() {
		return colony;
	}
	/** @return lazily initialize the starmap or return the existing one. */
	public StarmapGFX starmap() {
		return starmap;
	}
	/** @return lazily initialize the database or return the existing one. */
	public DatabaseGFX database() {
		return database;
	}
	/** @return lazily initialize the text or return the existing one. */
	public TextRenderer text() {
		return text;
	}
	/** @return lazily initialize the common graphics or return the existing one. */
	public CommonGFX common() {
		return common;
	}
	/** @return lazily initialize the diplomacy graphics or return the existing one. */
	public DiplomacyGFX diplomacy() {
		return diplomacy;
	}
	/**
	 * Convenience method to return a video for the current language.
	 * @param name the video name.
	 * @return the resource place for the video
	 */
	public ResourcePlace video(String name) {
		return rl.get(name, ResourceType.VIDEO);
	}
	/**
	 * Convenience method to return a video for the current language.
	 * @param name the video name.
	 * @param fallback fall back to english if not found?
	 * @return the resource place for the video
	 */
	public ResourcePlace video(String name, boolean fallback) {
		ResourcePlace rp = rl.get(name, ResourceType.VIDEO);
		if (rp == null) {
			rp = rl.getExactly("en", name, ResourceType.VIDEO);
		}
		return rp;
	}
	/**
	 * Convenience method to return an audio for the current language.
	 * @param name the video name.
	 * @return the resource place for the video
	 */
	public ResourcePlace audio(String name) {
		return rl.get(name, ResourceType.AUDIO);
	}
	/** Close the resources. */
	public void stop() {
//		timer.stop();
		stopTimer();
		
		close0(allocatorHandler);
		close0(radarHandler);
		close0(simulation);


		for (Future<?> sw : runningAI.values()) {
			sw.cancel(true);
		}
		runningAI.clear();
				
		allocatorHandler = null;
		radarHandler = null;
		simulation = null;
		
		stopMusic();
	}
	/**
	 * Close the given closeable silently.
	 * @param c the closeable
	 */
	void close0(Closeable c) {
		try {
			if (c != null) {
				c.close();
			}
		} catch (IOException ex) {
			// Ignored
		}
	}
	/** Restore the main simulation speed function. Call this function after the battle completes. */
	public void restoreMainSimulationSpeedFunction() {
		replaceSimulation(
				new Action0() {
					@Override
					public void invoke() {
						simulation();
					}
				},
				new Func1<SimulationSpeed, Integer>() {
					@Override
					public Integer invoke(SimulationSpeed value) {
						return simulationMilliseconds(value);
					}
				}
		);
	}
	/**
	 * Returns the milliseconds between simulation steps.
	 * @param value the simulation speed indicator
	 * @return the step size in milliseconds
	 */
	public int simulationMilliseconds(SimulationSpeed value) {
		switch (value) {
		case NORMAL: return roundMillis(1000 / world.params().simulationRatio());
		case FAST: return roundMillis(500 / world.params().simulationRatio());
		case ULTRA_FAST: return roundMillis(250 / world.params().simulationRatio());
		default:
			throw new AssertionError("" + value);
		}
	}
	/**
	 * Rounds a millisecond value upwards to the next 25-multiple value.
	 * @param n the original value
	 * @return the rounded value.
	 */
	int roundMillis(int n) {
		if (n < TIMER_DELAY) {
			return TIMER_DELAY;
		} else
		if (n % TIMER_DELAY == 0) {
			return n;
		}
		return (int)(Math.round(n / (double)TIMER_DELAY) * TIMER_DELAY); 
		
	}
	/**
	 * Invoke the AI for the player if not already running.
	 * @param p the player
	 * @param wip the work in progress port
	 * @return AI was executed?
	 */
	boolean prepareAI(final Player p, final WipPort wip) {

		Future<?> sw = runningAI.get(p);
		// if not present or finished, start a new
		if (sw == null) {
			wip.inc();
			Runnable run = new Runnable() {
				@Override
				public void run() {
					prepareAIAsync(p, wip);
				}
			};
			runningAI.put(p, pool.submit(run));
			return true;
		}
		return false;
	}
	/**
	 * Invoke the actual AI management code.
	 * @param p the player
	 */
	void invokeAI(final Player p) {
		Runnable run = new Runnable() {
			@Override
			public void run() {
				runAIAsync(p);
			}
		};
		runningAI.put(p, pool.submit(run));
	}
	/**
	 * Prepare the AI state in parallel.
	 * @param p the player
	 * @param wip the completion port
	 */
	void prepareAIAsync(final Player p, final WipPort wip) {
		try {
			try {
				// parallel convert world state
				p.ai.prepare();
			} finally {
				// wait for all to read world state
				wip.dec();
			}
		} catch (Throwable t) {
			Exceptions.add(t);
		}
	}
	/**
	 * Run the AI body function.
	 * @param p the player
	 */
	void runAIAsync(final Player p) {
		try {
			// act on the world state
			p.ai.manage();
			// issue commands
			completeAIAsync(p);
		} catch (Throwable t) {
			Exceptions.add(t);
		}
	}
	/**
	 * Complete the AI activities on the EDT.
	 * @param p the player
	 */
	void completeAIAsync(final Player p) {
		SwingUtilities.invokeLater(new Runnable() {
			@Override
			public void run() {
				try {
					if (!battleMode) {
						p.ai.apply();
					}
				} catch (Throwable t) {
					Exceptions.add(t);
				} finally {
					runningAI.remove(p);
				}
			}
		});
	}
	/**
	 * Execute a step of simulation.
	 */
	public void simulation() {
		boolean repaint = false;
		if (simulationStep++ % world.params().simulationRatio() == 0) {
			if (Simulator.compute(world)) {
				if (world.scripting.mayAutoSave()) {
					control.save(null, SaveMode.AUTO);
				}
			}
			// run AI routines in background
			final WipPort wip = new WipPort(1);
			
			List<Player> ais = new ArrayList<>();
			for (final Player p : world.players.values()) {
				if (p.ai != null) {
					if (prepareAI(p, wip)) {
						ais.add(p);
					}
				}
			}
			wip.dec();
			try {
				wip.await();
			} catch (InterruptedException ex) {
				// ignored
			}
			for (final Player p : ais) {
				invokeAI(p);
			}
			repaint = true;
		}
		// let the screen's natural frequency update the on-screen location
		/* repaint |= */Simulator.moveFleets(world);
		
		if (!world.pendingBattles.isEmpty()) {
			world.env.startBattle();
		}
		if (repaint) {
			control.repaintInner();
		}
	}
	/**
	 * Replace the current simulation controls with a new
	 * simulation controls.
	 * @param action the new simulation action
	 * @param delay the function to tell the delay value from speed enumeration.
	 */
	public void replaceSimulation(Action0 action, Func1<SimulationSpeed, Integer> delay) {
		close0(simulation);
		simulation = newSimulationTimer(action, delay);
	}
	/** 
	 * Start the timed actions.
	 * @param withMusic set if play music 
	 */
	public void start(boolean withMusic) {
		restoreMainSimulationSpeedFunction();

		radarHandler = register(1000, new Action0() {
			@Override
			public void invoke() {
				world.statistics.playTime.value++;
				if (!simulation.paused()) {
					world.statistics.simulationTime.value++;
				}
				Radar.compute(world);
				if (control.primary() == Screens.STARMAP) {
					control.repaintInner();
				}
			}
		});
		allocatorHandler = register(1000, new Action0() {
			@Override
			public void invoke() {
				Allocator.compute(world, pool);
				control.repaintInner();
			}
		});
		Allocator.compute(world, pool);
		Radar.compute(world);
		
		simulation.resume();
		
		startTimer();
//		timer.start();
		if (withMusic) {
			playRegularMusic();
		}
		
		battleMode = false;
	}
	/** @return the world instance. */
	@Override
	public World world() {
		return world;
	}
	/**
	 * Set the world.
	 * @param w the new world
	 * @return this
	 */
	public CommonResources world(World w) {
		if (this.world != null && w == null) {
			world.scripting.done();
		}
		this.world = w;
		return this;
	}
	/**
	 * Set the game control peer.
	 * @param ctrl the new game control peer
	 * @return this
	 */
	public CommonResources control(GameControls ctrl) {
		this.control = ctrl;
		return this;
	}
	/** @return the control object */
	public GameControls control() {
		return control;
	}
	/** Execute the timer tick actions. */
	void doTimerTick() {
		for (TimerAction act : new ArrayList<>(timerHandlers.values())) {
			if (!act.cancelled) {
				if ((tick * TIMER_DELAY) % act.delay == 0) {
					try {
						act.action.invoke();
					} catch (CancellationException ex) {
						act.cancelled = true;
					} catch (Throwable t) {
						Exceptions.add(t);
					}
				}
			}
		}
	}
	/**
	 * Register a repeating action with the given delay.
	 * @param delay the requested frequency in milliseconds
	 * @param action the action to invoke
	 * @return the handler to close this instance
	 */
	public Closeable register(int delay, Action0 action) {
		if (delay % TIMER_DELAY != 0 || delay == 0) {
			throw new IllegalArgumentException("The delay must be in multiples of " + TIMER_DELAY + " milliseconds!");
		}
		if (action == null) {
			throw new IllegalArgumentException("action is null");
		}
		final TimerAction ta = new TimerAction(action);
		ta.delay = delay;
		Closeable res = new Closeable() {
			@Override
			public void close() throws IOException {
				ta.cancelled = true;
				timerHandlers.remove(this);
			}
		};
		timerHandlers.put(res, ta);
		return res;
	}
	/**
	 * Convenience method to start playing the original three musics.
	 */
	public void playRegularMusic() {
		stopMusic();
		
		List<String> musicList = new ArrayList<>();
		int maxLen = 0;
		for (ResourcePlace rp : rl.list(language(), "music/Music")) {
			musicList.add(rp.getName());
			maxLen = Math.max(rp.getName().length(), maxLen);
		} 
		
		final int fMaxLen = maxLen;
		Collections.sort(musicList, new Comparator<String>() {
			@Override
			public int compare(String o1, String o2) {
				String s1 = U.padLeft(o1.substring(11), fMaxLen, ' ');
				String s2 = U.padLeft(o2.substring(11), fMaxLen, ' ');
				return s1.compareTo(s2);
			}
		});
		
		String[] musics = musicList.toArray(new String[musicList.size()]);
		music.playLooped(musics);
	}
	/** Convenience method to start playing the original battle music. */
	public void playBattleMusic() {
		stopMusic();
		music.playLooped("music/War");
	}
	@Override
	public void stopMusic() {
		music.close();
	}
	/**
	 * Create a new simulation timer controls with the given action.
	 * @param action the simulation action
		 * @param delay the function which tells the delay from the speed enumeration
	 * @return the new simulation timer
	 */
	public SimulationTimer newSimulationTimer(Action0 action, Func1<SimulationSpeed, Integer> delay) {
		return new SimulationTimer(action, delay);
	}
	/**
	 * The class to manage simulation timer related commands (pause, resume, current).
	 * @author akarnokd, 2011.09.01.
	 */
	public class SimulationTimer implements Closeable {
		/** The handler for the timer. */
		protected Closeable handler;
		/** The current speed value. */
		protected SimulationSpeed speed = SimulationSpeed.NORMAL;
		/** Is the simulation paused? */
		protected boolean paused = true;
		/** The timer action. */
		protected final Action0 action;
		/** The delay computation function. */
		protected final Func1<SimulationSpeed, Integer> delay;
		/**
		 * Constructor.
		 * @param action the timer action.
		 * @param delay the function which tells the delay from the speed enumeration
		 */
		public SimulationTimer(Action0 action, Func1<SimulationSpeed, Integer> delay) {
			this.action = action;
			this.delay = delay;
		}
		/**
		 * Pauses the simulation if not already paused.
		 */
		public void pause() {
			if (!paused) {
				paused = true;
				close();
			}
		}
		/** Resumes the simulation if not already running. */
		public void resume() {
			if (paused) {
				speed(speed);
			}
		}
		/** Register the action with the timer and delay. */
		void registerAction() {
			close();
			handler = register(delay.invoke(speed), action);
		}
		/**
		 * Sets a new simulation speed and resumes the simulation.
		 * @param newSpeed the new speed
		 */
		public void speed(SimulationSpeed newSpeed) {
			if (newSpeed != speed || paused) {
				speed = newSpeed;
				paused = false;
				registerAction();
			}
		}
		/** @return the current simulation speed. */
		public SimulationSpeed speed() {
			return speed;
		}
		/** @return true if the simulation is paused. */
		public boolean paused() {
			return paused;
		}
		@Override
		public void close() {
			close0(handler);
			handler = null;
		}
		/**
		 * Returns the simulation speed in milliseconds.
		 * @return the simulation speed in milliseconds.
		 */
		public int speedValue() {
			return delay.invoke(speed);
		}
	}
	@Override
	public AIManager getAI(Player player) {
		return control.aiFactory().invoke(player);
	}
	@Override
	public void startBattle() {
		control.startBattle();
	}
	@Override
	public void playAudio(String name, final Action0 action) {
		Music m = new Music(rl);
		m.onComplete = new Action1<String>() {
			@Override
			public void invoke(String value) {
				if (world != null) {
					world.scripting.onSoundComplete(value);
				}
				if (action != null) {
					action.invoke();
				}
			}
		};
		m.setVolume(config.effectVolume);
		m.setMute(config.muteEffect);
		m.playSequence(name);
	}
	@Override
	public Action0 playSound(SoundTarget target, SoundType type, Action0 action) {
		switch (target) {
		case COMPUTER:
			if (config.computerVoiceNotify) {
				return sounds.playSound(type, action);
			}
			if (action != null) {
				action.invoke();
			}
			break;
		case BUTTON:
			if (config.buttonSounds) {
				return sounds.playSound(type, action);
			}
			if (action != null) {
				action.invoke();
			}
			break;
		case EFFECT:
			return sounds.playSound(type, action);
		case SCREEN:
			if (config.computerVoiceScreen) {
				return sounds.playSound(type, action);
			}
			if (action != null) {
				action.invoke();
			}
			break;
		default:
			if (action != null) {
				action.invoke();
			}
		}
		return null;
	}
	@Override
	public void playVideo(final String name, final Action0 action) {
		final boolean running = simulation.paused();
		simulation.pause();
		control.playVideos(new Action0() {
			@Override
			public void invoke() {
				if (world != null) {
					world.scripting.onVideoComplete(name);
				}
				if (!running) {
					simulation.resume();
				}
				if (action != null) {
					action.invoke();
				}
			}
		}, name);
	}
	@Override
	public Configuration config() {
		return config;
	}
	@Override
	public Deque<String> achievementQueue() {
		return achievementNotifier;
	}
	@Override
	public Profile profile() {
		return profile;
	}
	@Override
	public void forceMessage(String messageId, Action0 onSeen) {
		control().forceMessage(messageId, onSeen);
	}
	@Override
	public void loseGame() {
		control().loseGame();
	}
	@Override
	public void winGame() {
		control().winGame();
	}
	@Override
	public void showObjectives(boolean state) {
		control().showObjectives(state);
	}
	@Override
	public int simulationSpeed() {
		return simulation.speedValue();
	}
	@Override
	public void pause() {
		simulation.pause();
	}
	@Override
	public void speed1() {
		if (!simulation.paused()) {
			simulation.speed(SimulationSpeed.NORMAL);
		}
	}
	@Override
	public void playMusic() {
		playRegularMusic();
	}
	@Override
	public boolean isBattle() {
		return battleMode;
	}
	/**
	 * Execute a function on the EDT and return its value.
	 * @param <T> the value type
	 * @param func the function to call
	 * @return the value returned
	 * @throws InterruptedException on interrupt
	 * @throws InvocationTargetException on error
	 */
	public static <T> T callEDT(final Func0<? extends T> func) 
			throws InterruptedException, InvocationTargetException {
		if (SwingUtilities.isEventDispatchThread()) {
			return func.invoke();
		}
		final AtomicReference<T> result = new AtomicReference<>();
		SwingUtilities.invokeAndWait(new Runnable() {
			@Override
			public void run() {
				result.set(func.invoke());
			}
		});
		return result.get();
	}
	/**
	 * Cleanup all resources.
	 */
	public void done() {
	}
	@Override
	public void save(SaveMode mode) {
		control.save(null, mode);
	}
	@Override
	public String version() {
		return Configuration.VERSION;
	}
	/**
	 * Play a given sound for buttons if the effect is enabled in options.
	 * @param type the sound type
	 */
	public void buttonSound(SoundType type) {
		playSound(SoundTarget.BUTTON, type, null);
	}
	/**
	 * Play a given sound if the computer notification voice is enabled.
	 * @param type the sound type
	 */
	public void computerSound(SoundType type) {
		playSound(SoundTarget.COMPUTER, type, null);
	}
	/**
	 * Play sound effects (space and ground wars).
	 * @param type the sound type
	 */
	public void effectSound(SoundType type) {
		playSound(SoundTarget.EFFECT, type, null);
	}
	/**
	 * Play a screen-switching related sound.
	 * @param type the sound type
	 */
	public void screenSound(SoundType type) {
		playSound(SoundTarget.SCREEN, type, null);
	}
	/**
	 * Retrieve a label translation.
	 * @param label the label
	 * @return the translation
	 */
	public String get(String label) {
		return labels().get(label);
	}
	/**
	 * Format a label with parameters.
	 * @param label the label
	 * @param params the parameters
	 * @return the translation
	 */
	public String format(String label, Object... params) {
		return labels().format(label, params);
	}
	/** @return the main player */
	public Player player() {
		return world().player;
	}
	/**
	 * Instruct the R/P screen to play the animation of the technology.
	 * @param rt the technology
	 */
	public void researchChanged(ResearchType rt) {
		Screens sb = control().secondary();
		if (sb == Screens.RESEARCH || sb == Screens.PRODUCTION) {
			((ResearchProductionAnimation)control().getScreen(sb)).playAnim(rt, true);
		}
		if (sb == Screens.EQUIPMENT) {
			EquipmentScreenAPI eq = (EquipmentScreenAPI)control().getScreen(Screens.EQUIPMENT);
			eq.onResearchChanged();
		}
	}
	@Override
	public Traits traits() {
		return traits;
	}
	@Override
	public boolean isLoading() {
		return worldLoading;
	}
	/**
	 * Start a new campaign game.
	 * @param game the game definition to load
	 */
	public void startGame(GameDefinition game) {
		// TODO refactor/implement
	}
	/**
	 * Start a new skirmish game.
	 * @param game the game definition to load
	 */
	public void startGame(SkirmishDefinition game) {
		// TODO refactor/implement
	}
	/**
	 * Start a new mulitplayer game.
	 * @param game the game to start
	 */
	public void startGame(MultiplayerDefinition game) {
		// TODO implement
	}
	/**
	 * Check if the mouse event is a panning event.
	 * @param e the event
	 * @return true if panning event
	 */
	public boolean isPanningEvent(UIMouse e) {
		return (e.has(Button.RIGHT) && !config.classicControls && !e.has(Modifier.CTRL))
				|| (e.has(Button.MIDDLE) && config.classicControls);
	}
	@Override
	public <T> Future<T> schedule(Callable<T> call) {
		return pool.submit(call);
	}
	@Override
	public Future<?> schedule(Runnable run) {
		return pool.submit(run);
	}
	/**
	 * Extract skirmishable players from the given definition.
	 * @param def the definition
	 * @return the set of players
	 */
	public List<SkirmishPlayer> getPlayersFrom(GameDefinition def) {
		List<SkirmishPlayer> result = new ArrayList<>();
		Set<SkirmishPlayer> rs = new HashSet<>();
		
		XElement xplayers = rl.getXML(def.players);
		
		Labels lbl = new Labels();
		lbl.load(rl, U.startWith(def.labels, "labels"));
		
		for (XElement xplayer : xplayers.childrenWithName("player")) {
			if (xplayer.getBoolean("noskirmish", false)) {
				continue;
			}
			SkirmishPlayer sp = new SkirmishPlayer();
			
			sp.originalId = xplayer.get("id");
			sp.race = xplayer.get("race");
			sp.name = lbl.get(xplayer.get("name") + ".short");
			sp.description = lbl.get(xplayer.get("name"));
			sp.iconRef = xplayer.get("icon");
			sp.icon = rl.getImage(sp.iconRef);
			sp.color = (int)Long.parseLong(xplayer.get("color"), 16);
			sp.nodatabase = xplayer.getBoolean("nodatabase", false);
			sp.nodiplomacy = xplayer.getBoolean("nodiplomacy", false);
			sp.diplomacyHead = xplayer.get("diplomacy-head", null);
			sp.picture = xplayer.get("picture", null);
			
			String ai = xplayer.get("ai", null);
			if (xplayer.getBoolean("user", false)) {
				sp.ai = SkirmishAIMode.USER;
			} else
			if ("TRADERS".equals(ai)) {
				sp.ai = SkirmishAIMode.TRADER;
			} else
			if ("PIRATES".equals(ai)) {
				sp.ai = SkirmishAIMode.PIRATE;
			} else {
				sp.ai = SkirmishAIMode.AI_NORMAL;
			}
			
			if (rs.add(sp)) {
				result.add(sp);
			}
		}
		
		return result;
	}
}