package ui.controller;

import java.awt.Point;
import java.awt.Toolkit;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.StringSelection;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.io.IOException;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.stream.Collectors;

import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonParser;
import com.google.gson.JsonPrimitive;

import classes.AbstractCpsObject;
import classes.CpsEdge;
import classes.CpsUpperNode;
import classes.HolonElement;
import classes.HolonObject;
import classes.HolonSwitch;
import classes.IdCounter;
import classes.IdCounterElem;
import classes.Position;
import ui.controller.SaveController.EDGETYPE;
import ui.controller.SaveController.GRAPHTYPE;
import ui.controller.SaveController.NUMTYPE;
import ui.controller.SaveController.TYPE;
import ui.model.Model;

public class ClipboardController {

	private Model model;
	private SaveController store;
	private LoadController load;
	private CanvasController cvsC;
	private ObjectController objC;
	private NodeController uppC;
	private JsonParser parser;
	private Clipboard clipboard;
	private HashMap<Integer, Integer> objIDMap;
	private HashMap<Integer, Integer> eleIDMap;
	private String sav;
	private Point point;

	public ClipboardController(Model model, SaveController store, LoadController load, CanvasController cvs,
			ObjectController obj, NodeController uppC, MultiPurposeController mp) {
		this.model = model;
		this.store = store;
		this.load = load;
		this.cvsC = cvs;
		this.objC = obj;
		this.uppC = uppC;
		this.clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
		parser = new JsonParser();
	}

	/**
	 * Copy marked Objects into Clipboard in Json Format
	 * @param upperNode
	 */
	public void copy(CpsUpperNode upperNode) {

		JsonObject file = new JsonObject();
		ArrayDeque<AbstractCpsObject> queue = new ArrayDeque<>();
		AbstractCpsObject u = null;

		store.initNumeration();

		file.add("SAV", new JsonPrimitive((upperNode == null ? "CVS" : "" + upperNode.getId())));
		Position pos = uppC.calculatePos(model.getSelectedCpsObjects());
		file.add("CENTER", model.getGson().toJsonTree(pos, Position.class));

		for (AbstractCpsObject abs : model.getSelectedCpsObjects()) {
			queue.add(abs);
		}

		while (!queue.isEmpty()) {

			u = queue.pop();

			String key = "CVSOBJECT" + store.getNumerator(NUMTYPE.OBJECT);
			file.add(key, model.getGson().toJsonTree(u, AbstractCpsObject.class));
			edgeToJson(EDGETYPE.CONNECTION, file, u.getId(), u.getConnections());

			if (u instanceof HolonObject)
				store.elementsToJson(TYPE.CANVAS, file, u);

			if (u instanceof HolonSwitch)
				if (((HolonSwitch) u).getGraphPoints().size() != 0)
					store.unitgraphToJson(GRAPHTYPE.SWITCH, file, u.getId(), ((HolonSwitch) u).getGraphPoints());

			if (u instanceof CpsUpperNode) {
				for (AbstractCpsObject adjacent : ((CpsUpperNode) u).getNodes()) {
					queue.add(adjacent);
				}
				edgeToJson(EDGETYPE.NODE, file, u.getId(), ((CpsUpperNode) u).getNodeEdges());
				edgeToJson(EDGETYPE.OLD, file, u.getId(), ((CpsUpperNode) u).getOldEdges());
			}
		}
		if (upperNode == null)
			edgeToJson(EDGETYPE.LAYER, file, 0, model.getEdgesOnCanvas());
		else
			edgeToJson(EDGETYPE.LAYER, file, upperNode.getId(), upperNode.getNodeEdges());

		StringSelection selection = new StringSelection(model.getGson().toJson(file));
		clipboard.setContents(selection, selection);

	}

