package com.WalkerRCase.JavaSwingHelper;

import java.awt.Component;
import java.awt.Container;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.Rectangle;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.ArrayList;
import java.util.HashMap;

import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JLabel;

/**
 * The new JFrame! This class has everything you need to get started.
 * 
 * @author Walker Case
 *
 */
public class SwingWindow {

	private JFrame frame;
	private Scene currentScene;
	private String title;
	private Rectangle originalBounds;
	
	/**
	 * Returns the JFrame everything is built upon.</br>
	 * Modifications to this may make aspects of the program behave unexpectedly.
	 * @return
	 */
	public JFrame getWindowBase(){
		return frame;
	}

	/**
	 * Start the window with the given title and bounds.
	 * 
	 * @param title
	 * @param bounds
	 * @return
	 */
	public static SwingWindow start(String title, Rectangle bounds) {
		SwingWindow window = new SwingWindow(title, bounds);
		window.frame.setVisible(true);
		window.frame.setResizable(true);
		return window;
	}

	/**
	 * Create the application window.
	 * 
	 * @param title
	 * @param bounds
	 */
	public SwingWindow(String title, Rectangle bounds) {
		initialize(title, bounds);
	}

	/**
	 * Create the application window.
	 * 
	 * @param title
	 * @param width
	 * @param height
	 */
	public SwingWindow(String title, int width, int height) {
		initialize(title, new Rectangle(0, 0, width, height));
	}

	/**
	 * Returns the scene of the given class.</br>
	 * If the scene is not of that class null is returned instead.
	 * 
	 * @param clazz
	 * @return
	 */
	public Scene getSceneOfType(Class<? extends Scene> clazz) {
		if (currentScene.getClass() == clazz)
			return currentScene;
		return null;
	}

	public Scene getScene() {
		return currentScene;
	}

	/**
	 * Initialize the contents of the frame.
	 */
	private void initialize(String title, Rectangle bounds) {
		frame = new JFrame();
		frame.setBounds(bounds);
		frame.setMinimumSize(frame.getBounds().getSize());
		frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		frame.getContentPane().setLayout(null);
		frame.setTitle(title);
		this.title = title;
		this.originalBounds = frame.getBounds();
	}

	/**
	 * Stores the original component object for later use.
	 */
	private HashMap<Integer, byte[]> originalComps = new HashMap<Integer, byte[]>();

	/**
	 * Load a new scene into the frame.</br>
	 * This will clear out the old scene.
	 * 
	 * @param scene
	 */
	public void loadScene(Scene scene) {
		if (currentScene != null)
			currentScene.unloadScene();
		currentScene = scene;

		frame.getContentPane().removeAll();
		originalComps.clear();

		scene.loadScene(frame.getContentPane());

		addComponentToList(frame.getContentPane());
		// Used to avoid an infinite loop.
		originalComps.remove(System.identityHashCode(frame.getContentPane()));

		frame.setTitle(title + " - " + scene.getTitle());
		frame.getContentPane().repaint();

		frame.getContentPane().addComponentListener(new ComponentAdapter() {
			@Override
			public void componentResized(ComponentEvent e) {
				pack();
			}
		});

		pack();
	}

	/**
	 * Add a component and all of it's sub-components to the render list.</br>
	 * <b>Warning</b>: Never add the window's content pane itself to this list! If you
	 * do make sure you remove it!
	 * 
	 * @param comp
	 */
	public void addComponentToList(Component comp) {
		if (comp instanceof Container) {
			Container cont = (Container) comp;
			for (Component com : cont.getComponents()) {
				addComponentToList(com);
			}
		}
		try {
			if (comp instanceof JComponent) {
				originalComps.put(System.identityHashCode(comp), serializeObject(JComponent.class.cast(comp)));
			}
		} catch (IOException e1) {
			e1.printStackTrace();
		}
	}

	/**
	 * Convert the given byte array into an Object of T class.
	 * 
	 * @param c
	 * @param s
	 * @return
	 * @throws IOException
	 * @throws ClassNotFoundException
	 */
	public <T extends Object> T deserializeObject(Class<T> c, byte[] byteArray)
			throws IOException, ClassNotFoundException {
		if (c == null || byteArray == null || byteArray.length <= 0)
			return null;
		ByteArrayInputStream bais = new ByteArrayInputStream(byteArray);
		ObjectInputStream ois = new ObjectInputStream(bais);

		Object os = ois.readObject();

		ois.close();
		bais.close();

		return c.cast(os);
	}

	/**
	 * Serialize an object into a byte array.
	 * 
	 * @param o
	 * @return
	 * @throws IOException
	 */
	public byte[] serializeObject(Object o) throws IOException {
		ByteArrayOutputStream baos = new ByteArrayOutputStream();
		ObjectOutputStream oos = new ObjectOutputStream(baos);

		oos.writeObject(o);
		oos.flush();

		byte[] arr = baos.toByteArray();

		oos.close();
		baos.close();
		return arr;
	}
	/**
	 * Returns true if a window option has been enabled.
	 * 
	 * @param option
	 * @return
	 */
	public boolean isWindowOptionEnabled(WindowOptions option) {
		if(currentScene.getWindowOptions() == null)
			return false;
		for(WindowOptions option2 : currentScene.getWindowOptions()){
			if(option2 == option)
				return true;
		}
		return false;
	}

