package ui.controller;

import classes.AbstractCpsObject;
import classes.CpsEdge;
import classes.CpsNode;
import classes.CpsUpperNode;
import classes.Position;
import classes.IntermediateCalculationCable;
import ui.model.Model;

import java.awt.*;
import java.util.ArrayList;
import java.util.Iterator;


class NodeController {

	private Model model;
	private CanvasController cvs;
	private MultiPurposeController mpC;
	private Point point;

    NodeController(Model model, CanvasController cvs, MultiPurposeController mpC) {
        this.model = model;
		this.cvs = cvs;
		this.mpC = mpC;
	}

	/**
	 * Add a CpsUpperNode into Canvas
	 */
    void doUpperNode(String nodeName, CpsUpperNode upperNode, ArrayList<AbstractCpsObject> toGroup) {
		CpsUpperNode node = new CpsUpperNode(nodeName);
		node.setPosition(calculatePos(toGroup));
		makeAdjacent(node, upperNode, toGroup);
		makeNodeOfNodes(node, upperNode, toGroup);
		backupOldEdges(node, upperNode);
		if (upperNode == null)
			cvs.addNewObject(node);
		else
			addObjectInUpperNode(node, upperNode, false);

	}

	/**
	 * Delete a CpsUpperNode from the Canvas
	 */
    void undoUpperNode(CpsUpperNode node, CpsUpperNode upperNode) {

		Position old = calculatePos(node.getNodes());
		Position p = node.getPosition();
		point = new Point(old.x - p.x, old.y - p.y);

		restoreOldEdges(node, upperNode);
		unmakeNodesOfNodes(node, upperNode);
		unmakeAdjacent(node, upperNode);
		if (upperNode == null)
			cvs.deleteObjectOnCanvas(node);
		else
			deleteObjectInUpperNode(node, upperNode);
	}

	/**
	 * Put selected Nodes inside the Upper Node
	 */
	private void makeNodeOfNodes(CpsUpperNode node, CpsUpperNode upperNode, ArrayList<AbstractCpsObject> toGroup) {

		ArrayList<AbstractCpsObject> nodes = node.getNodes();
		ArrayList<CpsEdge> toDelete = new ArrayList<>();

		// Put all selected Nodes into the Upper Node
        for (AbstractCpsObject obj : toGroup) {
            // füge Neue Objecte in Neuen Node hinzu
			addObjectInUpperNode(obj, node, false);
		}

		// Füge die Edges die Alle Objekte vorher verbunden hat in neuen Node
        for (CpsEdge edge : (upperNode == null ? model.getEdgesOnCanvas() : upperNode.getNodeEdges())) {

			// kopiere edge in UpperNode
			if (nodes.contains(edge.getA()) && nodes.contains(edge.getB())) {
				toDelete.add(edge);
				addEdge(edge, node);
			}

		}
		// Lösche dann die Edges aus der nächst höheren schicht
		(upperNode == null ? model.getEdgesOnCanvas() : upperNode.getNodeEdges()).removeAll(toDelete);
		
		
		for (AbstractCpsObject abs : toGroup) {
			if (upperNode == null)
				removeForNodeOfNode(abs, null);
			else
				removeForNodeOfNode(abs, upperNode);
		}

	}

	/**
	 * Transfer all relevant data from Node into the next higher layer of Node
	 * (upperNode)
	 */
	private void unmakeNodesOfNodes(CpsUpperNode node, CpsUpperNode upperNode) {
		// add all nodes into upperNode

		for (CpsEdge edge : node.getConnections()) {
			if (edge.getA().equals(node))
				edge.getB().getConnections().remove(edge);
			if (edge.getB().equals(node))
				edge.getA().getConnections().remove(edge);
		}

		for (AbstractCpsObject obj : node.getNodes()) {
			updatePosition(obj, upperNode);
			if (upperNode == null)
				obj.setSav("CVS");
			else
				obj.setSav("" + upperNode.getId());
		}

		(upperNode == null ? model.getObjectsOnCanvas() : upperNode.getNodes()).addAll(node.getNodes());
		// change the indices accordingly the higher layer
		mpC.adjustIdx(mpC.getHighestIdx((upperNode == null ? model.getCvsObjIdx() : upperNode.getNodesIdx())),
				node.getNodesIdx());
		// add all indices of nodes into upperNode
		(upperNode == null ? model.getCvsObjIdx() : upperNode.getNodesIdx()).putAll(node.getNodesIdx());
		// add all Edges of node into upperNode
		(upperNode == null ? model.getEdgesOnCanvas() : upperNode.getNodeEdges()).addAll(node.getNodeEdges());
	}