	/**
	 * Paste the Copied JsonTree into Canvas
	 * @param upperNode
	 */
	public void paste(CpsUpperNode upperNode, Point p)
			throws UnsupportedFlavorException, IOException, JsonParseException {

		if (p == null)
			return;

		JsonObject json = new JsonObject();
		Transferable content = clipboard.getContents(null);

		if (content != null && content.isDataFlavorSupported(DataFlavor.stringFlavor)
				&& !content.isDataFlavorSupported(DataFlavor.allHtmlFlavor)) {

			String str = (String) content.getTransferData(DataFlavor.stringFlavor);

			if (parser.parse(str).isJsonObject())
				json = (JsonObject) parser.parse(str);
			else
				throw new JsonParseException("Unknown Clipboard Information");

		} else
			return;

		List<String> keys = load.getKeys(json);
		List<String> edges = keys.stream().filter(key -> key.contains("EDGE"))
				.collect(Collectors.toCollection(ArrayList::new));

		HashMap<Integer, AbstractCpsObject> objDispatch = new HashMap<>();
		HashMap<Integer, HolonElement> eleDispatch = new HashMap<>();
		model.getSelectedCpsObjects().clear();

		objIDMap = new HashMap<>();
		eleIDMap = new HashMap<>();
		sav = json.get("SAV").getAsString();

		Position old = model.getGson().getAdapter(Position.class).fromJsonTree(json.get("CENTER"));
		point = new Point(old.x - p.x, old.y - p.y);

		forwardObjects(keys, json, objDispatch, eleDispatch, upperNode);
		// for selecting Cps
		getObjectsInDepth();
		forwardEdges(edges, json, objDispatch, upperNode);

	}
	
	/**
	 * Cuts the marked Objects out of Canvas and saves them into the Clipboard
	 * @param upperNode
	 */
	public void cut(CpsUpperNode upperNode) {
		copy(upperNode);
		for (AbstractCpsObject abs : model.getSelectedCpsObjects()) {
			if (upperNode == null)
				cvsC.deleteObjectOnCanvas(abs);
			else
				uppC.deleteObjectInUpperNode(abs, upperNode);

			if (abs instanceof CpsUpperNode)
				cvsC.bfsNodeCleaner((CpsUpperNode) abs);
		}

	}

	private void forwardEdges(List<String> keys, JsonObject json, HashMap<Integer, AbstractCpsObject> objDispatch,
			CpsUpperNode upperNode) {
		// TODO Auto-generated method stub
		List<String> conn = new ArrayList<>();

		for (String edge : keys) {
			if (edge.contains("LAYEREDGE"))
				loadEdge(EDGETYPE.LAYER, json.get(edge), objDispatch, upperNode);
			if (edge.contains("CONNEDGE"))
				conn.add(edge);
			if (edge.contains("NODEEDGE"))
				loadEdge(EDGETYPE.NODE, json.get(edge), objDispatch, null);
			if (edge.contains("OLDEDGE"))
				loadEdge(EDGETYPE.OLD, json.get(edge), objDispatch, null);
		}

		for (String edge : conn) {
			loadEdge(EDGETYPE.CONNECTION, json.get(edge), objDispatch, null);
		}

	}

	private void forwardObjects(List<String> keys, JsonObject json, HashMap<Integer, AbstractCpsObject> objDispatch,
			HashMap<Integer, HolonElement> eleDispatch, CpsUpperNode upperNode) {
		// TODO Auto-generated method stub

		for (String key : keys) {
			if (key.contains("CVSOBJECT"))
				loadCanvasObject(json.get(key), objDispatch, upperNode);
			if (key.contains("CVSELEMENT"))
				loadCanvasElements(json.get(key), objDispatch, eleDispatch);
			if (key.contains("SWUNITGRAPH"))
				loadUnitGraph(GRAPHTYPE.SWITCH, json.get(key), objDispatch, null);
			if (key.contains("ELEUNITGRAPH"))
				loadUnitGraph(GRAPHTYPE.ELEMENT, json.get(key), null, eleDispatch);
		}
	}

	private void loadCanvasObject(JsonElement jsonElement, HashMap<Integer, AbstractCpsObject> objDispatch,
			CpsUpperNode upperNode) {
		// TODO Auto-generated method stub
		AbstractCpsObject temp = model.getGson().fromJson(jsonElement.getAsJsonObject(), AbstractCpsObject.class);
		load.initObjects(temp);
		objIDMapper(temp);
		updatePosition(temp, upperNode);

		// if its stored before on the canvas just put it there
		if (temp.getSav().equals(sav)) {
			if (upperNode == null)
				cvsC.addObject(temp);
			else
				uppC.addObjectInUpperNode(temp, upperNode);
			// mark the Pasted Objects
			model.getSelectedCpsObjects().add(temp);
		} else {
			// else look up the table and put it into the right Uppernode
			CpsUpperNode temp2 = (CpsUpperNode) objDispatch.get(objIDMap.get(Integer.parseInt(temp.getSav())));
			uppC.addObjectInUpperNode(temp, temp2);
		}

		objDispatch.put(temp.getId(), temp);
	}

