package minicraft.saveload;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import minicraft.core.Game;
import minicraft.core.Renderer;
import minicraft.core.Updater;
import minicraft.core.World;
import minicraft.core.io.Localization;
import minicraft.core.io.Settings;
import minicraft.entity.Arrow;
import minicraft.entity.Entity;
import minicraft.entity.ItemEntity;
import minicraft.entity.Spark;
import minicraft.entity.furniture.Chest;
import minicraft.entity.furniture.Crafter;
import minicraft.entity.furniture.DeathChest;
import minicraft.entity.furniture.DungeonChest;
import minicraft.entity.furniture.Lantern;
import minicraft.entity.furniture.Spawner;
import minicraft.entity.mob.AirWizard;
import minicraft.entity.mob.EnemyMob;
import minicraft.entity.mob.Mob;
import minicraft.entity.mob.Player;
import minicraft.entity.mob.RemotePlayer;
import minicraft.entity.particle.Particle;
import minicraft.entity.particle.TextParticle;
import minicraft.item.*;
import minicraft.network.MinicraftServer;
import minicraft.screen.LoadingDisplay;
import minicraft.screen.MultiplayerDisplay;
import minicraft.screen.WorldSelectDisplay;

public class Save {
	
	public String location = Game.gameDir;
	File folder;
	
	public static String extension = ".miniplussave";
	
	List<String> data;
	Game game;
	
	private Save(File worldFolder) {
		data = new ArrayList<>();
		
		
		if(worldFolder.getParent().equals("saves")) {
			String worldName = worldFolder.getName();
			if (!worldName.toLowerCase().equals(worldName)) {
				if (Game.debug) System.out.println("Renaming world in " + worldFolder + " to lowercase");
				String path = worldFolder.toString();
				path = path.substring(0, path.lastIndexOf(worldName));
				File newFolder = new File(path + worldName.toLowerCase());
				if (worldFolder.renameTo(newFolder))
					worldFolder = newFolder;
				else
					System.err.println("Failed to rename world folder " + worldFolder + " to " + newFolder);
			}
		}
		
		folder = worldFolder;
		location = worldFolder.getPath() + "/";
		
		folder.mkdirs();
	}
	
	/// this saves world options
	public Save(String worldname) {
		this(new File(Game.gameDir+"/saves/" + worldname + "/"));
		
		if(Game.isValidClient()) {
			// clients are not allowed to save.
			Updater.saving = false;
			return;
		}
		
		writeGame("Game");
		writeWorld("Level");
		if(!Game.isValidServer()) { // this must be waited for on a server.
			writePlayer("Player", Game.player);
			writeInventory("Inventory", Game.player);
		}
		writeEntities("Entities");
		
		WorldSelectDisplay.refreshWorldNames();
		
		Updater.notifyAll("World Saved!");
		Updater.asTick = 0;
		Updater.saving = false;
	}
	
	/// this saves server config options
	public Save(String worldname, MinicraftServer server) {
		this(new File(Game.gameDir+"/saves/" + worldname + "/"));
		
		if (Game.debug) System.out.println("Writing server config...");
		writeServerConfig("ServerConfig", server);
	}
	
	// this saves global options
	public Save() {
		this(new File(Game.gameDir+"/"));
		if(Game.debug) System.out.println("Writing preferences and unlocks...");
		writePrefs();
	}
	
	public Save(Player player, boolean writePlayer) {
		// this is simply for access to writeToFile.
		this(new File(Game.gameDir+"/saves/"+ WorldSelectDisplay.getWorldName() + "/"));
		if(writePlayer) {
			writePlayer("Player", player);
			writeInventory("Inventory", player);
		}
	}
	
	public static void writeFile(String filename, String[] lines) throws IOException {
		try (BufferedWriter br = new BufferedWriter(new FileWriter(filename))) {
			br.write(String.join(System.lineSeparator(), lines));
		}
	}
	
	public void writeToFile(String filename, List<String> savedata) {
		try {
			writeToFile(filename, savedata.toArray(new String[0]), true);
		} catch(IOException ex) {
			ex.printStackTrace();
		}
		
		data.clear();
		
		LoadingDisplay.progress(7);
		if(LoadingDisplay.getPercentage() > 100) {
			LoadingDisplay.setPercentage(100);
		}
		
		Renderer.render(); // AH HA!!! HERE'S AN IMPORTANT STATEMENT!!!!
	}
	