	/**
	 * Look for adjacent Nodes connected with the new CpsUpperNode and make
	 * Connections
	 */
	private void makeAdjacent(CpsUpperNode node, CpsUpperNode upperNode, ArrayList<AbstractCpsObject> toGroup) {

		ArrayList<AbstractCpsObject> adj = new ArrayList<>();
		ArrayList<Float> maxCapacity = new ArrayList<>();

		ArrayList<CpsEdge> oldEdges = node.getOldEdges();
		ArrayList<CpsEdge> toDelete = new ArrayList<>();

		// für alle Edges auf dem Canvas
		for (Iterator<CpsEdge> it = (upperNode == null ? model.getEdgesOnCanvas() : upperNode.getNodeEdges())
				.iterator(); it.hasNext();) {

			CpsEdge edge = it.next();

			// wenn ausgewählte Objekte in edge und andere auserhalb
			if (toGroup.contains(edge.getA()) && !toGroup.contains(edge.getB())) {
				// und wenn in der adj noch nicht vorhanden und füg
				if (!adj.contains(edge.getB())) {
					adj.add(edge.getB());
					maxCapacity.add(edge.getCapacity());
				}
				// wenn vorhanden teste maxCapacity
				else if (maxCapacity.get(adj.indexOf(edge.getB())) < edge.getCapacity()) {
					maxCapacity.set(adj.indexOf(edge.getB()), edge.getCapacity());
				}
				// speichere alte edge
				oldEdges.add(edge);
				toDelete.add(edge);

			}

			// Analog
			else if (!toGroup.contains(edge.getA()) && toGroup.contains(edge.getB())) {
				if (!adj.contains(edge.getA())) {
					adj.add(edge.getA());
					maxCapacity.add(edge.getCapacity());
				} else if (maxCapacity.get(adj.indexOf(edge.getA())) < edge.getCapacity()) {
					maxCapacity.set(adj.indexOf(edge.getA()), edge.getCapacity());

				}
				oldEdges.add(edge);
				toDelete.add(edge);
			}
		}

		(upperNode == null ? model.getEdgesOnCanvas() : upperNode.getNodeEdges()).removeAll(toDelete);
		
	}

	/**
	 * Restore the Old Connections from the adjacent Object
	 */
	private void unmakeAdjacent(CpsUpperNode node, CpsUpperNode upperNode) {
		ArrayList<CpsEdge> toDelete = new ArrayList<>();
		ArrayList<CpsEdge> lostEdges = new ArrayList<>();
		ArrayList<CpsEdge> foundEdges = (upperNode == null ? model.getEdgesOnCanvas() : upperNode.getNodeEdges());
		ArrayList<AbstractCpsObject> lostChildren = new ArrayList<>();
		ArrayList<AbstractCpsObject> foundChildren = (upperNode == null ? model.getObjectsOnCanvas()
				: upperNode.getNodes());

		// für jede Edge aus upperNode die Node enthält tu sie in toDelete
        for (CpsEdge edge : foundEdges) {
            if (edge.getA().equals(node) || edge.getB().equals(node))
				toDelete.add(edge);

		}
		// mark all object that arent in the restored oldEdges
		for (CpsEdge edge : node.getOldEdges()) {
			if (node.getNodes().contains(edge.getA()) && !foundChildren.contains(edge.getB())
					&& !lostEdges.contains(edge)) {
				lostChildren.add(edge.getA());
				lostEdges.add(edge);
			}

			else if (node.getNodes().contains(edge.getB()) && !foundChildren.contains(edge.getA())
					&& !lostEdges.contains(edge)) {
				lostChildren.add(edge.getB());
				lostEdges.add(edge);
			}
		}
		node.getOldEdges().removeAll(lostEdges);

		// für alle Edges für die nix passendes gefunden ist
		for (CpsEdge edge : lostEdges) {
			AbstractCpsObject toSearch = null;
			AbstractCpsObject lost = null;
			boolean foundCps = false;

			// bestimmung welcher verloren ist und wen man suchen muss
			if (lostChildren.contains(edge.getA())) {
				toSearch = edge.getB();
				lost = edge.getA();
			} else if (lostChildren.contains(edge.getB())) {
				toSearch = edge.getA();
				lost = edge.getB();
			}
			// wenn der zu suchende ein CpsUpperNode war
			if (toSearch instanceof CpsUpperNode) {

				System.out.println("UpperNode!");
				// guck einfach in den Connections des Verlorenen nach Edges die
				// auf der Canvas sind.
				for (CpsEdge e : lost.getConnections()) {
					if (foundChildren.contains(e.getA()) && foundChildren.contains(e.getB())
							&& !node.getOldEdges().contains(e) && !foundEdges.contains(e)) {
						node.getOldEdges().add(e);
						foundCps = true;
					}

				}

			}
			// wenn das verlorene Object nicht gefunden
			if (!foundCps)
				// für alle auf der Ebene liegende Objekte
				outerLoop: for (AbstractCpsObject cps : foundChildren) {
				if (!cps.equals(node) && !lostChildren.contains(cps))
					// such per Backtracking Algorithmus ob der enthalten ist
					if (backtrackLostChild(cps, toSearch, lost)) {
					// wenns stimmt dann überspringe den aktuellen Loop
					for (CpsEdge f : node.getOldEdges())
					if ((f.getA().equals(cps) && f.getB().equals(toSearch)) || (f.getB().equals(cps) && f.getA().equals(toSearch)))
					continue outerLoop;
					// guck nach duplikaten in den Old Edges
					if (!lookforDuplicates(cps, lost, node.getOldEdges())) {
					// wenn es keine gibt erzeuge eine neue Oldedge
					CpsEdge temp = new CpsEdge(cps, lost, edge.getCapacity());
					node.getOldEdges().add(temp);
					}
					// falls das Objekt ein CpsUpperNode gewesen ist verschiebe
					// bitte die OldEdge dahin
					if (cps instanceof CpsUpperNode) {
					((CpsUpperNode) cps).getOldEdges().add(edge);
					}

					}
				}

		}

		// lösch alle Edges die in toDelete sind und wiederherstell alte Edges
		(upperNode == null ? model.getEdgesOnCanvas() : upperNode.getNodeEdges()).removeAll(toDelete);
		(upperNode == null ? model.getEdgesOnCanvas() : upperNode.getNodeEdges()).addAll(node.getOldEdges());

	}