	private void loadCanvasElements(JsonElement jsonElement, HashMap<Integer, AbstractCpsObject> objDispatch,
			HashMap<Integer, HolonElement> eleDispatch) {
		// TODO Auto-generated method stub
		JsonObject object = jsonElement.getAsJsonObject();

		HolonElement temp = model.getGson().fromJson(object.get("properties"), HolonElement.class);
		load.initElements(temp);
		eleIDMapper(temp);
		// id which Object it was stored before
		int stored = objIDMap.get(object.get("ID").getAsInt());
		// lookup that object
		HolonObject temp2 = (HolonObject) objDispatch.get(stored);
		// add it
		objC.addElement(temp2, temp);
		// store element also inside a table
		eleDispatch.put(temp.getId(), temp);

	}

	private void loadUnitGraph(GRAPHTYPE type, JsonElement jsonElement, HashMap<Integer, AbstractCpsObject> objDispatch,
			HashMap<Integer, HolonElement> eleDispatch) {
		// TODO Auto-generated method stub
		JsonObject object = jsonElement.getAsJsonObject();
		List<String> keys = load.getKeys(object);
		String p = null;
		int mid, x, y = 0;

		LinkedList<Point> graphpoint = new LinkedList<>();
		int sav = 0;
		// foreach Point in the graph restore it
		for (String k : keys) {
			if (!k.equals("ID")) {
				p = object.get(k).getAsString();
				mid = p.indexOf(':');
				x = Integer.parseInt(p.substring(0, mid));
				y = Integer.parseInt(p.substring(mid + 1, p.length()));
				graphpoint.add(new Point(x, y));
			} else
				// else its an ID
				sav = object.get(k).getAsInt();

		}

		switch (type) {
		case SWITCH:
			sav = objIDMap.get(sav);
			HolonSwitch sw = (HolonSwitch) objDispatch.get(sav);
			sw.setGraphPoints(graphpoint);
			break;
		case ELEMENT:
			sav = eleIDMap.get(sav);
			HolonElement ele = eleDispatch.get(sav);
			ele.setGraphPoints(graphpoint);
			break;
		default:
			break;
		}

	}
	
	/**
	 * 
	 * @param type
	 * @param jsonElement
	 * @param objDispatch
	 * @param upperNode
	 */
	private void loadEdge(EDGETYPE type, JsonElement jsonElement, HashMap<Integer, AbstractCpsObject> objDispatch,
			CpsUpperNode upperNode) {
		// TODO Auto-generated method stub
		JsonObject object = jsonElement.getAsJsonObject();
		CpsEdge temp = model.getGson().fromJson(object.get("properties"), CpsEdge.class);
		load.initCpsEdge(temp);
		// look for A and B inside the Table
		temp.setA(objDispatch.get(objIDMap.get(object.get("A").getAsInt())));
		temp.setB(objDispatch.get(objIDMap.get(object.get("B").getAsInt())));

		int sav = 0;

		switch (type) {
		case LAYER:
			// if in canvas add it into the canvas but delete connection before
			(upperNode == null ? model.getEdgesOnCanvas() : upperNode.getNodeEdges()).add(temp);
			break;
		case CONNECTION:
			// if no duplicates in connection store them into the given A and B
			if (!uppC.lookforDuplicates(temp.getA(), temp.getB(), temp.getA().getConnections()))
				temp.getA().getConnections().add(temp);
			if (!uppC.lookforDuplicates(temp.getA(), temp.getB(), temp.getB().getConnections()))
				temp.getB().getConnections().add(temp);
			break;
		case NODE:
			// put it into given nodeofnode
			sav = objIDMap.get(object.get("ID").getAsInt());
			((CpsUpperNode) objDispatch.get(sav)).getNodeEdges().add(temp);
			break;
		case OLD:
			// same as above
			sav = objIDMap.get(object.get("ID").getAsInt());
			((CpsUpperNode) objDispatch.get(sav)).getOldEdges().add(temp);
			break;
		default:
			break;
		}

		if (object.get("connection").getAsBoolean() && !type.equals(EDGETYPE.CONNECTION)) {
			temp.getA().getConnections().add(temp);
			temp.getB().getConnections().add(temp);
		}

	}

