package org.mt4jx.components.visibleComponents.widgets.menus;

import java.util.ArrayList;
import java.util.List;

import org.mt4j.MTApplication;
import org.mt4j.components.MTComponent;
import org.mt4j.components.TransformSpace;
import org.mt4j.components.clipping.Clip;
import org.mt4j.components.css.style.CSSFont;
import org.mt4j.components.css.style.CSSStyle;
import org.mt4j.components.css.util.CSSFontManager;
import org.mt4j.components.css.util.CSSKeywords.CSSFontWeight;
import org.mt4j.components.visibleComponents.font.IFont;
import org.mt4j.components.visibleComponents.shapes.MTPolygon;
import org.mt4j.components.visibleComponents.shapes.MTRectangle;
import org.mt4j.components.visibleComponents.widgets.MTTextArea;
import org.mt4j.input.inputProcessors.IGestureEventListener;
import org.mt4j.input.inputProcessors.MTGestureEvent;
import org.mt4j.input.inputProcessors.componentProcessors.tapProcessor.TapEvent;
import org.mt4j.input.inputProcessors.componentProcessors.tapProcessor.TapProcessor;
import org.mt4j.util.MTColor;
import org.mt4j.util.math.Tools3D;
import org.mt4j.util.math.Vector3D;
import org.mt4j.util.math.Vertex;

import processing.core.PConstants;
import processing.core.PImage;

/**
 * The Class MTHexagonMenu. Menu that contains Hexagons as buttons containing
 * text or images
 */
public class MTHexagonMenu extends MTRectangle{

	/** The app. */
	private MTApplication app;

	/** The menu contents. */
	private List<MTPolygon> menuContents = new ArrayList<MTPolygon>();

	/** The layout. */
	private List<ArrayList<MTPolygon>> layout = new ArrayList<ArrayList<MTPolygon>>();

	/** The size. */
	private float size;

	/** The current. */
	private int current = 0;

	/** The max per line. */
	private int maxPerLine = 0;

	/** The bezel. */
	private float bezel = 4f;
	

	// List of the Child Polygons and their IGestureEventListeners
	/** The polygon listeners. */
	private List<PolygonListeners> polygonListeners = new ArrayList<PolygonListeners>();


	/** The menu items. */
	private List<MenuItem> menuItems = new ArrayList<MenuItem>();


	/**
	 * Instantiates a new MTHexagonMenu.
	 * 
	 * @param app
	 *            the app
	 * @param position
	 *            the position of the Menu
	 * @param menuItems
	 *            the menu items
	 * @param size
	 *            the size (width of the Hexagon, the height is bigger)
	 */
	public MTHexagonMenu(MTApplication app, Vector3D position,
			List<MenuItem> menuItems, float size) {
		super(app, position.x, position.y, (float) (int) Math
						.sqrt(menuItems.size() + 1) * size, (float) (int) Math
				.sqrt(menuItems.size() + 1) * size);
		this.app = app;
		this.size = size;

		// Set the Rectangle to be invisible
		this.setCssForceDisable(true);
		this.setNoFill(true);
		this.setNoStroke(true);

		this.menuItems = menuItems;

		this.createMenuItems();
		
		// Register the TapProcessor
		this.setGestureAllowance(TapProcessor.class, true);
		this.registerInputProcessor(new TapProcessor(app));
		this.addGestureListener(TapProcessor.class, new TapListener(
				polygonListeners));
		this.setCssForceDisable(true);

	}


