package com.HawkEngine.Core;

import java.awt.*;
import java.awt.event.MouseEvent;
import java.awt.image.BufferStrategy;
import java.awt.image.BufferedImage;
import java.io.*;
import java.util.*;
import java.util.Timer;
import javax.swing.*;
import org.w3c.dom.Document;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.jbox2d.common.Vec2;
import org.jbox2d.dynamics.World;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import sun.awt.SunToolkit;

public class Game extends JPanel
{
	JFrame frame;
	static Game inst;
	public static World world;
	BufferStrategy strategy;
	BufferedImage icon;

	static AudioPool audioPool = null;
	static TexturePool texturePool = null;

	public static AudioPool GetAudioPool()
	{
		if(audioPool == null)
		{
			audioPool = new AudioPool();
		}
		return audioPool;
	}
	public static TexturePool GetTexturePool()
	{
		if(texturePool == null)
		{
			texturePool = new TexturePool();
		}
		return texturePool;
	}

	Hierarchy hierarchy;
	public static Camera camera = new Camera(new Vector2(0, 0));



	//TODO: create an ini file
	public static boolean on = false;

	public static Game getInst() {
		if(inst == null)
		{
			inst = new Game();
		}
		return inst;
	}

	public Game() {
		inst = this;
		this.frame = new JFrame("Game");
		this.frame.setSize(new Dimension(Core.GAME_WIDTH, Core.GAME_HEIGHT));
		//this.setBorder(null);
		this.frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		this.frame.setVisible(true);
		this.frame.add(this);
		this.setSize(new Dimension(Core.GAME_WIDTH, Core.GAME_HEIGHT));
		this.frame.setResizable(false);
		//this.frame.setIcon();
		this.init();
	}

	synchronized void init()
	{
		KeyboardManager.init();
		this.hierarchy = new Hierarchy();
		this.world = new World(new Vec2(0, 0.1f));

		this.GetAudioPool().Link("click", "Resources/click.wav");

		this.texturePool = new TexturePool();

		this.texturePool.Link("box", "Resources/box.png");
		this.texturePool.Link("test", "Resources/test.png");

		Game.world.setAllowSleep(true);
		Game.world.setContactListener(new WorldContactListener());

		KeyboardManager keyboardManager = new KeyboardManager();
		this.addKeyListener(keyboardManager);
		this.addMouseListener(new MouseManager());
		this.addMouseMotionListener(new MouseMotionController());

		new Timer().scheduleAtFixedRate(new TimerTask() {

			@Override
			public void run() {
				Run();
			}
		}, 0, (long) ((float) 1 / (float) Core.GAME_FPS * 1000));

		this.SpawnObject(new Ground(new Vector2(0, Core.GAME_HEIGHT - 100), new Vector2(20000, 25)));

		if(this.texturePool.Get("box") == null)
		{
			try
			{
				throw new FileNotFoundException("Resources/box.png Not Found!");
			}
			catch(Exception e)
			{
				System.err.println("An error was made while trying to show an error message!");
			}
		}

		//this.SpawnObject(new Sprite(new Vector2(100, 50), new Vector2(25, 25), true, this.texturePool.Get("box")));

		this.setIgnoreRepaint(true);

		this.frame.createBufferStrategy(2);
		strategy = this.frame.getBufferStrategy();
	}

	synchronized void Run() {
		if (Game.on)
		{
			Game.world.step((float) 1 / 16, 8, 3);
			update();
		}
		do
		{
			do
			{
				Graphics graphics = strategy.getDrawGraphics();
				render(graphics);
				graphics.dispose();
			}
			while (strategy.contentsRestored());
			strategy.show();
		}
		while (strategy.contentsLost());
	}