	public static void writeToFile(String filename, String[] savedata, boolean isWorldSave) throws IOException {
		try (BufferedWriter bufferedWriter = new BufferedWriter(new FileWriter(filename))) {
			for(int i = 0; i < savedata.length; i++) {
				bufferedWriter.write(savedata[i]);
				if(isWorldSave) {
					bufferedWriter.write(",");
					if(filename.contains("Level5") && i == savedata.length - 1) {
						bufferedWriter.write(",");
					}
				} else
					bufferedWriter.write("\n");
			}
		}
	}
	
	private void writeGame(String filename) {
		data.add(String.valueOf(Game.VERSION));
		data.add(Settings.getIdx("mode") + (Game.isMode("score")?";"+Updater.scoreTime+";"+Settings.get("scoretime"):""));
		data.add(String.valueOf(Updater.tickCount));
		data.add(String.valueOf(Updater.gameTime));
		data.add(String.valueOf(Settings.getIdx("diff")));
		data.add(String.valueOf(AirWizard.beaten));
		writeToFile(location + filename + extension, data);
	}
	
	private void writePrefs() {
		data.add(String.valueOf(Game.VERSION));
		data.add(String.valueOf(Settings.get("sound")));
		data.add(String.valueOf(Settings.get("autosave")));
		data.add(String.valueOf(Settings.get("fps")));
		data.add(MultiplayerDisplay.savedIP);
		data.add(MultiplayerDisplay.savedUUID);
		data.add(MultiplayerDisplay.savedUsername);
		data.add(Localization.getSelectedLanguage());
		
		List<String> keyPairs = new ArrayList<>();
		Collections.addAll(keyPairs, Game.input.getKeyPrefs());
		
		data.add(String.join(":", keyPairs.toArray(new String[keyPairs.size()])));
		
		writeToFile(location + "Preferences" + extension, data);
		
		if((boolean)Settings.get("unlockedskin"))
			data.add("AirSkin");
		
		if(Settings.getEntry("scoretime").getValueVisibility(10))
			data.add("10_ScoreTime");
		if(Settings.getEntry("scoretime").getValueVisibility(120))
			data.add("120_ScoreTime");
		
		writeToFile(location + "Unlocks" + extension, data);
	}
	
	private void writeServerConfig(String filename, MinicraftServer server) {
		data.add(String.valueOf(server.getPlayerCap()));
		
		writeToFile(location + filename + extension, data);
	}
	
	private void writeWorld(String filename) {
		LoadingDisplay.setMessage("Levels");
		for(int l = 0; l < World.levels.length; l++) {
			String worldSize = String.valueOf(Settings.get("size"));
			data.add(worldSize);
			data.add(worldSize);
			data.add(String.valueOf(World.levels[l].depth));
			
			for(int x = 0; x < World.levels[l].w; x++) {
				for(int y = 0; y < World.levels[l].h; y++) {
					data.add(String.valueOf(World.levels[l].getTile(x, y).name));
				}
			}
			
			writeToFile(location + filename + l + extension, data);
		}
		
		for(int l = 0; l < World.levels.length; l++) {
			for(int x = 0; x < World.levels[l].w; x++) {
				for(int y = 0; y < World.levels[l].h; y++) {
					data.add(String.valueOf(World.levels[l].getData(x, y)));
				}
			}
			
			writeToFile(location + filename + l + "data" + extension, data);
		}
		
	}
	
	private void writePlayer(String filename, Player player) {
		LoadingDisplay.setMessage("Player");
		writePlayer(player, data);
		writeToFile(location + filename + extension, data);
	}
	
	public static void writePlayer(Player player, List<String> data) {
		data.clear();
		data.add(String.valueOf(player.x));
		data.add(String.valueOf(player.y));
		data.add(String.valueOf(player.spawnx));
		data.add(String.valueOf(player.spawny));
		data.add(String.valueOf(player.health));
		data.add(String.valueOf(player.hunger));
		data.add(String.valueOf(player.armor));
		data.add(String.valueOf(player.armorDamageBuffer));
		data.add(String.valueOf(player.curArmor == null ? "NULL" : player.curArmor.getName()));
		data.add(String.valueOf(player.getScore()));
		data.add(String.valueOf(Game.currentLevel));
		
		StringBuilder subdata = new StringBuilder("PotionEffects[");
		
		for(java.util.Map.Entry<PotionType, Integer> potion: player.potioneffects.entrySet())
			subdata.append(potion.getKey()).append(";").append(potion.getValue()).append(":");
		
		if(player.potioneffects.size() > 0)
			subdata = new StringBuilder(subdata.substring(0, subdata.length() - (1)) + "]"); // cuts off extra ":" and appends "]"
		else subdata.append("]");
		data.add(subdata.toString());
		
		data.add(String.valueOf(player.shirtColor));
		data.add(String.valueOf(player.skinon));
	}
	