	/**
	 * Just checking if an Egde already exists
	 */
    boolean lookforDuplicates(AbstractCpsObject a, AbstractCpsObject b, ArrayList<CpsEdge> list) {
        for (CpsEdge cpsEdge : list) {
			if ((a.equals(cpsEdge.getA()) && b.equals(cpsEdge.getB()))
					|| (b.equals(cpsEdge.getA()) && a.equals(cpsEdge.getB())))
				return true;
		}
		return false;
	}

	/**
	 * Backups Old Edges into next lower layer in Current UpperNode anymore
	 */
	private void backupOldEdges(CpsUpperNode node, CpsUpperNode upperNode) {

		ArrayList<CpsEdge> backup = new ArrayList<>();
		ArrayList<AbstractCpsObject> adj = new ArrayList<>();
		ArrayList<Float> maxCapacity = new ArrayList<>();

		// wenn upperNode in UpperNode erzeugt wird
		if (upperNode != null) {
			// für alle oldEdges die die zu gruppierenden nodes enthält werden
			// in die neuen upperNOde verschoben
			for (CpsEdge edge : upperNode.getOldEdges()) {
				if (!upperNode.getNodes().contains(edge.getA()) && !upperNode.getNodes().contains(edge.getB()))
					backup.add(edge);
			}
			// hier wird verschoben
			node.getOldEdges().addAll(backup);

			// neue adjazent muss hergestellt werden in den alten oldEdges
//			for (CpsEdge edge : upperNode.getOldEdges()) {
//
//				if (node.getNodes().contains(edge.getA()))
//					if (!adj.contains(edge.getB())) {
//						adj.add(edge.getB());
//						maxCapacity.add(edge.getCapacity());
//					} else if (maxCapacity.get(adj.indexOf(edge.getB())) < edge.getCapacity()) {
//						maxCapacity.set(adj.indexOf(edge.getB()), edge.getCapacity());
//
//					}
//				if (node.getNodes().contains(edge.getB()))
//					if (!adj.contains(edge.getA())) {
//						adj.add(edge.getA());
//						maxCapacity.add(edge.getCapacity());
//					} else if (maxCapacity.get(adj.indexOf(edge.getA())) < edge.getCapacity()) {
//						maxCapacity.set(adj.indexOf(edge.getA()), edge.getCapacity());
//
//					}
//			}
			// alle übertragenen Edges werden gelöscht
			upperNode.getOldEdges().removeAll(backup);
			// neue edges werden erzeugt
			for (AbstractCpsObject cps : adj)
				upperNode.getOldEdges().add(new CpsEdge(cps, node, maxCapacity.get(adj.indexOf(cps))));

		}
	}