	public synchronized void update()
    {
//		System.out.println(this.objects.size());
		for (Object go : new ArrayList(this.hierarchy.GetObjects()).toArray()) {
			GameObject gameObject = (GameObject) go;
			gameObject.Update();
		}
		//System.out.println("Is W being pressed? " + String.valueOf(KeyboardManager.KeyPressed('W')));
		this.grabFocus();
		this.MoveCamera();
		/*
		Box toSpawn = new Box(new Vector2(Core.GAME_WIDTH/2, Core.GAME_HEIGHT/4 ), new Vector2(10,10), true);
		toSpawn.Init();
			
		this.objects.add(toSpawn);
		*/
	}

	void MoveCamera()
	{
		if(KeyboardManager.KeyPressed('A'))
		{
			Game.camera.Traslate(new Vector2(3, 0));
		}
		if(KeyboardManager.KeyPressed('D'))
		{
			Game.camera.Traslate(new Vector2(-3, 0));
		}
		if(KeyboardManager.KeyPressed('W'))
		{
			Game.camera.Traslate(new Vector2(0, 3));
		}
		if(KeyboardManager.KeyPressed('S'))
		{
			Game.camera.Traslate(new Vector2(0, -3));
		}
	}


	public synchronized void render(Graphics g)
    {
		BufferStrategy strat = this.frame.getBufferStrategy();
		if (strat == null) {
			this.frame.createBufferStrategy(2);
		}

		g.translate((int) Game.camera.GetPosition().x, (int) Game.camera.GetPosition().y);

		g.setColor(Color.BLACK);
		g.fillRect(-11000, -11000, 900000, 900000);
		for (GameObject go : this.hierarchy.GetObjects())
		{
			go.Render(g);
		}
		int x = MouseManager.X;
		int y = MouseManager.Y;
//		RaycastHitInformation[] hitObjects = Physics.RaycastAll(Coordinates.PixelsToMeters(new Vector2(x - 10, y)), Coordinates.PixelsToMeters(new Vector2(x + 10, y)));
/*
		for (RaycastHitInformation info : hitObjects) {
			System.out.println("Object over mouse with ID: " + info.hitObject.GetID());
		}
*/
		//g2d.drawString("Selected Object id: " + ((hitObjectID != -1) ? String.valueOf(hitObjectID) : "Unknown"), 10, 10);
	}

	public synchronized JFrame GetFrame() {
		return this.frame;
	}


	public synchronized void SpawnObject(GameObject go)
    {
		this.hierarchy.Add(go);
		go.Init();
		if (Game.on)
			if (go.getPhysics() != null)
				go.getPhysics().ActivatePhysics();
			else if (go.getPhysics() != null)
				go.DisablePhysics();
	}

	public synchronized void DestroyAndReset()
    {
		ArrayList<GameObject> toRemove = new ArrayList<GameObject>();
		for (GameObject g : this.hierarchy.GetObjects()) {
			if (g.GetID() == 0) // 0 = Ground
			{
				continue;
			}
			toRemove.add(g);
		}
		for (GameObject g : toRemove)
		{
			g.DeletePhysics();
			this.hierarchy.Remove(g);
		}
	}

	public void OnMouseDown(MouseEvent e)
    {
		Core.getInstance().SpawnObjectWithLocation(MouseManager.X, MouseManager.Y - ((int)25 / (int)2));

		this.audioPool.Get("click").Play();
	}

	public synchronized ArrayList<GameObject> GetObjectList() {
		return this.hierarchy.GetObjects();
	}

	public synchronized void SetObjectList(ArrayList<GameObject> list) {
		this.hierarchy.SetObjects(list);
	}

	public GameObject FindGameObjectByID(int ID) {
		for (GameObject g : this.hierarchy.GetObjects())
		{
			if (g.GetID() == ID) {
				return g;
			}
		}
		return null;
	}

	public void startGame()
    {
		stopGame();
		for (GameObject g : this.hierarchy.GetObjects())
		{
			if (g instanceof Ground)
				continue;
			g.Init();
			g.start();
		}
		Game.on = true;
	}

	public void stopGame()
    {
		for (GameObject g : this.hierarchy.GetObjects())
		{
			if (g instanceof Ground) //Don't stop the ground
				continue;
			g.stop();
		}
		Game.on = false;
	}

