package net.fe;

import static org.lwjgl.opengl.GL11.GL_COLOR_BUFFER_BIT;
import static org.lwjgl.opengl.GL11.GL_DEPTH_BUFFER_BIT;
import static org.lwjgl.opengl.GL11.GL_LINE_SMOOTH;
import static org.lwjgl.opengl.GL11.GL_STENCIL_BUFFER_BIT;
import static org.lwjgl.opengl.GL11.glClear;
import static org.lwjgl.opengl.GL11.glClearDepth;
import static org.lwjgl.opengl.GL11.glEnable;
import static org.lwjgl.opengl.GL11.glPopMatrix;
import static org.lwjgl.opengl.GL11.glPushMatrix;

import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.ByteBuffer;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashMap;

import org.lwjgl.Sys;
import org.lwjgl.openal.AL;
import org.lwjgl.opengl.Display;
import org.newdawn.slick.Color;
import org.newdawn.slick.openal.SoundStore;
import org.newdawn.slick.opengl.TextureLoader;
import org.newdawn.slick.util.ResourceLoader;

import chu.engine.Game;
import chu.engine.Stage;
import chu.engine.menu.Notification;
import net.fe.builderStage.TeamDraftStage;
import net.fe.fightStage.CombatCalculator;
import net.fe.fightStage.FightStage;
import net.fe.lobbystage.ClientLobbyStage;
import net.fe.network.Client;
import net.fe.network.FEServer;
import net.fe.network.Message;
import net.fe.network.command.Command;
import net.fe.network.message.CommandMessage;
import net.fe.overworldStage.ClientOverworldStage;
import net.fe.overworldStage.ClientOverworldStage.FogType;
import net.fe.overworldStage.ClientOverworldStage.SpectatorFogOption;
import net.fe.overworldStage.objective.Seize;
import net.fe.rng.SimpleRNG;
import net.fe.rng.TrueHitRNG;
import net.fe.unit.Unit;
import net.fe.unit.UnitFactory;
import net.fe.unit.UnitIdentifier;
import net.fe.unit.WeaponFactory;

/**
 * Main class for the Clientside program.
 *
 * @author Shawn
 */
public class FEMultiplayer extends Game{
	
	/** The current stage. */
	private static Stage currentStage;
	
	/** The client. */
	private static Client client;
	
	/** The local player. */
	private static Player localPlayer;
	
	/** The lobby. */
	public static ClientLobbyStage lobby;
	
	/** The connecting stage. */
	public static ConnectStage connect;
	
	/** The test session, for testing fightstage. */
	private static Session testSession;
	
	private static final RunnableList postRenderRunnables = new RunnableList();

	
	/**
	 * The main method.
	 *
	 * @param args the arguments
	 */
	public static void main(String[] args) {
		try{
			FEMultiplayer game = new FEMultiplayer();
//			SoundTrack.enabled = false;
			game.init(480, 320, "Fire Emblem Multiplayer");
			/* Testing code */
//			game.testFightStage();
//			game.testOverworldStage();
//			game.testDraftStage();
			game.loop();
		} catch (Throwable e){
			System.err.println("Exception occurred, writing to logs...");
			e.printStackTrace();
			try{
				File errLog = new File("error_log_client" + LocalDateTime.now().toString().replace("T", "@").replace(":", "-") + ".log");
				PrintWriter pw = new PrintWriter(errLog);
				e.printStackTrace(pw);
				pw.close();
			}catch (IOException e2){
				e2.printStackTrace();
			}
			System.exit(0);
		}
	}
	
	
	
