package core.game;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Random;


import core.logging.Logger;
import core.logging.Message;

import core.vgdl.VGDLRegistry;
import ontology.Types;
import ontology.avatar.MovingAvatar;

/**
 * This is an abstract class encapsulating all the data required for generating
 * and testing game levels.
 *
 * @author Ahmed A Khalifa
 */
public class GameDescription {

	/**
	 * object from the current loaded game. This object is used to initialize
	 * other fields and get interaction data.
	 */
	private Game currentGame;

	/**
	 * abstract data about main avatar
	 */
	private ArrayList<SpriteData> avatar;

	/**
	 * list of all allowed actions by the avatar including the NIL action
	 */
	private ArrayList<Types.ACTIONS> actionsNIL;

	/**
	 * list of all allowed actions by the avatar
	 */
	private ArrayList<Types.ACTIONS> actions;

	/**
	 * list of sprite data for all npc labeled objects
	 */
	private ArrayList<SpriteData> npcList;

	/**
	 * list of sprite data for all portal labeled objects
	 */
	private ArrayList<SpriteData> portalList;

	/**
	 * list of sprite data for all resource labeled objects
	 */
	private ArrayList<SpriteData> resourceList;

	/**
	 * list of sprite data for all static labeled objects
	 */
	private ArrayList<SpriteData> staticList;

	/**
	 * list of sprite data that is not labeled any of the previous
	 */
	private ArrayList<SpriteData> movingList;

	/**
	 * list of termination data objects that supplies game termination
	 * conditions
	 */
	private ArrayList<TerminationData> terminationData;

	/**
	 *
	 */
	private HashMap<Character, ArrayList<String>> charMapping;

	/**
	 * Constructor to the Game Description. It initialize all the data using the
	 * passed game object.
	 *
	 * @param currentGame
	 *            The current running game object.
	 */
	public GameDescription(Game currentGame) {
		this.currentGame = currentGame;
		this.avatar = new ArrayList<SpriteData>();
		this.npcList = new ArrayList<SpriteData>();
		this.portalList = new ArrayList<SpriteData>();
		this.resourceList = new ArrayList<SpriteData>();
		this.staticList = new ArrayList<SpriteData>();
		this.movingList = new ArrayList<SpriteData>();
		this.charMapping = currentGame.getCharMapping();

		reset(currentGame);
	}