	/**
	 * Re-Creates the menu items.
	 */
	public void createMenuItems() {

		for (MTComponent c : this.getChildren()) {
			c.destroy();
		}
		this.removeAllChildren();
		menuContents.clear();
		polygonListeners.clear();
		
		for (MenuItem s : menuItems) {

			if (s != null && s.getType() == MenuItem.TEXT) {
				// Create Hexagon with Text Included
				MTPolygon container = getHexagon(size);
				this.addChild(container);

				// Add MTTextArea Children to take single lines of the Menu Text
				for (String t : s.getMenuText().split("\n")) {
					MTTextArea menuItem = new MTTextArea(app);
					menuItem.setText(t);
					menuItem.setCssForceDisable(true);
					menuItem.setFillColor(new MTColor(0, 0, 0, 0));
					menuItem.setStrokeColor(new MTColor(0, 0, 0, 0));
					menuItem.setPickable(false);
					container.addChild(menuItem);

				}
				container.setChildClip(new Clip(container));
				container.setPickable(false);
				polygonListeners.add(new PolygonListeners(container, s
						.getGestureListener()));
				menuContents.add(container);
			} else if (s != null && s.getType() == MenuItem.PICTURE) {

				if (s.getMenuImage() != null) {
					// Create Polygon for holding an Image
					PImage texture = null;
					MTPolygon container = getHexagon(size);
					container.setCssForceDisable(true);
					int height = (int) container
							.getHeightXY(TransformSpace.LOCAL);

					// If Image doesn't fit, make it fit!
					if (s.getMenuImage().width != size
							|| s.getMenuImage().height != height) {
						texture = cropImage(s.getMenuImage(), (int) size,
								height, true);
					} else {
						texture = s.getMenuImage();
					}

					this.addChild(container);

					// Normalize Texture Coordinates
					for (Vertex v : container.getVerticesLocal()) {
						v.setTexCoordU(v.getX() / (float) texture.width);
						if (v.getTexCoordU() > 1)
							v.setTexCoordU(1);
						v.setTexCoordV(v.getY() / (float) texture.height);
						if (v.getTexCoordV() > 1)
							v.setTexCoordV(1);
					}

					// Set the Texture
					container.getGeometryInfo()
							.setTextureCoordsNormalized(true);
					container.setTexture(texture);

					menuContents.add(container);
					container.setPickable(false);
					polygonListeners.add(new PolygonListeners(container, s
							.getGestureListener()));
				}

			}

		}

	


		// Apply Style to Children
		this.styleChildren(getNecessaryFontSize(menuItems, size));

	}

	/**
	 * Gets the size.
	 *
	 * @return the size
	 */
	public float getSize() {
		return size;
	}

	/**
	 * Sets the size.
	 *
	 * @param size the new size
	 */
	public void setSize(float size) {
		this.size = size;
		this.createMenuItems();
	}

	/**
	 * Calculates the total height of a number of MTTextAreas.
	 *
	 * @param components the components
	 * @return the height
	 */
	private float calcTotalHeight(MTComponent[] components) {
		// Calculate the total height of several MTTextAreas
		float height = 0;
		for (MTComponent c : components) {
			if (c instanceof MTTextArea)
				height += ((MTTextArea) c).getHeightXY(TransformSpace.LOCAL);
		}

		return height;
	}

	/**
	 * Crop image.
	 * 
	 * @param image
	 *            the image
	 * @param width
	 *            the width
	 * @param height
	 *            the height
	 * @param resize
	 *            resize the image?
	 * @return the cropped image
	 */
	private PImage cropImage(PImage image, int width, int height, boolean resize) {
		PImage workingCopy;
		try {
			workingCopy= (PImage) image.clone();
		} catch (CloneNotSupportedException e) {
			System.out.println("Cloning not supported!");
			workingCopy = image;
		}
		
		// Crops an Image to fit to the Hexagon Size
		PImage returnImage = app.createImage(width, height, PConstants.RGB);

		// Resize Image to match size, but retain aspect ration
		if (resize || workingCopy.width < size || workingCopy.height < size) {
			if (((float) workingCopy.width / (float) width) < ((float) workingCopy.height / (float) height)) {
				workingCopy.resize(
						width,
						(int) ((float) workingCopy.height / ((float) workingCopy.width / (float) width)));
			} else {
				workingCopy.resize(
						(int) ((float) workingCopy.width / ((float) workingCopy.height / (float) height)),
						height);
			}

		}
		
		
		
		// Crop Starting Points
		int x = (workingCopy.width / 2) - (width / 2);
		int y = (workingCopy.height / 2) - (height / 2);

		// Bugfixing: Don't Allow Out-of-Bounds coordinates
		if (x + width > workingCopy.width)
			x = workingCopy.width - width;
		if (x < 0)
			x = 0;
		if (x + width > workingCopy.width)
			width = workingCopy.width - x;
		if (y + height > workingCopy.height)
			x = workingCopy.height - height;
		if (y < 0)
			y = 0;
		if (y + height > workingCopy.height)
			height = workingCopy.height - y;

		// Crop Image
		returnImage.copy(workingCopy, x, y, width, height, 0, 0, width, height);

		return returnImage;
	}