	/* (non-Javadoc)
	 * @see chu.engine.Game#init(int, int, java.lang.String)
	 */
	public void init(int width, int height, String name) {
		super.init(width, height, name);
		Player p1 = new Player("Player", (byte) 0);
		localPlayer = p1;
		ByteBuffer icon16, icon32;
		icon16 = icon32 = null;
		try {
			icon16 = ByteBuffer.wrap(TextureLoader.getTexture("PNG", ResourceLoader.getResourceAsStream("res/gui/icon16.png")).getTextureData());
			icon32 = ByteBuffer.wrap(TextureLoader.getTexture("PNG", ResourceLoader.getResourceAsStream("res/gui/icon32.png")).getTextureData());
		} catch (IOException e) {
			e.printStackTrace();
		}
		Display.setIcon(new ByteBuffer[]{icon16, icon32});
		FEResources.loadResources();
		FEResources.loadBitmapFonts();
		WeaponFactory.loadWeapons();
		UnitFactory.loadUnits();
		p1.getParty().setColor(Party.TEAM_BLUE);
		
		/* OpenGL final setup */
		glEnable(GL_LINE_SMOOTH);
		
		UnitFactory.getUnit("Lyn");
		connect = new ConnectStage();
		setCurrentStage(new TitleStage());
		SoundTrack.loop("main");
		
	}
	
	/**
	 * Test draft stage.
	 */
	public void testDraftStage() {
		Player p1 = localPlayer;
		testSession = new Session(new net.fe.overworldStage.objective.Rout(), "test", 6, new java.util.HashSet<>(), new net.fe.pick.Draft(), new TrueHitRNG(), new SimpleRNG(), new SimpleRNG(), FogType.NONE, SpectatorFogOption.REVEAL_ALL, 3, 8, false, true);
		Player p2 = new Player("p2", (byte) 1);
		Player p3 = new Player("p3", (byte) 2);
		p2.getParty().setColor(Party.TEAM_RED);
		p3.getParty().setColor(Party.TEAM_GREEN);
		p2.getParty().addUnit(UnitFactory.getUnit("Mia"));
		p2.getParty().addUnit(UnitFactory.getUnit("L'Arachel"));
		testSession.addPlayer(p1);
		testSession.addPlayer(p2);
		testSession.addPlayer(p3);
		currentStage = new TeamDraftStage(testSession);
	}
	
	/**
	 * Test fight stage.
	 * 
	 * Use debugStat on units to mess with fights, is also useful for testing animations. 
	 * Intentionally crashes at the end for lack of session info, so don't worry about it.
	 * 
	 * range can be set by messing with map.addUnit(), and you can change all the other 
	 * unit-related things to test them out. I can see us using this to use this bit for speed 
	 * testing in the future, if I ever make a gui or something for it.
	 * 
	 * TODO: gui for speed balancing
	 * 
	 */
	public void testFightStage(){
		Player p1 = localPlayer;
		testSession = new Session(new Seize(), "test", 8, new java.util.HashSet<>(), new net.fe.pick.Draft(), new TrueHitRNG(), new SimpleRNG(), new SimpleRNG(), FogType.NONE, SpectatorFogOption.REVEAL_ALL, 3, 8, false, true);
		Player p2 = new Player("p2", (byte) 1);
		p2.getParty().setColor(Party.TEAM_RED);
		p1.getParty().setColor(Party.TEAM_BLUE);
		p2.setTeam(2);
		p1.setTeam(1);
		
		testSession.addPlayer(p1);
		testSession.addPlayer(p2);
		
		final ClientOverworldStage map = new ClientOverworldStage(testSession);
		Unit u1 = UnitFactory.getUnit("Eirika");
		u1.getInventory().add(WeaponFactory.getWeapon("Silver Sword"));
		u1.equip(0);
		map.addUnit(u1, 0, 0);
		u1.setLevel(20);
		u1.loadMapSprites();
		p1.getParty().addUnit(u1);
		
		Unit u2 = UnitFactory.getUnit("Dart");
		u2.getInventory().add(WeaponFactory.getWeapon("Tomahawk"));
		map.addUnit(u2, 1, 0);
		u2.equip(0);
		u2.setLevel(1);
		u2.loadMapSprites();
		p2.getParty().addUnit(u2);
		
		map.processAddStack();
		int u2Uses = u2.getWeapon().getMaxUses();

		//u1.debugStat("Skl");
		//u1.debugStat("Str");
		
		// ^------- put all pre-calc stuff here
		
		CombatCalculator calc = new CombatCalculator(new UnitIdentifier(u1), new UnitIdentifier(u2), FEMultiplayer::getUnit, new TrueHitRNG(), new SimpleRNG(), new SimpleRNG());
		System.out.println(calc.getAttackQueue());
		
		
		u2.getWeapon().setUsesDEBUGGING(u2Uses);
		u1.fillHp();
		u2.fillHp();
		
		
		setCurrentStage(new FightStage(new UnitIdentifier(u1), new UnitIdentifier(u2), calc.getAttackQueue(), map, new EmptyRunnable()));
	}
	