	public void RemoveWithID(int id)
    {
        GameObject go = this.hierarchy.GetObjectByID(id);
        if(go != null)
        {
            this.hierarchy.Remove(go);
        }
    }

	public void Remove(int index) {
		GameObject object = this.hierarchy.Get(index);
		if (object.getPhysics() != null) {
			Game.world.destroyBody(object.getPhysics().GetPhysicsBody());
		}
		this.hierarchy.Remove(object);
	}

	public void Remove(GameObject object) {
		if (object.getPhysics() != null) {
			Game.world.destroyBody(object.getPhysics().GetPhysicsBody());
		}
		this.hierarchy.Remove(object);
	}

    /*
    	SAVE DATA

    	Starts with: type - "box", "ellipse", "fountain"

    	BOX:
    		x, y, w, h, simPhys, color, data
    	ELLIPSE:
    		x, y, size, simPhys, color, data
    	FOUNTAIN:
    		x, y, w, h
     */


	public synchronized void SaveToFile(String path) {
		Game.getInst().stopGame();

		BufferedWriter writer = null;
		try {
			writer = new BufferedWriter(new FileWriter(path));
		} catch (IOException e) {
			System.err.println("Could not open a writer object!");
		}


		for (GameObject g : this.hierarchy.GetObjects()) {
			String[] toWrite = {null};
			if (g instanceof Box) {
				toWrite = new String[]{"box", String.valueOf((int) g.GetPosition().x), String.valueOf((int) g.GetPosition().y), String.valueOf((int) g.GetSize().x), String.valueOf((int) g.GetSize().y), String.valueOf(g.getSimulatePhysics()), String.valueOf(g.GetColor().getRGB())};
			}
			if (g instanceof Ellipse) {
				toWrite = new String[]{"ellipse", String.valueOf((int) g.GetPosition().x), String.valueOf((int) g.GetPosition().y), String.valueOf((int) g.GetSize().y), String.valueOf(g.getSimulatePhysics()), String.valueOf(g.GetColor().getRGB())};
			}
			if (g instanceof Fountain) {
				toWrite = new String[]{"fountain", String.valueOf((int) g.GetPosition().x), String.valueOf((int) g.GetPosition().y), String.valueOf((int) g.GetSize().x), String.valueOf((int) g.GetSize().y), String.valueOf(g.getSimulatePhysics())};
			}

			ArrayList<Behavior> behaviors = g.GetBehaviors();
			for (Behavior b : behaviors) {
				String[] toWrite2 = new String[toWrite.length + 1];
				for (int i = 0; i < toWrite.length; i++) {
					toWrite2[i] = toWrite[i];
				}
				toWrite2[toWrite.length] = b.GetName();
				toWrite = toWrite2;
			}

			for (int i = 0; i < toWrite.length; i++) {
				String value = toWrite[i];
				if (value != null) {
					try {
						//System.out.println("Saved Object");
						writer.write(value + ((i == toWrite.length - 1) ? "" : " "));
					} catch (Exception e) {
					}
				}
			}
			try {
				writer.newLine();
			}
			catch (Exception e) {
			}
		}
		try {
			writer.close();
			writer.flush();
		} catch (Exception e) {
		}
	}