	private void writeInventory(String filename, Player player) {
		writeInventory(player, data);
		writeToFile(location + filename + extension, data);
	}
	public static void writeInventory(Player player, List<String> data) {
		data.clear();
		if(player.activeItem != null) {
			data.add(player.activeItem.getData());
		}
		
		Inventory inventory = player.getInventory();
		
		for(int i = 0; i < inventory.invSize(); i++) {
			data.add(inventory.get(i).getData());
		}
	}
	
	private void writeEntities(String filename) {
		LoadingDisplay.setMessage("Entities");
		for(int l = 0; l < World.levels.length; l++) {
			for(Entity e: World.levels[l].getEntitiesToSave()) {
				String saved = writeEntity(e, true);
				if(saved.length() > 0)
					data.add(saved);
			}
		}
		
		writeToFile(location + filename + extension, data);
	}
	
	public static String writeEntity(Entity e, boolean isLocalSave) {
		String name = e.getClass().getName();
		name = name.substring(name.lastIndexOf('.')+1);
		StringBuilder extradata = new StringBuilder();
		
		// don't even write ItemEntities or particle effects; Spark... will probably is saved, eventually; it presents an unfair cheat to remove the sparks by reloading the Game.
		
		//if(e instanceof Particle) return ""; // TODO I don't want to, but there are complications.
		
		if(isLocalSave && (e instanceof ItemEntity || e instanceof Arrow || e instanceof RemotePlayer || e instanceof Spark || e instanceof Particle)) // wirte these only when sending a world, not writing it. (RemotePlayers are saved separately, when their info is received.)
			return "";
		
		if(!isLocalSave)
			extradata.append(":").append(e.eid);
		
		if(!isLocalSave && e instanceof RemotePlayer) {
			RemotePlayer rp = (RemotePlayer)e;
			extradata.append(":").append(rp.getData());
		} // the "else" part is so that remote player, which is a mob, doesn't get the health thing.
		else if(e instanceof Mob) {
			Mob m = (Mob)e;
			extradata.append(":").append(m.health);
			if(e instanceof EnemyMob)
				extradata.append(":").append(((EnemyMob) m).lvl);
		}
		
		if(e instanceof Chest) {
			Chest chest = (Chest)e;
			
			for(int ii = 0; ii < chest.getInventory().invSize(); ii++) {
				Item item = chest.getInventory().get(ii);
				extradata.append(":").append(item.getData());
			}
			
			if(chest instanceof DeathChest) extradata.append(":").append(((DeathChest) chest).time);
			if(chest instanceof DungeonChest) extradata.append(":").append(((DungeonChest) chest).isLocked);
		}
		
		if(e instanceof Spawner) {
			Spawner egg = (Spawner)e;
			String mobname = egg.mob.getClass().getName();
			mobname = mobname.substring(mobname.lastIndexOf(".")+1);
			extradata.append(":").append(mobname).append(":").append(egg.mob instanceof EnemyMob ? ((EnemyMob) egg.mob).lvl : 1);
		}
		
		if (e instanceof Lantern) {
			extradata.append(":").append(((Lantern) e).type.ordinal());
		}
		
		if (e instanceof Crafter) {
			name = ((Crafter)e).type.name();
		}
		
		if (!isLocalSave) {
			if(e instanceof ItemEntity) extradata.append(":").append(((ItemEntity) e).getData());
			if(e instanceof Arrow) extradata.append(":").append(((Arrow) e).getData());
			if(e instanceof Spark) extradata.append(":").append(((Spark) e).getData());
			if(e instanceof TextParticle) extradata.append(":").append(((TextParticle) e).getData());
		}
		//else // is a local save
		
		int depth = 0;
		if(e.getLevel() == null)
			System.out.println("WARNING: Saving entity with no level reference: " + e + "; setting level to surface");
		else
			depth = e.getLevel().depth;
		
		extradata.append(":").append(World.lvlIdx(depth));
		
		return name + "[" + e.x + ":" + e.y + extradata + "]";
	}
}