	/**
	 * Creates a new Hexagon.
	 *
	 * @param size the width of the Hexagon
	 * @return the hexagon
	 */
	private MTPolygon getHexagon(float size) {
		// Create a new Polygon
		float hypotenuse = (float) ((size / 2f) / Math.cos(Math.toRadians(30)));
		//float ankathete = (float) (Math.cos(Math.toRadians(30)) * hypotenuse);
		float gegenkathete = (float) (Math.sin(Math.toRadians(30)) * hypotenuse);

		Vertex v1 = new Vertex(0, gegenkathete);
		Vertex v2 = new Vertex(size / 2f, 0);
		Vertex v3 = new Vertex(size, gegenkathete);
		Vertex v4 = new Vertex(size, gegenkathete + hypotenuse);
		Vertex v5 = new Vertex(size / 2f, 2 * gegenkathete + hypotenuse);
		Vertex v6 = new Vertex(0, gegenkathete + hypotenuse);
		Vertex v7 = new Vertex(0, gegenkathete);

		MTPolygon hexagon = new MTPolygon(app, new Vertex[] { v1, v2, v3, v4,
				v5, v6, v7 });

		return hexagon;
	}

	/**
	 * Gets the maximum font size for a certain width.
	 *
	 * @param strings the strings
	 * @param size the width
	 * @return the maximum font size
	 */
	private int getNecessaryFontSize(List<MenuItem> strings, float size) {
		// Calculate the Necessary font size for a SansSerif Bold font
		int maxNumberCharacters = 0;

		for (MenuItem s : strings) {

			if (s.getType() == MenuItem.TEXT) {
				if (s.getMenuText().contains("\n")) {
					for (String t : s.getMenuText().split("\n")) {

						if (t.length() > maxNumberCharacters)
							maxNumberCharacters = t.length();

					}
				} else {

					if (s.getMenuText().length() > maxNumberCharacters)
						maxNumberCharacters = s.getMenuText().length();

				}
			}
		}

		float spc = size / (float) maxNumberCharacters; // Space Per Character
		int returnValue = (int) (-0.5 + 1.725 * spc); // Determined using Linear
		// Regression
		return returnValue;

	}

	/**
	 * Returns the next n items.
	 *
	 * @param next the number of items to return
	 * @return the list of n next items
	 */
	private List<MTPolygon> next(int next) {
		// Return the next n MTPolygons from the list of children
		List<MTPolygon> returnValues = new ArrayList<MTPolygon>();
		for (int i = 0; i < next; i++) {
			returnValues.add(menuContents.get(current++));
		}

		return returnValues;
	}

	/**
	 * Distribute the contents of the menu to the rows.
	 */
	private void organizeHexagons() {
		// Distribute the items on the cell rows
		layout.clear();
		layout.add(new ArrayList<MTPolygon>());
		layout.add(new ArrayList<MTPolygon>());
		layout.add(new ArrayList<MTPolygon>());
		layout.add(new ArrayList<MTPolygon>());
		current = 0;
		switch (menuContents.size()) {
		case 0:
		case -1:
			break;

		case 1:
			layout.get(0).addAll(next(1));
			maxPerLine = 1;
			break;
		case 2:
			layout.get(0).addAll(next(2));
			maxPerLine = 2;
			break;
		case 3:
			layout.get(0).addAll(next(1));
			layout.get(1).addAll(next(2));
			maxPerLine = 2;
			break;
		case 4:
			layout.get(0).addAll(next(1));
			layout.get(1).addAll(next(2));
			layout.get(2).addAll(next(1));
			maxPerLine = 2;
			break;
		case 5:
			layout.get(0).addAll(next(2));
			layout.get(1).addAll(next(3));
			maxPerLine = 3;
			break;
		case 6:
			layout.get(0).addAll(next(1));
			layout.get(1).addAll(next(2));
			layout.get(1).addAll(next(3));
			maxPerLine = 3;
			break;
		case 7:
			layout.get(0).addAll(next(2));
			layout.get(1).addAll(next(3));
			layout.get(2).addAll(next(2));
			maxPerLine = 3;
			break;
		case 8:
			layout.get(0).addAll(next(3));
			layout.get(1).addAll(next(2));
			layout.get(2).addAll(next(3));
			maxPerLine = 3;
			break;
		case 9:
			layout.get(0).addAll(next(2));
			layout.get(1).addAll(next(3));
			layout.get(2).addAll(next(4));
			maxPerLine = 4;
			break;
		case 10:
			layout.get(0).addAll(next(3));
			layout.get(1).addAll(next(4));
			layout.get(2).addAll(next(3));
			maxPerLine = 4;
			break;
		case 11:
			layout.get(0).addAll(next(4));
			layout.get(1).addAll(next(3));
			layout.get(2).addAll(next(4));
			maxPerLine = 4;
			break;
		case 12:
			layout.get(0).addAll(next(3));
			layout.get(1).addAll(next(4));
			layout.get(2).addAll(next(5));
			maxPerLine = 5;
			break;
		case 13:
			layout.get(0).addAll(next(4));
			layout.get(1).addAll(next(5));
			layout.get(2).addAll(next(4));
			maxPerLine = 5;
			break;
		case 14:
			layout.get(0).addAll(next(3));
			layout.get(1).addAll(next(4));
			layout.get(2).addAll(next(3));
			layout.get(3).addAll(next(4));
			maxPerLine = 4;
			break;
		case 15:
			layout.get(0).addAll(next(4));
			layout.get(1).addAll(next(5));
			layout.get(2).addAll(next(6));
			maxPerLine = 6;
			break;
		case 16:
			layout.get(1).addAll(next(5));
			layout.get(2).addAll(next(6));
			layout.get(3).addAll(next(5));
			maxPerLine = 6;
			break;
		default:{
			System.err.println("Unsupported number of menu items in: " + this);
			}
		}

	}