	public void LoadFromFile(String path) {
		Game.getInst().DestroyAndReset();

		BufferedReader br = null;
		try {
			br = new BufferedReader(new FileReader(path));
		} catch (IOException e) {
			JOptionPane.showMessageDialog(null, "File does not exist.");
			return;
		}
		try {

			String line = null;
			try {
				line = br.readLine();
			} catch (Exception e) {
				System.err.println("Could not get next line.");
			}

			while (line != null) {
				String[] input = line.split("\\s+");
				GameObject toSpawn = null;
				ArrayList<Behavior> behaviors = new ArrayList<Behavior>();
				int colorRGB;
				switch (input[0]) {
					case "box":
						toSpawn = new Box(new Vector2(Integer.valueOf(input[1]), Integer.valueOf(input[2])), new Vector2(Integer.valueOf(input[3]), Integer.valueOf(input[4])), Boolean.valueOf(input[5]));
						colorRGB = Integer.valueOf(input[6]);
						Color c = new Color(colorRGB);
						toSpawn.SetColor(c);
						for (int i = 7; i < input.length; i++) {
							if (input[i] != null) {
								switch (input[i]) {
									case "ConstantJumpBehavior":
										behaviors.add(new ConstantJumpBehavior(toSpawn));
										System.out.println("Added constantjumpbehavior");
										break;
									case "RandomStartForceBehavior":
										behaviors.add(new RandomStartForceBehavior(toSpawn));
										break;
									default:
										System.err.println("No behavior with name: " + input[i]);
								}
							}
						}
						break;
					case "ellipse":
						toSpawn = new Ellipse(new Vector2(Integer.valueOf(input[1]), Integer.valueOf(input[2])), new Vector2(Integer.valueOf(input[3]), Integer.valueOf(input[3])), Boolean.valueOf(input[4]));
						colorRGB = Integer.valueOf(input[5]);
						toSpawn.SetColor(new Color(colorRGB));

						for (int i = 6; i < input.length; i++) {
							if (input[i] != null) {
								switch (String.valueOf(input[i])) {
									case "ConstantJumpBehavior":
										behaviors.add(new ConstantJumpBehavior(toSpawn));
										System.out.println("Added ConstantJumpBehavior.");
										break;
									case "RandomStartForceBehavior":
										behaviors.add(new RandomStartForceBehavior(toSpawn));
										System.out.println("Added RandomStartForceBehavior.");
										break;
									default:
										System.err.println("No behavior with name: " + input[i]);
										break;
								}
							}
						}
						break;
					case "fountain":
						toSpawn = new Fountain(new Vector2(Integer.valueOf(input[1]), Integer.valueOf(input[2])), new Vector2(Integer.valueOf(input[3]), Integer.valueOf(input[4])), new ArrayList<Behavior>());
						break;
					default:
						break;
				}
				if (toSpawn == null) {
					System.out.println("ToSpawn is null");
				} else {
					for (Behavior b : behaviors) {
						toSpawn.AddBehavior(b);
					}
					Game.getInst().SpawnObject(toSpawn);
				}
				/*
				for(int i = 0; i < line.length(); i++)
				{
					char letter = line.charAt(i);
					System.out.print(letter);
				}
				*/

				try {
					line = br.readLine();
				} catch (Exception e) {
					System.err.println("Could not read line");
				}
			}
		} finally {
			try {
				br.close();
			} catch (Exception e) {
			}
		}
	}