	/**
	 * Modified Method from LoadController. Slightly different
	 * @param type
	 * @param file
	 * @param id
	 * @param arr
	 */
	private void edgeToJson(EDGETYPE type, JsonObject file, int id, ArrayList<CpsEdge> arr) {
		// TODO Auto-generated method stub
		String k = null;
		boolean b = false;
		JsonObject temp = new JsonObject();

		for (CpsEdge edge : arr) {
			if (model.getClipboradObjects().contains(edge.getA())
					&& model.getClipboradObjects().contains(edge.getB())) {
				// add properties and only the ids from a and b
				temp.add("properties", model.getGson().toJsonTree(edge));
				temp.add("A", new JsonPrimitive(edge.getA().getId()));
				temp.add("B", new JsonPrimitive(edge.getB().getId()));

				// Key and occasionally the id of Uppernode
				switch (type) {
				case LAYER:
					temp.add("ID", new JsonPrimitive(id));
					k = "LAYEREDGE" + store.getNumerator(NUMTYPE.EDGE);
					break;
				case CONNECTION:
					k = "CONNEDGE" + store.getNumerator(NUMTYPE.CONNECTION);
					break;
				case NODE:
					temp.add("ID", new JsonPrimitive(id));
					k = "NODEEDGE" + store.getNumerator(NUMTYPE.NODEEDGE);
					break;
				case OLD:
					temp.add("ID", new JsonPrimitive(id));
					k = "OLDEDGE" + store.getNumerator(NUMTYPE.OLDEDGE);
					break;
				default:
					break;
				}
				// lookup if the CVS, NODE or OLDEDGE are also connections
				if (edge.getA().getConnections().contains(edge) && edge.getA().getConnections().contains(edge)
						&& !type.equals(EDGETYPE.CONNECTION))
					b = true;
				temp.add("connection", new JsonPrimitive(b));
				file.add(k, model.getGson().toJsonTree(temp));
				temp = new JsonObject();
			}
		}
	}

	/**
	 * Adds all Objects in Depth into Clipboardobjects preemptive when objects are selected
	 */
	public void getObjectsInDepth() {
		model.setClipboradObjects(new ArrayList<>());
		for (AbstractCpsObject obj : model.getSelectedCpsObjects()) {
			clipboadDepth(obj);
		}
	}

	/**
	 * Get all Objects inside the Currentobject and add them into ClipboardObjects
	 * @param obj
	 */
	private void clipboadDepth(AbstractCpsObject obj) {
		//modified backtracking Algorithm no True/False
		if (!(obj instanceof CpsUpperNode)) {
			model.getClipboradObjects().add(obj);
			return;
		} else {
			model.getClipboradObjects().add(obj);
			for (AbstractCpsObject abs : ((CpsUpperNode) obj).getNodes()) {
				clipboadDepth(abs);
			}
			return;
		}

	}
	
	/**
	 * Map the Copied Object ID into a new One
	 * @param temp
	 */
	private void objIDMapper(AbstractCpsObject temp) {
		// TODO Auto-generated method stub
		int id = temp.getId();
		temp.setId(IdCounter.nextId());
		// oldID -> currentID
		objIDMap.put(id, temp.getId());
	}
	/**
	 * Map the Copied Element ID into a new One
	 * @param temp
	 */
	private void eleIDMapper(HolonElement temp) {
		// TODO Auto-generated method stub
		int id = temp.getId();
		temp.setId(IdCounterElem.nextId());
		// oldID -> currentID
		eleIDMap.put(id, temp.getId());

	}

	private void updatePosition(AbstractCpsObject temp, CpsUpperNode upperNode) {
		// TODO Auto-generated method stub
		int x = temp.getPosition().x - point.x;
		int y = temp.getPosition().y - point.y;

		if (y < 0)
			y = 0 + model.getScaleDiv2() + 1;
		if (upperNode != null) {
			if (x < upperNode.getLeftBorder() + model.getScaleDiv2() + 1)
				x = upperNode.getLeftBorder() + model.getScaleDiv2() + 1;
		} else if (x < 0)
			x = 0 + model.getScaleDiv2() + 1;
		if (x > model.getCanvasX())
			x = model.getCanvasX() - model.getScaleDiv2() - 1;
		if (y > model.getCanvasX())
			y = model.getCanvasY() - model.getScaleDiv2() - 1;

		temp.setPosition(new Position(x, y));

	}

}