	/**
	 * Restore Old Edges into next higher layer of UpperNode
	 */
	private void restoreOldEdges(CpsUpperNode node, CpsUpperNode upperNode) {

		ArrayList<CpsEdge> toDelete = new ArrayList<>();

		if (upperNode != null) {
			for (CpsEdge edge : node.getOldEdges()) {
				// wenn upperNode weder A oder B von von den OldEdges enthält
				if (!upperNode.getNodes().contains(edge.getA()) && !upperNode.getNodes().contains(edge.getB()))
					// Übertrage die OldEdges eine Schicht höher *
					toDelete.add(edge);
			}
			// *übertrage sie hier
			upperNode.getOldEdges().addAll(toDelete);
			// lösch sie aus den zu löschenden node, da sonst durch unmake
			// Adjacent probleme auftauchen
			node.getOldEdges().removeAll(toDelete);
			toDelete.clear();
			// lösche alle OldEdges aus der höheren schicht die zu den zu
			// löschenden Node vorher führten
			for (CpsEdge edge : upperNode.getOldEdges()) {
				if (edge.getA().equals(node) || edge.getB().equals(node))
					toDelete.add(edge);
			}
			upperNode.getOldEdges().removeAll(toDelete);
		}

	}

	/**
	 * Calculate new Position of the Upper Node
	 */
    Position calculatePos(ArrayList<AbstractCpsObject> toGroup) {

		Position pos = new Position(0, 0);

		// sum(x0 .. xn) / numOfPos, y analog
		for (AbstractCpsObject abs : toGroup) {
			pos.x += abs.getPosition().x;
			pos.y += abs.getPosition().y;
		}
		pos.x /= toGroup.size();
		pos.y /= toGroup.size();

		return pos;
	}

	/**
	 * Removes the Given Obj from current Layer and adjusts the idx
	 */
	private void removeForNodeOfNode(AbstractCpsObject obj, CpsUpperNode upperNode) {

		mpC.decIdx(obj.getId(), (upperNode == null ? model.getCvsObjIdx() : upperNode.getNodesIdx()));
		(upperNode == null ? model.getCvsObjIdx() : upperNode.getNodesIdx()).remove(obj.getId());
		(upperNode == null ? model.getObjectsOnCanvas() : upperNode.getNodes()).remove(obj);
	}

	/**
	 * Search Lost Nodes by Backtracking by looking for edges
	 * 
	 * @param object
	 *            actual object which we are looking at
	 * @param find
	 *            one part of the Edge
	 * @param lost
	 *            other part of the Edge
	 * @return boolean if found
	 */
	private boolean backtrackLostChild(AbstractCpsObject object, AbstractCpsObject find, AbstractCpsObject lost) {
		//if not instance of CpsUpperNode
		if (!(object instanceof CpsUpperNode)) {
			//forEach connection look if lost and found inside
			for (CpsEdge edge : object.getConnections()) {
				if (edge.getA().equals(find) && edge.getB().equals(lost)
						|| edge.getA().equals(lost) && edge.getB().equals(find))
					return true;
			}
			return false;
		} else {
			//if instance of Uppernode Call again
			for (AbstractCpsObject cps : ((CpsUpperNode) object).getNodes()) {
				if (backtrackLostChild(cps, find, lost))
					return true;
			}
			return false;
		}
	}


	/**
	 * Adds object to the upperNode, might replace objects if replace==true
	 * @param object
	 * @param upperNode
	 * @param replace
	 */
    void addObjectInUpperNode(AbstractCpsObject object, CpsUpperNode upperNode, boolean replace) {
        if(object == null){
        		new Error("object == null while adding to "+upperNode.toString()).printStackTrace();
        		return;
        	}
        if(upperNode == null){
    		new Error("upperNode == null while adding "+object.toString()).printStackTrace();
    		return;
    	}
    	object.setSav("" + upperNode.getId());
		upperNode.getNodesIdx().put(object.getId(), upperNode.getNodes().size());
		upperNode.getNodes().add(object);
		
		/**
		 * check if we should drag & drop replace
		 */
		if(replace && !(object instanceof CpsNode) ){
			/** x of the dragged Object */
			int x = object.getPosition().x;
		
			/** y of the dragged Object */
			int y = object.getPosition().y;
			
			/** distance threshold for replacement */
			int treshhold = model.getScale()/2;
		
			/** number of Objects that might be replaced (should be 1) */
			int replaceCounter = 0;
		
			/** last object that could be replaced */
			AbstractCpsObject toBeReplaced = null;
			
			/** for each cps on Canvas */
			for (AbstractCpsObject cps : upperNode.getNodes()){
				
				/** same object -> ignore */
				if(cps == object)continue;
			
				/** x of object that might get replaced */
				int c_x = cps.getPosition().x;
				
				/** y of object that might get replaced */
				int c_y = cps.getPosition().y;
				
				/** if near enough */
				if(Math.abs(x-c_x)<treshhold && Math.abs(y-c_y)<treshhold){
					replaceCounter++;
					toBeReplaced = cps;
				}
			}
			/** if replacement of exactly one object possible */
			if(replaceCounter == 1 && toBeReplaced != null){
				replaceObjectInUpperNode(toBeReplaced, object, upperNode);
			}
		}
	}

