package bouncingballs.engine;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;

import javax.swing.JOptionPane;
import javax.swing.JPanel;

import bouncingballs.ai.AI;
import bouncingballs.ball.Ball;
import bouncingballs.collisionsengine.CollisionsEngine;
import bouncingballs.engineentity.EngineEntity;
import bouncingballs.enginelisteners.ControllerListener;
import bouncingballs.enginelisteners.CrosshairListener;
import bouncingballs.enginelisteners.ProjectileListener;
import bouncingballs.projectile.Projectile;
import bouncingballs.sprite.SpriteAnimation;
import bouncingballs.sprite.SpriteCharacter;
import bouncingballs.sprite.SpriteSheet;

/**
 * The engine class.
 * 
 * This class uses a JPanel to draw in, add it to
 * your current frame to use it. To actually start
 * the engine you need to start a thread to call the
 * run method.
 */
public class Engine extends JPanel implements Runnable
{
	// If we ever actually use serialization we can worry about this.
	private static final long serialVersionUID = -3261950291129216393L;
	
	// All of the settings for the engine.
	private EngineSettings engineSettings;

	private ArrayList<SpriteCharacter> AIs;
	private SpriteCharacter player;
	private ConcurrentHashMap<Integer, EngineEntity> entities;
	
	// Fields for double buffering the image.
	private BufferedImage image;
	
	private ArrayList<BufferedImage> playerSprites;
	private ArrayList<BufferedImage> gunSprites;
	
	/**
	 * Creates a new Engine.
	 * 
	 * This sets up the JPanel and engine to run
	 * but you still need to call run() to get it
	 * to start.
	 *
	 *@param engineSettings The settings for the engine.
	 */
	public Engine(EngineSettings engineSettings)
	{
		this.engineSettings = engineSettings;
		entities = new ConcurrentHashMap<Integer, EngineEntity>();
		AIs = new ArrayList<>();
		playerSprites = SpriteSheet.getPlayerSprites();
		gunSprites = SpriteSheet.getWeaponSprite();
		player = new SpriteCharacter(engineSettings.getWidth() / 2.0, (double)engineSettings.getHeight() - playerSprites.get(0).getHeight(), engineSettings.getWidth(), new SpriteAnimation(playerSprites, gunSprites.get(0), 0.2));
	
		/*
		for(int i = 1; i < 5; ++i)
		{
			AI tmp = new AI(i * 90.0, (double)engineSettings.getHeight() - playerSprites.get(0).getHeight(), engineSettings.getWidth(),
					new SpriteAnimation(playerSprites, gunSprites.get(0), 0.2), player);
			tmp.setMomentum(0.2);
			AIs.add(tmp);
			entities.put(tmp.hashCode(), tmp);
		}
		*/
		
		entities.put(player.hashCode(), player);
		entities.putAll(makeBalls(engineSettings).stream().collect(Collectors.toConcurrentMap(Ball::hashCode, b -> b)));
//				.ballCount(4)
//				.speed(2)
//				.ballSize(20)
//				.width(16*50)
//				.height(9*50)
//				.momentum(0.5)
//				.build())
//				.stream().collect(Collectors.toConcurrentMap(Ball::hashCode, b -> b)));
		
		this.addMouseListener(new ProjectileListener(player, entities));
		this.addMouseMotionListener(new CrosshairListener(player));
		this.addKeyListener(new ControllerListener(player));
		
		// JPanel to draw in.
		setSize(new Dimension(engineSettings.getWidth(), engineSettings.getHeight()));
		setPreferredSize(new Dimension(engineSettings.getWidth(), engineSettings.getHeight()));
		setMinimumSize(getPreferredSize());
		setMaximumSize(getPreferredSize());
		setFocusable(true);
		requestFocus();
		setVisible(true);
		
		// Create a new Buffered Image to draw in.
		// swing or awt already support double buffering, this is to use bufferedimage getRGB method.
		image = new BufferedImage(this.getWidth(), this.getHeight(), BufferedImage.TYPE_INT_BGR);
	}
	