	private boolean checkHaveInteraction(String stype) {
		ArrayList<SpriteData> allSprites = currentGame.getSpriteData();
		for (SpriteData sprite : allSprites) {
			if (getInteraction(stype, sprite.name).size() > 0) {
				return true;
			}
			if (getInteraction(sprite.name, stype).size() > 0) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Reset the game description object and assign new game object
	 *
	 * @param currentGame
	 *            new game object assigned
	 */
	public void reset(Game currentGame) {
		this.currentGame = currentGame;
		this.avatar.clear();
		this.npcList.clear();
		this.portalList.clear();
		this.resourceList.clear();
		this.staticList.clear();
		this.movingList.clear();
		this.charMapping = currentGame.getCharMapping();

		ArrayList<SpriteData> allSprites = this.currentGame.getSpriteData();
		for (SpriteData sd : allSprites) {
			if (sd.isAvatar) {
				if (checkHaveInteraction(sd.name)) {
					avatar.add(sd);
				}
			} else if (sd.isNPC) {
				npcList.add(sd);
			} else if (sd.isPortal) {
				portalList.add(sd);
			} else if (sd.isResource) {
				resourceList.add(sd);
			} else if (sd.isStatic) {
				staticList.add(sd);
			} else {
				movingList.add(sd);
			}
		}

		for (int i = 0; i < avatar.size(); i++) {
			MovingAvatar temp = (MovingAvatar) currentGame.getTempAvatar(avatar.get(i));
			if (actions == null || actions.size() < temp.actions.size()) {
				actionsNIL = temp.actionsNIL;
				actions = temp.actions;
			}
		}

		terminationData = currentGame.getTerminationData();
	}

	/**
	 * Build the generated level to be tested using an agent using the original
	 * Level Mapping.
	 *
	 * @param level
	 *            a string of characters that are supplied in the character
	 *            mapping
	 * @return StateObservation object that can be used to simulate the game.
	 * 				return null when there is errors
	 */
	public StateObservation testLevel(String level) {
		return testLevel(level, null);
	}

	/**
	 * Build the generated level to be tested using an agent. You should call
	 * this version if you are using your own character mapping
	 *
	 * @param level
	 *            a string of characters that are supplied in the character
	 *            mapping
	 * @return StateObservation object that can be used to simulate the game.
	 * 				return null when there is errors
	 */
	public StateObservation testLevel(String level, HashMap<Character, ArrayList<String>> charMapping) {
		Logger.getInstance().flushMessages();

		if (charMapping != null) {
			currentGame.setCharMapping(charMapping);
		}
		String[] lines = level.split("\n");
		currentGame.reset();
		currentGame.buildStringLevel(lines, new Random().nextInt());
		currentGame.setCharMapping(this.charMapping);

		if(Logger.getInstance().getMessageCount(1) > 0){
			return null;
		}
		return currentGame.getObservation();
	}

	/**
	 * Disable/Enable the logger
	 * @param value		enable or disable
	 */
	public void enableLogger(boolean value){
	    Logger.getInstance().active = value;
	}
	
	/**
	     * get list of errors from the system
	     * 
	     * @return a list of errors
	     */
	    public ArrayList<Message> getErrors() {
		return Logger.getInstance().getMessages(Message.ERROR);
	    }
	    
	    /**
	     * get list of warnings from the system
	     * 
	     * @return a list of warning
	     */
	    public ArrayList<Message> getWarnings() {
		return Logger.getInstance().getMessages(Message.WARNING);
	    }

	/**
	 * Get player supported actions
	 *
	 * @param includeNIL
	 *            boolean to identify if the NIL action should exists in the
	 *            supported actions
	 * @return list of all player supported actions
	 */
	public ArrayList<Types.ACTIONS> getAvailableActions(boolean includeNIL) {
		if (includeNIL) {
			return actionsNIL;
		}

		return actions;
	}

	/**
	 * Get avatar sprite data information
	 *
	 * @return avatar's sprite data
	 */
	public ArrayList<SpriteData> getAvatar() {
		return avatar;
	}

	/**
	 * Get NPCs sprite data information
	 *
	 * @return array of sprite data
	 */
	public ArrayList<SpriteData> getNPC() {
		return npcList;
	}

	/**
	 * Get Statics sprite data information
	 *
	 * @return array of sprite data
	 */
	public ArrayList<SpriteData> getStatic() {
		return staticList;
	}

	/**
	 * Get Resources sprite data information
	 *
	 * @return array of sprite data
	 */
	public ArrayList<SpriteData> getResource() {
		return resourceList;
	}

	/**
	 * Get Portals sprite data information
	 *
	 * @return array of sprite data
	 */
	public ArrayList<SpriteData> getPortal() {
		return portalList;
	}

	/**
	 * Get all movable sprites
	 *
	 * @return arary of sprite data
	 */
	public ArrayList<SpriteData> getMoving() {
		return movingList;
	}

	/**
	 * Get all defined game sprites
	 *
	 * @return an array of sprite data
	 */
	public ArrayList<SpriteData> getAllSpriteData() {
		ArrayList<SpriteData> result = new ArrayList<SpriteData>();
		result.addAll(avatar);
		result.addAll(npcList);
		result.addAll(resourceList);
		result.addAll(staticList);
		result.addAll(portalList);
		result.addAll(movingList);

		return result;
	}

	/**
	 * Get a list of all effects happening to the first sprite
	 *
	 * @param stype1
	 *            the sprite name of the first sprite in the collision
	 * @param stype2
	 *            the sprite name of the second sprite in the collision
	 * @return an array of all possible effects. If there is no effects, an
	 *         empty array is returned
	 */
	public ArrayList<GameDescription.InteractionData> getInteraction(String stype1, String stype2) {
		int itype1 = VGDLRegistry.GetInstance().getRegisteredSpriteValue(stype1);
		int itype2 = VGDLRegistry.GetInstance().getRegisteredSpriteValue(stype2);

		return currentGame.getInteractionData(itype1, itype2);
	}

	/**
	 * Get a list of all termination conditions for the current game
	 *
	 * @return an array of termination data objects
	 */
	public ArrayList<GameDescription.TerminationData> getTerminationConditions() {
		return terminationData;
	}

	/**
	 * Get default character mapping
	 *
	 * @return hashmap of level characters and their corresponding sprites
	 */
	public HashMap<Character, ArrayList<String>> getLevelMapping() {
		return charMapping;
	}

	/**
	 * Simple data class represents all game sprites
	 */
	public static class SpriteData implements Cloneable {
		private HashMap<String, String> parameters;

		/**
		 * VGDL class type for the current sprite
		 */
		public String type;

		/**
		 * Sprite name
		 */
		public String name;

		/**
		 * List of all dependent sprite names
		 */
		public ArrayList<String> sprites;
		
		/**
		 * List of all the parents till the top of the hierarchy
		 */
		public ArrayList<String> parents;

		public boolean isSingleton;

		public boolean isAvatar;
		public boolean isNPC;
		public boolean isPortal;
		public boolean isResource;
		public boolean isStatic;

		public SpriteData(HashMap<String, String> parameters) {
			this.sprites = new ArrayList<String>();
			this.parents = new ArrayList<String>();
			this.parameters = new HashMap<String, String>();
			for(String key:parameters.keySet()){
			    this.parameters.put(key, parameters.get(key));
			}
		}

		@Override
		public String toString() {
			String reset = "";
			for(String key:parameters.keySet()){
				reset += " " + key + "=" + parameters.get(key);
			}
			return name + " > " + type + " " + reset;
		}

		/**
		 * Used in encode the names sprites
		 * @param oldName	the current name for the sprite
		 * @param newName	the new name for the sprite
		 */
		public void changeSpriteName(String oldName, String newName){
			if(name.equalsIgnoreCase(oldName)){
				name = newName;
			}
			for(int i=0; i<sprites.size(); i++){
				if(sprites.get(i).equalsIgnoreCase(oldName)){
					sprites.set(i, newName);
				}
			}
			for(String key:parameters.keySet()){
				if(parameters.get(key).equalsIgnoreCase(oldName)){
					parameters.put(key, newName);
				}
			}
		}

		@Override
		protected Object clone() throws CloneNotSupportedException {
			SpriteData s = new SpriteData(this.parameters);
			s.type = type;
			s.name = name;
			for (int i = 0; i < sprites.size(); i++) {
				s.sprites.add(sprites.get(i));
			}
			s.isSingleton = isSingleton;
			s.isAvatar = isAvatar;
			s.isNPC = isNPC;
			s.isPortal = isPortal;
			s.isResource = isResource;
			s.isStatic = isStatic;

			return s;
		}
	}

	/**
	 * Simple data class represents all game termination conditions
	 */
	public static class TerminationData {
		/**
		 * Termination Condition type
		 */
		public String type;

		/**
		 * Array of all dependent sprite names
		 */
		public ArrayList<String> sprites;

		/**
		 * Condition Limit
		 */
		public int limit;

		/**
		 * String containing booleans to differentiate between Winning or Losing
		 * Condition
		 */
		public String win;

		public TerminationData() {
			sprites = new ArrayList<String>();
		}

		/**
		 * Player ID for win state used is 0, default for single player games.
		 *
		 * @return
		 */
		@Override
		public String toString() {
			String[] winners = win.split(",");
			String result = Boolean.parseBoolean(winners[0]) ? "Win" : "Lose";
			result += ":" + type + " " + sprites.toString();
			return result;
		}
	}

	/**
	 * Simple data class represents the interaction between game sprites
	 */
	public static class InteractionData {
		/**
		 * Interaction class type
		 */
		public String type;

		/**
		 * The amount of score this interaction changes
		 */
		public String scoreChange;

		/**
		 * All the depending sprites on that
		 */
		public ArrayList<String> sprites;

		public InteractionData() {
			sprites = new ArrayList<String>();
		}

		@Override
		public String toString() {
			return type + " (" + scoreChange + ") " + sprites.toString();
		}
	}
}