	/**
	 * Test overworld stage.
	 */
	public void testOverworldStage() {
		testSession = new Session(new Seize(), "test", 8, new java.util.HashSet<>(), new net.fe.pick.Draft(),
				new TrueHitRNG(), new SimpleRNG(), new SimpleRNG(),
				FogType.SNES, SpectatorFogOption.REVEAL_ALL, 3, 8, false, false);
		testSession.addPlayer(localPlayer);
		
		Player p2 = new Player("P2", (byte)1);
		p2.getParty().setColor(Party.TEAM_RED);
		testSession.addPlayer(p2);
		localPlayer.getParty().setColor(Party.TEAM_BLUE);
		p2.setTeam(2);
		localPlayer.setTeam(1);
		
		Unit u1 = UnitFactory.getUnit("Natasha");
		u1.addToInventory(WeaponFactory.getWeapon("Physic"));
		u1.setHp(1);
		localPlayer.getParty().addUnit(u1);
		
		Unit u3 = UnitFactory.getUnit("Oswin");
		u3.addToInventory(WeaponFactory.getWeapon("Silver Axe"));
		u3.equip(0);
		u3.setHp(1);
		p2.getParty().addUnit(u3);
		
		Unit u4 = UnitFactory.getUnit("Eirika");
		u4.addToInventory(WeaponFactory.getWeapon("Silver Sword"));
		u4.equip(0);
		u4.setHp(1);
		p2.getParty().addUnit(u4);
		
		Unit u2 = UnitFactory.getUnit("Lute");
		u2.addToInventory(WeaponFactory.getWeapon("Physic"));
		u2.setHp(1);
		localPlayer.getParty().addUnit(u2);
		
		currentStage = new ClientOverworldStage(testSession);

		client = new Client("nope", 12345) {
			@Override
			public void sendMessage(Message message) {
				if (message instanceof CommandMessage) {
					((ClientOverworldStage) currentStage).processCommands((CommandMessage) message);
				}
			}
		};
	}
	
	/**
	 * Gets the unit.
	 *
	 * @param id the Unit Identifier
	 * @return the unit
	 */
	public static Unit getUnit(UnitIdentifier id){
		for(Player p : getPlayers().values()){
			if(!p.isSpectator() && p.getParty().getColor().equals(id.partyColor)){
				return p.getParty().search(id.name);
			}
		}
		return null;
	}
	
	/**
	 * Connect.
	 *
	 * @param nickname player nickname
	 * @param ip the host ip
	 * @param port the host port
	 */
	public static void connect(String nickname, String ip, int port) {
		new Thread(() -> {
			getLocalPlayer().setName(nickname);
			client = new Client(ip, port);
			if(client.isOpen()) {
				postRenderRunnables.add(() -> {
					lobby = new ClientLobbyStage(client.getSession());
					setCurrentStage(lobby);
					client.start();
				});
			} else {
				currentStage.addEntity(new Notification(
						180, 120, "default_med", "ERROR: Could not connect to the server!", 5f, new Color(255, 100, 100), 0f));
		}}).start();
	}