	/**
	 * The actual engine, calls move on all the balls and
	 * handles collisions.
	 */
	 /*NOT THREAD SAFE!!
	 * 	Our paintComponent methods uses values from the balls
	 * 	HashMap, if repaint doesn't finish before sleep finishes
	 * 	we can possibly change values in the next move before
	 * 	they can be painted, thus throwing a ConcurrentModificationException()
	 * 
	 * 	Finding touching balls was from here: http://stackoverflow.com/questions/345838/ball-to-ball-collision-detection-and-handling
	 *
	 *	Currently when a collision is detected the ball simply moves in the reverse
	 *	direction, this obviously isn't what happens when two object collide and 
	 *	needs to be changed.
	 * 	
	 */
	@Override
	public void run()
	{
		long cycleStartTime = 0;
		long cycleStopTime = 0;

		// Infinite loop is the best way to implement this?
		while(true)
		{
			// repaint the scene!
			repaint();
			
			// Currently we just sleep the engine to imitate speed, this
			// will be changed once we create vector graphics instead.
			try
			{
				Thread.sleep( Math.max(0, engineSettings.getSpeed() - (cycleStopTime - cycleStartTime)));
			}
			catch (InterruptedException e)
			{
				e.printStackTrace();
			}

			cycleStartTime = System.currentTimeMillis();
			
			ArrayList<Ball> tmpBalls = new ArrayList<>();
			
			for(EngineEntity e : entities.values())
				if(e instanceof Ball)
					tmpBalls.add((Ball) e);
			
			// Call collision on all found colliding balls.
			CollisionsEngine.findCollisions(tmpBalls).forEach(Ball::collision);
			
			for(Iterator<Map.Entry<Integer, EngineEntity>> it = entities.entrySet().iterator(); it.hasNext();)
			{
				Map.Entry<Integer, EngineEntity> entry = it.next();
				if(!entry.getValue().isAlive())
					it.remove();
			}
			
			for(EngineEntity e : entities.values())
			{
				if(e instanceof Ball && !(e instanceof Projectile))
				{
					Ball tmp = (Ball)e;
					if(tmp.getY() > engineSettings.getHeight())
					{
						//JOptionPane.showConfirmDialog(null, "You lost!");
						//tmp.setAlive(false);
					}
				}
			}
			
			// Actually move the balls now that we've found and corrected
			// all the collisions.
			for(EngineEntity e : entities.values())
				e.move();
			
			cycleStopTime = System.currentTimeMillis();
		}
	}
	
	@Override
	public void paintComponent(Graphics g)
	{
		super.paintComponent(g);
		((Graphics2D) image.getGraphics()).setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
		doubleBufferedPaint((Graphics2D)image.getGraphics());
		g.drawImage(image, 0, 0, null);
	}
	
	/**
	 * Draws the scene onto a Graphics object.
	 *
	 *@param g Graphics object to draw on.
	 */
	protected void doubleBufferedPaint(Graphics2D g)
	{
		g.setColor(engineSettings.getBackgroundColor());
		g.fillRect(0, 0, this.getWidth(), this.getHeight());
		
		for(EngineEntity e : entities.values())
			e.draw(g);
	}
	
	/**
	 * Creates a HashMap of randomly generated balls.
	 *
	 *@param engineSettings EngineSettings object used to create ball settings.
	 *@return	ArrayList<Ball>
	 */
	private static ArrayList<Ball> makeBalls(EngineSettings engineSettings)
	{
		ArrayList<Ball> balls = new ArrayList<Ball>();
		ArrayList<Point> ballLocations = new ArrayList<>();
		
		// So balls aren't placed directly beside another we add 3 pixels as a spacer.
		// To ensure we don't have any overlapping balls we generate all possible locations beforehand then shuffle them.
		for(int x = 0; x < engineSettings.getWidth() - engineSettings.getBallSize(); x += engineSettings.getBallSize() + 3)
			for(int y = 0; y < engineSettings.getHeight() - engineSettings.getBallSize() * 5; y += engineSettings.getBallSize() + 3)
				ballLocations.add(new Point(x, y));
		
		// Shuffle the ball locations.
		long seed = System.nanoTime();
		Collections.shuffle(ballLocations, new Random(seed));
		
		// Create map of balls.
		for(int i = 0; i < engineSettings.getBallCount() && i < ballLocations.size(); ++i)
		{
			// Create a color as a lookup key.
			Color color = new Color((float)Math.random(), (float)Math.random(), (float)Math.random());
			
			balls.add(new Ball(ballLocations.get(i).getX(), ballLocations.get(i).getY(), 
					Math.random() * 360,
					engineSettings.getMomentum(),
					engineSettings.getWidth(), engineSettings.getHeight() + (engineSettings.getBallSize() * 2),
					engineSettings.getBallSize(),
					color));
		}
		
		return balls;
	}
}