	private Thread renderThread;

	/**
	 * Scale the windows contents to fit the frame.</br>
	 * Note: The pack will only run if and only if the last pack call has
	 * finished executing.
	 */
	private void pack() {
		if (renderThread == null || !renderThread.isAlive()) {
			renderThread = new Thread() {
				@Override
				public void run() {
					doRender(frame.getContentPane());
					stopRender();

					frame.repaint();
				}
			};
			renderThread.setName("JSH - Render Thread");
			renderThread.start();
		}
	}

	private ArrayList<Integer> rendered = new ArrayList<Integer>();

	/**
	 * Clear the render buffer.
	 */
	private void stopRender() {
		rendered.clear();
	}

	/**
	 * Run the render method of the given component.
	 * 
	 * @param comp
	 */
	private void doRender(Component comp) {
		if (rendered.contains(System.identityHashCode(comp))) {
			return;
		}
		rendered.add(System.identityHashCode(comp));
		if (comp instanceof Container) {
			Container cont = (Container) comp;
			for (Component comp2 : cont.getComponents()) {
				doRender(comp2);
			}
		}

		float[] scale = getScalef();

		Component originalComponent = null;
		try {
			originalComponent = deserializeObject(JComponent.class, originalComps.get(System.identityHashCode(comp)));
		} catch (ClassNotFoundException | IOException e) {
			e.printStackTrace();
		}

		if (originalComponent != null) {
			// Bounds
			if (isWindowOptionEnabled(WindowOptions.SCALE_COMPONENTS)) {
				Rectangle original = originalComponent.getBounds();
				int newX = (int) (original.getX() * scale[0]);
				int newY = (int) (original.getY() * scale[1]);
				int newW = (int) (original.getWidth() * scale[0]);
				int newH = (int) (original.getHeight() * scale[1]);
				comp.setBounds(newX, newY, newW, newH);
			}

			// Fonts
			if (isWindowOptionEnabled(WindowOptions.SCALE_FONTS)) {
				if (comp.getFont() != null && originalComponent.getFont() != null) {
					Font font = comp.getFont();
					Font newFont = font.deriveFont(font.getStyle(),
							getSuggestedFontSize(originalComponent.getFont().getSize()));
					comp.setFont(newFont);
				}
			}

			// Images
			if (isWindowOptionEnabled(WindowOptions.SCALE_IMAGES)) {
				if (comp instanceof JLabel) {
					JLabel label = (JLabel) comp;
					JLabel orig = (JLabel) originalComponent;
					label.setIcon(getScaledImageIcon(orig));
				}
			}
		}
	}

	/**
	 * Get the scaled icon for the given JLabel.
	 * 
	 * @param label
	 * @return
	 */
	public ImageIcon getScaledImageIcon(JLabel label) {
		return getScaledImageIcon(label.getIcon(), label.getWidth(), label.getHeight());
	}

	/**
	 * Scale the given icon to the given dimensions
	 * 
	 * @param label
	 * @return
	 */
	public ImageIcon getScaledImageIcon(Icon icon, int width, int height) {
		if (icon != null) {
			BufferedImage buf = new BufferedImage(icon.getIconWidth(), icon.getIconHeight(),
					BufferedImage.TYPE_INT_RGB);
			Graphics graphics = buf.createGraphics();
			icon.paintIcon(null, graphics, 0, 0);
			graphics.dispose();

			Image dimg = buf.getScaledInstance(width, height, Image.SCALE_SMOOTH);
			return new ImageIcon(dimg);
		}
		return null;
	}

	/**
	 * Get the current UI scale of the window.
	 * 
	 * @return float[width, height]
	 */
	public float[] getScalef() {
		Rectangle sceneNew = frame.getBounds();
		return new float[] { ((float) (sceneNew.width * 100) / this.originalBounds.width) / 100,
				((float) (sceneNew.height * 100) / this.originalBounds.height) / 100 };
	}

	/**
	 * Get the suggested font size calculated with the current UI scale.
	 * 
	 * @param originalSize
	 * @return
	 */
	public int getSuggestedFontSize(int originalSize) {
		float[] scale = getScalef();

		float diff = (((scale[0] * 100) + (scale[1] * 100)) / 2);
		int fontSize = (int) ((originalSize * diff) / 100);

		return fontSize;
	}

	/**
	 * Get the original bounds to the frame.</br>
	 * Scaling doesn't affect this.
	 * 
	 * @return
	 */
	public Rectangle getOriginalBounds() {
		return (Rectangle) originalBounds.clone();
	}

}