	public void LoadFromXML(String fileName)
	{
		File f = new File(fileName);
		if(f.exists())
		{
			Game.getInst().DestroyAndReset();
		}
		else
		{
			return;
		}
		DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
		DocumentBuilder dBuilder;
		Document doc = null;
		try
		{
			dBuilder = dbFactory.newDocumentBuilder();
			doc = dBuilder.parse(f);
		} catch (Exception e)
		{
			return;
		}

		//System.out.println("Root: " + doc.getDocumentElement().getNodeName());

		NodeList objectList = doc.getElementsByTagName("Object");



		for (int i = 0; i < objectList.getLength(); i++)
		{
			Node currentNode = objectList.item(i);

			System.out.println("node: " + currentNode.getNodeName());

			if (currentNode.getNodeType() == Node.ELEMENT_NODE)
			{

				Element e = (Element) currentNode;
				String objectType = ((Element) currentNode).getAttribute("type");
				String name = ((Element) currentNode).getAttribute("name");

				Node positionNode = e.getElementsByTagName("position").item(0);
				Node sizeNode = ((Element) currentNode).getElementsByTagName("size").item(0);
				Node colorNode = ((Element) currentNode).getElementsByTagName("color").item(0);
				NodeList dataList = e.getElementsByTagName("data");
				ArrayList<String> behaviorList = new ArrayList<String>();
				ArrayList<Behavior> behaviors = new ArrayList<Behavior>();
				boolean simPhys = false;

				for (int d = 0; d < dataList.getLength(); d++)
				{
					Node n = dataList.item(d);
					Element element = (Element) n;
					String type = n.getAttributes().getNamedItem("type").getNodeValue();
					switch (type) {
						case "simulatephysics":
							simPhys = stob(n.getTextContent());
							break;
						case "behavior":
							behaviorList.add(n.getTextContent());
							break;
						default:
							System.err.println("Unknown data value: " + type);
							break;
					}
				}
				Vector2 position = null;
				Vector2 size = new Vector2(5, 5);
				Color c = Color.white;
				System.out.println("looped");
				try
				{
					position = new Vector2(stoi(positionNode.getAttributes().getNamedItem("x").getTextContent()), stoi(positionNode.getAttributes().getNamedItem("y").getTextContent()));
					System.out.println("ObjectType: " + objectType);

					int x, y;

					System.out.println("Object type is " + objectType);

					if(!objectType.equals("ellipse"))
					{
						System.out.println(objectType + " is not an ellipse");
						x = stoi(sizeNode.getAttributes().getNamedItem("x").getTextContent());
						System.out.println("X is " + x);
						y = stoi(sizeNode.getAttributes().getNamedItem("y").getTextContent());
					}
					else
					{
						x = stoi(sizeNode.getAttributes().getNamedItem("r").getTextContent());
						y = stoi(sizeNode.getAttributes().getNamedItem("r").getTextContent());;
					}
					System.out.println(new Vector2(x, y).ToString());
					size = new Vector2(x, y);

					Color color;
					try
					{
						color = new Color(Integer.valueOf(colorNode.getTextContent()));
						System.out.println("Color is " + color.toString());
					}
					catch (Exception ex)
					{
						color = Color.WHITE;
					}
					c = color;
				}
				catch (Exception ex) {
					ex.printStackTrace();
				}

				GameObject toSpawn = null;

				switch (objectType)
				{
					case "box":
						System.out.println("Should spawn a box");
						toSpawn = new Box(position, size, simPhys);
						break;
					case "ellipse":
						System.out.println("Should spawn a ellipse");
						toSpawn = new Ellipse(position, size, simPhys);
						break;
					case "fountain":
						System.out.println("Should spawn a fountain");
						break;
					default:
						System.out.println("returned");
						continue;
				}

				System.out.println("Set color to " + c.toString());
				toSpawn.SetColor(c);
				toSpawn.setName(name);

				HashMap<String, Behavior> behaviorMap = new HashMap<String, Behavior>();

				behaviorMap.put("ConstantJumpBehavior", new ConstantJumpBehavior());
				behaviorMap.put("RandomStartForceBehavior", new RandomStartForceBehavior());

				for(String s : behaviorList)
				{
					Behavior b = behaviorMap.get(s);

					if(b == null)
					{
						System.err.println("Invalid Behavior");
						return;
					}
					b.SetTarget(toSpawn);
					toSpawn.AddBehavior(b);
				}
				if (toSpawn == null)
				{
					System.err.println("Could not read object from XML!");
				} else
				{
					System.out.println("Spawning object with position: " + toSpawn.GetPosition().ToString() + ", With Size: " + toSpawn.GetSize().ToString() + ", With Color: " + toSpawn.GetColor().toString());
					this.SpawnObject(toSpawn);
				}
			}
		}

	}

	public static Game GameFromFile(String path) {
		Game game = new Game();
		game.LoadFromFile(path);
		return game;
	}

	public static int stoi(String i) {
		return Integer.valueOf(i);
	}

	public static boolean stob(String b) {
		return Boolean.valueOf(b);
	}