	/**
	 * Style the cells of the menu.
	 *
	 * @param fontsize the font-size
	 */
	private void styleChildren(int fontsize) {
		organizeHexagons();
		CSSStyle vss = this.getCssHelper().getVirtualStyleSheet();
		CSSFont cf = this.getCssHelper().getVirtualStyleSheet().getCssfont().clone();
		// Style Font: Bold + fitting fontsize
		cf.setFontsize(fontsize);
		cf.setWeight(CSSFontWeight.BOLD);

		// Load Font
		CSSFontManager cfm = new CSSFontManager(app);
		IFont font = cfm.selectFont(cf);

		for (MTPolygon c : menuContents) {

			MTPolygon rect = c;

			// Set Stroke/Border
			rect.setStrokeColor(vss.getBorderColor());
			rect.setStrokeWeight(vss.getBorderWidth());

			// Set Font and Position for the child MTTextAreas
			if (((MTPolygon) c).getTexture() == null) {
				rect.setFillColor(vss.getBackgroundColor());
				for (MTComponent d : c.getChildren()) {
					if (d instanceof MTTextArea) {
						MTTextArea ta = (MTTextArea) d;
						ta.setFont(font);
					}
				}

				float height = calcTotalHeight(c.getChildren());
				float ypos = rect.getHeightXY(TransformSpace.LOCAL) / 2f
						- height / 2f;
				for (MTComponent d : c.getChildren()) {
					if (d instanceof MTTextArea) {
						MTTextArea ta = (MTTextArea) d;

						ta.setPositionRelativeToParent(new Vector3D(size / 2f,
								ypos + ta.getHeightXY(TransformSpace.LOCAL)
										/ 2f));
						ypos += ta.getHeightXY(TransformSpace.LOCAL);

					}

				}
			} else {
				// Set Fill Color for Pictures
				MTColor fillColor = MTColor.WHITE;
				// fillColor.setAlpha(vss.getOpacity());
				rect.setFillColor(fillColor);
			}

		}

		// Min/Max Values of the Children
		float minx = 16000, maxx = -16000, miny = 16000, maxy = -16000;

		float hypotenuse = (float) ((size / 2f) / Math.cos(Math.toRadians(30)));
		float gegenkathete = (float) (Math.sin(Math.toRadians(30)) * hypotenuse);
		int currentRow = 0;

		// Position the Polygons in the grid
		for (List<MTPolygon> lr : layout) {
			int currentColumn = 0;
			for (MTPolygon r : lr) {
				r.setPositionRelativeToParent((new Vector3D(this
						.getVerticesLocal()[0].x
						+ (size / 2f)
						+ (bezel / 2f)
						+ currentColumn++
						* (size + bezel)
						+ (maxPerLine - lr.size()) * (size / 2f + bezel / 2f),
						this.getVerticesLocal()[0].y
								+ (hypotenuse / 2 + bezel / 2f) + currentRow
								* (hypotenuse + gegenkathete + bezel))));

				// Determine Min/Max-Positions
//				for (Vertex v : r.getVerticesGlobal()) {
//					if (v.x < minx)
//						minx = v.x;
//					if (v.x > maxx)
//						maxx = v.x;
//					if (v.y < miny)
//						miny = v.y;
//					if (v.y > maxy)
//						maxy = v.y;
//				}
				
				//Determine Min/Max-Positions
				//We have to use the childrens relative-to parent vertices for the calculation of this component's local vertices
				Vertex[] unTransformedCopy = Vertex.getDeepVertexArrayCopy(r.getGeometryInfo().getVertices());
				//transform the copied vertices and save them in the vertices array
				Vertex[] verticesRelParent = Vertex.transFormArray(r.getLocalMatrix(), unTransformedCopy);
				for (Vertex v: verticesRelParent) {
					if (v.x < minx) minx = v.x;
					if (v.x > maxx) maxx = v.x;
					if (v.y < miny) miny = v.y;
					if (v.y > maxy) maxy = v.y;
				}
			}
			currentRow++;
		}

		// Set Vertices to include all children
//		this.setVertices(new Vertex[] { new Vertex(minx, miny),
//				new Vertex(maxx, miny), new Vertex(maxx, maxy),
//				new Vertex(minx, maxy), new Vertex(minx, miny) });
		
		MTColor fill = this.getFillColor();
		//Set Vertices to include all children
		this.setVertices(new Vertex[] {new Vertex(minx,miny, 0, fill.getR(), fill.getG(), fill.getB(), fill.getAlpha()), new Vertex(maxx,miny, 0, fill.getR(), fill.getG(), fill.getB(), fill.getAlpha()), new Vertex(maxx,maxy, 0, fill.getR(), fill.getG(), fill.getB(), fill.getAlpha()), new Vertex(minx,maxy, 0, fill.getR(), fill.getG(), fill.getB(), fill.getAlpha()),new Vertex(minx,miny, 0, fill.getR(), fill.getG(), fill.getB(), fill.getAlpha())});
	}