	/**
	 * Delete a AbstactCpsObject from CPSUpperNode
	 */
    void deleteObjectInUpperNode(AbstractCpsObject object, CpsUpperNode upperNode) {
        CpsEdge e = null;
		for (AbstractCpsObject cps : upperNode.getNodes()) {
			for (CpsEdge p : cps.getConnections()) {
				if (p.getA() == object || p.getB() == object) {
					e = p;
				}
			}
			if (!model.getClipboradObjects().contains(cps) && !(cps instanceof CpsUpperNode)) {
				cps.getConnectedTo().remove(e);
			}
			upperNode.getNodeEdges().remove(e);
		}

		mpC.decIdx(object.getId(), upperNode.getNodesIdx());
		upperNode.getNodesIdx().remove(object.getId());
		upperNode.getNodes().remove(object);
	}

    /**
     * Replaces {@code toBePlaced} by {@code by} in {@code upperNode}
     * @param toBeReplaced
     * @param by
     * @param upperNode
     */
	public void replaceObjectInUpperNode(AbstractCpsObject toBeReplaced,
			AbstractCpsObject by, CpsUpperNode upperNode) {
		/** let all edges of 'toBeReplaced' connect to 'by' */
		for(CpsEdge e: toBeReplaced.getConnections()){
			if(e.getA() == toBeReplaced){
				e.setA(by);
			}else if(e.getB() == toBeReplaced){
				e.setB(by);
			}
			
			/** if edge from an object to itself -> remove it */
			if(e.getA() == e.getB())
				deleteEdge(e, upperNode);
			else/** else add edge to 'by' */
				by.addConnection(e);
		}
		/** delete 'toBeReplaced' new empty connections, to prevent Nullpointer*/
		toBeReplaced.setConnections(new ArrayList<CpsEdge>(1));
		/**
		 * set Position of by to exactly toBeReplaced
		 */
		by.setPosition(toBeReplaced.getPosition());
		deleteObjectInUpperNode(toBeReplaced, upperNode);
	}

	/**
	 * Add Edge in UpperNode
	 */
    void addEdge(CpsEdge edge, CpsUpperNode upperNode) {
        upperNode.getNodeEdges().add(edge);
	}

	/**
	 * Delete Edge in UpperNode
	 */
    void deleteEdge(CpsEdge edge, CpsUpperNode upperNode) {
        edge.getA().getConnections().remove(edge);
		edge.getB().getConnections().remove(edge);
		upperNode.getNodeEdges().remove(edge);
		if (edge.getA() instanceof CpsUpperNode || edge.getB() instanceof CpsUpperNode)
			cvs.handleUpperNodes(edge);
	}

	/**
	 * Connect Objects outside of current Layer to Objects inside the Layer
	 */
    void connectNodes(CpsEdge edge, CpsUpperNode upperNode) {
        // keine Connections nötig, da automatisch erzeugt bei Instanziierung
		// mache diese Edge auch in OldEdges rein des aktuellen CpsUpperNodes
		upperNode.getOldEdges().add(edge);
	}
	
	/**
	 * Disconnect Objects outside of current Layer to Objects inside the Layer
	 */
    void disconnectNodes(CpsEdge edge, CpsUpperNode upperNode) {
        edge.getA().getConnections().remove(edge);
		edge.getB().getConnections().remove(edge);
		if (upperNode != null)
			upperNode.getOldEdges().remove(edge);
	}
	
	/**
	 * If Position is out of boundaries adjust it
	 */
	private void updatePosition(AbstractCpsObject temp, CpsUpperNode upperNode) {
		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));

	}
}