	public synchronized void SaveToXml(String fileName)
	{

		File f = null;
		f = new File(fileName);
		if(!f.exists())
		{
			f.getParentFile().mkdirs();
			try {
				f.createNewFile();
			}catch(Exception e){e.printStackTrace();}
		}
		System.out.println("made xml file");
		DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
		DocumentBuilder dBuilder;
		Document doc = null;
		try {
			dBuilder = dbFactory.newDocumentBuilder();
			doc = dBuilder.newDocument();
		} catch (Exception e) {
			e.printStackTrace();
			return;
		}

		Element map = doc.createElement("map");
		doc.appendChild(map);

		Inspector.GetInstance().RevertColor();

		for(GameObject g : this.hierarchy.GetObjectsExcludingGround())
		{
			Element root = doc.createElement("Object");
			CreateObject(doc, root, g);
			System.out.println("Made object");
			map.appendChild(root);
		}

		Inspector.GetInstance().SetWhite();

		// write the content into xml file
		try
		{
			TransformerFactory transformerFactory = TransformerFactory.newInstance();
			Transformer transformer = transformerFactory.newTransformer();
			transformer.setOutputProperty(OutputKeys.INDENT, "yes");
			transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");
			DOMSource source = new DOMSource(doc);
			StreamResult result = new StreamResult(f);

			// Output to console for testing
			// StreamResult result = new StreamResult(System.out);

			transformer.transform(source, result);
		}
		catch(Exception e)
		{
			e.printStackTrace();
		}

		System.out.println("File saved!");

	}
	private static void CreateObject(Document doc, Element root, GameObject object)
	{
		String type = "undefined";
		if(object instanceof Box)
		{
			type = "box";
		}
		else if(object instanceof Ellipse)
		{
			type = "ellipse";
		}
		else if(object instanceof Fountain)
		{
			type = "fountain";
		}

		root.setAttribute("type", type);
		root.setAttribute("name", object.getName());

		Element position = doc.createElement("position");
		position.setAttribute("x", Integer.toString((int)object.GetPosition().x));
		position.setAttribute("y", Integer.toString((int)object.GetPosition().y));
		root.appendChild(position);

		Element size = doc.createElement("size");
		if(object instanceof Ellipse)
		{
			size.setAttribute("r", Integer.toString((int)object.GetSize().y));
		}
		else
		{
			size.setAttribute("x", Integer.toString((int)object.GetSize().x));
			size.setAttribute("y", Integer.toString((int)object.GetSize().y));
		}
		root.appendChild(size);

		Element simPhys = doc.createElement("data");
		simPhys.setAttribute("type", "simulatephysics");
		simPhys.appendChild(CreateText(doc, String.valueOf(object.getSimulatePhysics())));
		root.appendChild(simPhys);

		ArrayList<String> behaviorList = new ArrayList<String>();

		for(Behavior behavior : object.GetBehaviors())
		{
			behaviorList.add(behavior.GetName());
		}

		for(String name : behaviorList)
		{
			Element newElement = doc.createElement("data");
			newElement.setAttribute("type", "behavior");
			newElement.appendChild(CreateText(doc, name));
			root.appendChild(newElement);
		}


		Element color = doc.createElement("color");
		color.appendChild(CreateText(doc, String.valueOf(object.GetColor().getRGB())));
		root.appendChild(color);
	}

	public void LoadCustomBehavior(String filePath, boolean resourcesIncluded)
	{
		DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
		DocumentBuilder dBuilder;
		Document doc = null;
		try
		{
			dBuilder = dbFactory.newDocumentBuilder();
			doc = dBuilder.parse(new File((resourcesIncluded ? "Resources/" : "") + filePath));
		} catch (Exception e)
		{
			e.printStackTrace();
			return;
		}

		//TODO: Load into behavior class
	}

	private static Node CreateText(Document document, String text)
	{
		return document.createTextNode(text);
	}
	public Hierarchy GetHierarchy()
	{
		return this.hierarchy;
	}
	public void ShowMessage(String message)
	{
		JOptionPane.showMessageDialog(null, message);
	}

	public static void SetCamera(Camera camera)
	{
		if(camera == null)
		{
			//Create a default camera at the default location
			Camera replacementCamera = new Camera(new Vector2(Core.GAME_WIDTH/2, Core.GAME_HEIGHT/2));
			Game.camera = replacementCamera;
			return;
		}
		Game.camera = camera;
		return;
	}
}