	/**
	 * The listener interface for receiving tap events. The class that is
	 * interested in processing a tap event implements this interface, and the
	 * object created with that class is registered with a component using the
	 * component's <code>addTapListener<code> method. When
	 * the tap event occurs, that object's appropriate
	 * method is invoked.
	 * 
	 * @see TapEvent
	 */
	public class TapListener implements IGestureEventListener {
		// Tap Listener to reach through TapListeners to children
	
		/** The children. */
		List<PolygonListeners> children;
	
		/**
		 * Instantiates a new tap listener.
		 * 
		 * @param children
		 *            the children
		 */
		public TapListener(List<PolygonListeners> children) {
			this.children = children;
		}
	
		/*
		 * (non-Javadoc)
		 * 
		 * @see
		 * org.mt4j.input.inputProcessors.IGestureEventListener#processGestureEvent
		 * (org.mt4j.input.inputProcessors.MTGestureEvent)
		 */
		public boolean processGestureEvent(MTGestureEvent ge) {
			if (ge instanceof TapEvent) {
	
				TapEvent te = (TapEvent) ge;
				if (te.getTapID() == TapEvent.TAPPED) {
	
					for (PolygonListeners pl : children) {
						pl.component.setPickable(true);
						if (pl.component.getIntersectionGlobal(Tools3D
								.getCameraPickRay(app, pl.component, te
										.getCursor().getPosition().x, te
										.getCursor().getPosition().y)) != null) {
							pl.listener.processGestureEvent(ge);
						} else {
	
						}
						pl.component.setPickable(false);
					}
				}
			}
			return false;
		}
	
	}

	/**
	 * The Class PolygonListeners.
	 */
	public class PolygonListeners {
	
		/** The component. */
		public MTPolygon component;
	
		/** The listener. */
		public IGestureEventListener listener;
	
		/**
		 * Instantiates a new polygon listeners mapping.
		 *
		 * @param component the component
		 * @param listener the listener
		 */
		public PolygonListeners(MTPolygon component,
				IGestureEventListener listener) {
			this.component = component;
			this.listener = listener;
		}
	
	}

}