	/* (non-Javadoc)
	 * @see chu.engine.Game#loop()
	 */
	@Override
	public void loop() {
		while(!Display.isCloseRequested()) {
			final long time = System.nanoTime();
			glClear(GL_COLOR_BUFFER_BIT |
			        GL_DEPTH_BUFFER_BIT |
			        GL_STENCIL_BUFFER_BIT);
			glClearDepth(1.0f);
			getInput();
			final ArrayList<Message> messages = new ArrayList<>();
			if(client != null){
				synchronized (client.messagesLock) {
					messages.addAll(client.messages);
					for(Message m : messages)
						client.messages.remove(m);
				}
			}
			SoundStore.get().poll(0);
			glPushMatrix();
			//Global resolution scale
//			Renderer.scale(scaleX, scaleY);
				currentStage.beginStep(messages);
				currentStage.onStep();
				currentStage.processAddStack();
				currentStage.processRemoveStack();
				currentStage.render();
//				FEResources.getBitmapFont("stat_numbers").render(
//						(int)(1.0f/getDeltaSeconds())+"", 440f, 0f, 0f);
				currentStage.endStep();
				postRenderRunnables.runAll();
			glPopMatrix();
			Display.update();
			Display.sync(FEResources.getTargetFPS());
			timeDelta = System.nanoTime()-time;
		}
		AL.destroy();
		Display.destroy();
		if(client != null && client.isOpen()) client.quit();
	}
	
	/**
	 * Send. Used by the game to tell the unit on either client to attempt an action.
	 *
	 * @param u the unit Identifier
	 * @param moveX the x-wards movement
	 * @param moveY the y-wards movement
	 * @param cmds the commands for the unit
	 */
	public static void send(UnitIdentifier u, Command... cmds){
		for(Object o: cmds){
			System.out.print(o + " ");
		}
		System.out.println();
		client.sendMessage(new CommandMessage(u, null, cmds));
	}
	
	/**
	 * Sets the current stage.
	 *
	 * @param stage the new current stage
	 */
	public static void setCurrentStage(Stage stage) {
		currentStage = stage;
		if(stage.soundTrack != null){
			SoundTrack.loop(stage.soundTrack);
		}
	}
	
	/**
	 * Gets the client.
	 *
	 * @return the client
	 */
	public static Client getClient() {
		return client;
	}

	/**
	 * Gets the current stage.
	 *
	 * @return the current stage
	 */
	public static Stage getCurrentStage() {
		return currentStage;
	}

	/**
	 * Sets the local player.
	 *
	 * @param p the new local player
	 */
	public static void setLocalPlayer(Player p) {
		localPlayer = p;
	}
	
	/**
	 * Gets the local player.
	 *
	 * @return the local player
	 */
	public static Player getLocalPlayer() {
		return localPlayer;
	}
	
	/**
	 * Gets the players.
	 *
	 * @return the players
	 */
	public static HashMap<Byte, Player> getPlayers() {
		return getSession().getPlayerMap();
	}
	
	/**
	 * Gets the session.
	 *
	 * @return the session
	 */
	public static Session getSession() {
		if(testSession != null) //Test session is only set by test stages.
			return testSession;
		return client.getSession();
	}
	
	/**
	 * Disconnect from game. 
	 * Allows for resetting server and client if triggered, but is not used in all situations.
	 *
	 * @param message the message
	 */
	public static void disconnectGame(String message){
		/*
		//wouldn't be hard to use something like this to reset to lobby rather than quit the game:
		//at the moment this disconnect is only in a few places between stages, i.e. while waiting
		//so it's not too bad to quit the game.
		Player leaver = null;
		for(Player p : session.getPlayers()) {
			if(p.getID() == message.origin) {
				leaver = p;
			}
		}
		session.removePlayer(leaver);
		System.out.println(leaver.getName()+" LEFT THE GAME");
		 * */
		if(FEServer.getServer() != null) {
			//boot the server back to lobby
			FEServer.resetToLobbyAndKickPlayers();
		}else{
			//exit the client
			if(message!=null && !message.equals("")){
				Sys.alert("FE:MP", message);
			}
			System.exit(0);
		}
	}

	private static final class EmptyRunnable implements Runnable {
		@Override public void run() {}
	}
	
	private static class RunnableList {
		
		private final ArrayList<Runnable> runnables = new ArrayList<Runnable>();
		
		
		public synchronized void runAll() {
			runnables.forEach(r -> r.run());
			runnables.clear();
		}
		
		public synchronized void add(Runnable runnable) {
			runnables.add(runnable);
		}
		
	}
}