package de.tu_darmstadt.tk.SmartHomeNetworkSim.view;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.Stroke;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;

import java.util.Collection;
import java.util.HashMap;
import java.util.Observable;
import java.util.Observer;

import javax.swing.JPanel;

import de.tu_darmstadt.tk.SmartHomeNetworkSim.control.LinkColorController;
import de.tu_darmstadt.tk.SmartHomeNetworkSim.control.SettingsController;
import de.tu_darmstadt.tk.SmartHomeNetworkSim.control.Controller;
import de.tu_darmstadt.tk.SmartHomeNetworkSim.control.NetworkController;
import de.tu_darmstadt.tk.SmartHomeNetworkSim.core.Connection;
import de.tu_darmstadt.tk.SmartHomeNetworkSim.core.Link;
import de.tu_darmstadt.tk.SmartHomeNetworkSim.core.Port;
import de.tu_darmstadt.tk.SmartHomeNetworkSim.core.SmartDevice;
import de.tu_darmstadt.tk.SmartHomeNetworkSim.core.util.Pair;
import de.tu_darmstadt.tk.SmartHomeNetworkSim.view.VisualizationInteractor;
import de.tu_darmstadt.tk.SmartHomeNetworkSim.view.util.LinkToolTip;

/**
 * Panel which visualizes the SmartHome Network, it's Devices and Connections
 * 
 *
 * @author Andreas T. Meyer-Berg
 */
@SuppressWarnings("serial")
public class VisualizationPanel extends JPanel implements Observer {

	/**
	 * Controller to notify when the model changes
	 */
	private Controller control;
	
	/**
	 * Settings Controller
	 */
	private SettingsController config;
	
	/**
	 * Network Controller
	 */
	private NetworkController network;
	
	/**
	 * Listener which processes the GUI Interactions
	 */
	private VisualizationInteractor interactor;
	
	/**
	 * Initializes the Visualization Panel
	 * 
	 * @param control
	 *            Control, which changes the model
	 */
	public VisualizationPanel(Controller control) {
		super();

		this.control = control;
		this.config = control.getSettingsController();
		this.network = control.getNetworkController();
		
		this.interactor = new VisualizationInteractor(control, this);
		this.addMouseMotionListener(interactor);
		this.addMouseListener(interactor);
		this.addKeyListener(interactor);
	}

	/**
	 * Performs further initializations, which have to be performed, after the
	 * model was made visible
	 */
	public void delayedInit() {
		this.addComponentListener(new ComponentListener() {

			@Override
			public void componentShown(ComponentEvent e) {
			}

			@Override
			public void componentResized(ComponentEvent e) {
				config.setDimension(getWidth(), getHeight(), control.getSettingsController().getDepth(),
						true);
				repaint();
			}

			@Override
			public void componentMoved(ComponentEvent e) {
			}

			@Override
			public void componentHidden(ComponentEvent e) {
			}
		});
		config.setDimension(getWidth(), getHeight(), control.getSettingsController().getDepth(),
				true);
		repaint();
	}

	@Override
	public void paint(Graphics g) {
		setRenderOptions(g);
		
		// paint white background
		g.setColor(Color.white);
		g.fillRect(0, 0, this.getWidth(), this.getHeight());

		if(config.isShowConnections())
			paintConnections(g);
		if(config.isShowSmartDevices())
			paintDevices(g);
		
		paintDrag(g);
		
		paintToolTip(g);
		
		paintDebugInformation(g);
		
		paintLinkLabels(g);
	}

	/**
	 * Paints debug information like the current interaction mode
	 * @param g Graphics which should be used
	 */
	private void paintDebugInformation(Graphics g) {
		if(!config.isDebugModus())
			return;
		
		/**
		 * Visual representation of the interaction modus
		 */
		String modus;
		switch (interactor.mode) {
		case VisualizationInteractor.DRAG_CONNECTION:
			modus="Drag Connection";
			break;
		case VisualizationInteractor.SELECTED_DRAG:
			modus="Drag Device";
			break;
		case VisualizationInteractor.DRAG_SELECT:
			modus="Drag Select";
			break;
		case VisualizationInteractor.NOTHING:
			modus="Nothing";
			break;
		case VisualizationInteractor.RIGHTCLICK_MENU:
			modus="RightClick Menu";
			break;
		case VisualizationInteractor.SELECTED:
			modus="Selected";
			break;
		default:
			modus="Unknown";
			break;
		}
		g.setColor(Color.RED);
		g.drawString("Debug(Modus:"+modus+")", 0, 10);
		if(interactor.controlDown)
			g.drawString("Control down", 0, 20);
	}
	
	/**
	 * Paints a list of all Link names on the left hand side
	 * @param g Graphics to be used
	 */
	private void paintLinkLabels(Graphics g) {
		if(!config.isShowLinks()||network.getLinks().isEmpty()||!config.isShowLinkNameList())
			return;
		g.setColor(Color.BLACK);
		/**
		 * y-position of the next text Link line
		 */
		int yPositionLink = 10;
		if(config.isDebugModus())
			yPositionLink+=25;
		g.drawString("Links:", 0, yPositionLink);
		yPositionLink += 10;
		for(Link l: network.getLinks()){
			if(!config.getNetworkTreeSettingsController().isVisible(l))continue;
			g.setColor(config.getLinkColors().getColorOfLink(l).getRight());
			g.drawString(l.getName(), 0, yPositionLink);
			yPositionLink+=10;
		}
	}

	/**
	 * Sets the different rending options if available
	 * @param g Graphics to be improved
	 */
	private void setRenderOptions(Graphics g) {
		// More beautiful Graphics
		if(g instanceof Graphics2D){
			/**
			 * Graphics2D options, if enabled on the machine
			 */
			Graphics2D graphics = (Graphics2D) g;
			/**
			 * Rendering Options for the visualization
			 */
			RenderingHints renders = new RenderingHints(new HashMap<>());
			/**
			 * Add AntiAliasing
			 */
			renders.add(new RenderingHints(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_ON));
			/**
			 * Add Text AntiAliasing
			 */
			renders.add(new RenderingHints(RenderingHints.KEY_TEXT_ANTIALIASING,RenderingHints.VALUE_TEXT_ANTIALIAS_ON));
			/**
			 * Add Improved Color Rendering
			 */
			renders.add(new RenderingHints(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY));
			/**
			 * Add general quality Rendering
			 */
			renders.add(new RenderingHints(RenderingHints.KEY_RENDERING,RenderingHints.VALUE_RENDER_QUALITY));
			/**
			 * Add improved interpolation for images
			 */
			renders.add(new RenderingHints(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC));
			graphics.setRenderingHints(renders);
		}
	}

	/**
	 * Paints the smart devices of the Model
	 * 
	 * @param g
	 *            the Graphics object to visualize on
	 */
	protected void paintDevices(Graphics g) {
		/**
		 * Radius of devices
		 */
		int deviceRadius = config.getDeviceVisualizationRadius();
		/*
		 * Visualization calculations for all devices
		 */
		/**
		 * Number of Links in the Model
		 */
		int numberOfLinks = network.getVisibleLinks().size();
		/**
		 * Angle per Link in "linkSlice degrees"
		 */
		double linkSlice = Math.min(360.0 / numberOfLinks,90.0);
		/**
		 * Map of links to Colors and position of the Color
		 */
		LinkColorController linkColors = config.getLinkColors();
		
		if(config.isShowLinks()){
			int i = 0;
			for(Link l: network.getVisibleLinks()){
				/**
				 * Index & Color
				 */
				Pair<Integer, Color> p = config.getLinkColors().getColorOfLink(l);
				p.setLeft(i);
				i++;
			}
		}
		/**
		 * Original Stroke - if it was changed
		 */
		Stroke f = null;
		/**
		 * Set bigger brush size if possible
		 */
		if(g instanceof Graphics2D){
			Graphics2D g2 = (Graphics2D) g;
			f = g2.getStroke(); 
			g2.setStroke(new BasicStroke(Math.max(deviceRadius/20.0f,1f)));
			}
		/**
		 * Paint Devices
		 */
			
		//Values for dragging of multiple Devices
		/**
		 * Pixel offset of dragged devices on the x axis
		 */
		int x_offset = 0;
		/**
		 * Pixel offset of dragged devices on the y axis
		 */
		int y_offset = 0;
		if(interactor.mode == VisualizationInteractor.SELECTED_DRAG && !control.getSettingsController().getConfigurationManager().getSelectionModel().selectedDevices.isEmpty()){
			x_offset = interactor.dragged_x-interactor.dragged.getX();
			y_offset = interactor.dragged_y-interactor.dragged.getY();
		}
		//Paint Devices
		for (SmartDevice s: network.getVisibleSmartDevices()) {
			/**
			 * x Position of the device
			 */
			int x = s.getX();
			/**
			 * y Position of the device
			 */
			int y = s.getY();
			if(interactor.mode==VisualizationInteractor.SELECTED_DRAG && control.getSettingsController().getConfigurationManager().getSelectionModel().selectedDevices.contains(s)) {
				// Update visualization of dragged object
				x += x_offset;
				y += y_offset;
			}
			//Paint Links of Device V0.0
			if(config.isShowLinks()){
				for(Link l:s.getLinks()){
					/*
					 * Split circle around the object into NumberOfLinks Sections, color the sections, connected with the device
					 */
					/**
					 * Number and color of the current link
					 */
					Pair<Integer, Color> linkPos = linkColors.getColorOfLink(l);
					if(linkPos==null||!config.getNetworkTreeSettingsController().isVisible(l))continue;//Skip Links, which are not in the model or hidden
					g.setColor(linkPos.getRight());
					g.fillArc(x - deviceRadius - config.getLinkRadius(), y - deviceRadius - config.getLinkRadius(),
							2 * deviceRadius - 1 + 2 * config.getLinkRadius(), 2 * deviceRadius - 1 + 2*config.getLinkRadius(),
							(int)Math.round(linkPos.getLeft()*linkSlice), (int)Math.ceil(linkSlice));
				}
			
			}
			if(//(interactor.mode==VisualisationInteractor.SELECTED||interactor.mode==VisualisationInteractor.DRAG_SELECT ||interactor.mode == VisualisationInteractor.SELECTED_DRAG || interactor.mode == VisualisationInteractor.RIGHTCLICK_MENU)&&
					(control.getSettingsController().getConfigurationManager().getSelectionModel().selectedDevices.contains(s)^control.getSettingsController().getConfigurationManager().getSelectionModel().selectedDevicesDrag.contains(s))){
				//HighlightSelected Devices
				g.setColor(new Color(135,206,235));
			}else{
				g.setColor(Color.WHITE);
			}
			fillCircle(g, x, y, deviceRadius);
			g.setColor(Color.BLACK);
			drawCircle(g,x,y,deviceRadius);
			g.setColor(new Color(91, 162, 229));
			/**
			 * Distance in pixels between the two circles at the border
			 */
			int innerDistance = Math.max(2, Math.round(deviceRadius/13f));
			drawCircle(g, x, y, deviceRadius-innerDistance);
			g.setColor(Color.BLACK);
			if(config.isShowSmartDeviceNames())
				g.drawString(s.getName(),
					x - g.getFontMetrics().stringWidth(s.getName()) / 2, y
							+ deviceRadius + 11 + (config.isShowLinks()?config.getLinkRadius():0));
		}
		
		if(f!=null)
			((Graphics2D) g).setStroke(f);
	}
	
	/**
	 * Paints the Connections
	 * 
	 * @param g
	 *            the Graphics object to visualize on
	 */
	protected void paintConnections(Graphics g) {
		//Values for dragging of multiple Devices
		int x_offset = 0;
		int y_offset = 0;
		if(interactor.mode == VisualizationInteractor.SELECTED_DRAG && !control.getSettingsController().getConfigurationManager().getSelectionModel().selectedDevices.isEmpty()){
			x_offset = interactor.dragged_x-interactor.dragged.getX();
			y_offset = interactor.dragged_y-interactor.dragged.getY();
		}
		
		// For all Connections
		for (Connection c : network.getVisibleConnections()) {
			Color connectionState;
			switch (c.getStatus()) {
			case Connection.ACTIVE:
				connectionState = Color.GREEN;
				break;
			case Connection.HALTED:
				connectionState = Color.ORANGE;
				break;
			case Connection.DONE:
				connectionState = Color.GRAY;
				break;
			case Connection.TERMINATED:
			case Connection.FINISHED:
				connectionState = Color.RED;
				break;
			default:
				connectionState = Color.BLUE;
				break;
			}
			g.setColor(connectionState);
			/**
			 * All Devices that are part of the connection
			 */
			Collection<Port> d = c.getParticipants();
			if(d.size()==0){
				System.out.println("WARNING: Empty, undeleted Connection: "+c.toString());
				continue;
			}
			if(c.getProtocol()==null){
				System.out.println("WARNING: Protocol is null: "+c.toString());
				continue;
			}
			for(Pair<Port,Port> p: c.getProtocol().getTopology() ){
				if(p.getLeft() != null && p.getLeft().getOwner() != null && p.getRight() != null && p.getRight().getOwner() != null){
					if(control.getSettingsController().getConfigurationManager().getSelectionModel().clickedConnection.contains(new Pair<Connection, Pair<Port,Port>>(c, p)))
						g.setColor(Color.BLUE);
					else
						g.setColor(connectionState);
					SmartDevice from = p.getLeft().getOwner();
					SmartDevice to = p.getRight().getOwner();
					if(!config.getNetworkTreeSettingsController().isVisible(from)||!config.getNetworkTreeSettingsController().isVisible(to))continue;
					int xFrom = from.getX();
					int yFrom = from.getY();
					int xTo = to.getX();
					int yTo = to.getY();
					if(control.getSettingsController().getConfigurationManager().getSelectionModel().selectedDevices.contains(from)^control.getSettingsController().getConfigurationManager().getSelectionModel().selectedDevicesDrag.contains(from)){
						xFrom+=x_offset;
						yFrom+=y_offset;
					}
					if(control.getSettingsController().getConfigurationManager().getSelectionModel().selectedDevices.contains(to)^control.getSettingsController().getConfigurationManager().getSelectionModel().selectedDevicesDrag.contains(to)){
						xTo+=x_offset;
						yTo+=y_offset;
					}
					g.drawLine(xFrom, yFrom, xTo, yTo);
				}
			}
			for(Pair<Port,Port> p: c.getProtocol().getDeletedTopology() ){
				if(control.getSettingsController().getConfigurationManager().getSelectionModel().clickedConnection.contains(new Pair<Connection, Pair<Port,Port>>(c, p)))
					g.setColor(Color.BLUE);
				else{
					//Just paint terminated connection, if config says so
					if(config.isShowTerminatedConnections())
						g.setColor(Color.RED);
					else
						continue;
				
				}
				if(p.getLeft() != null && p.getLeft().getOwner() != null && p.getRight() != null && p.getRight().getOwner() != null){
					SmartDevice from = p.getLeft().getOwner();
					SmartDevice to = p.getRight().getOwner();
					int xFrom = from.getX();
					int yFrom = from.getY();
					int xTo = to.getX();
					int yTo = to.getY();
					if(control.getSettingsController().getConfigurationManager().getSelectionModel().selectedDevices.contains(from)^control.getSettingsController().getConfigurationManager().getSelectionModel().selectedDevicesDrag.contains(from)){
						xFrom+=x_offset;
						yFrom+=y_offset;
					}
					if(control.getSettingsController().getConfigurationManager().getSelectionModel().selectedDevices.contains(to)^control.getSettingsController().getConfigurationManager().getSelectionModel().selectedDevicesDrag.contains(to)){
						xTo+=x_offset;
						yTo+=y_offset;
					}
					g.drawLine(xFrom, yFrom, xTo, yTo);
				}
			}
		}
		// paint new in progress connection, if a connection or link is in
		// creation
		if (interactor.mode == VisualizationInteractor.DRAG_CONNECTION && interactor.connectionFrom != null){
			g.setColor(Color.ORANGE);
			g.drawLine(interactor.connectionFrom.getX(),
					interactor.connectionFrom.getY(), interactor.dragged_x,
					interactor.dragged_y);
		}
	}
	
	/**
	 * Paints the new dragged Connection 
	 * @param g graphics of this panel
	 */
	protected void paintDrag(Graphics g){
		if(interactor.mode!=VisualizationInteractor.DRAG_SELECT)
			return;	
		int low_x = Math.min(interactor.dragged_x, interactor.dragged_x_start);
		int low_y = Math.min(interactor.dragged_y, interactor.dragged_y_start);
		int high_x = Math.max(interactor.dragged_x, interactor.dragged_x_start);
		int high_y = Math.max(interactor.dragged_y, interactor.dragged_y_start);
		g.setColor(Color.BLUE);
		g.drawRect(low_x, low_y, high_x-low_x, high_y-low_y);

	}
	
	/**
	 * Paints the link ToolTip
	 * @param g graphics of this panel
	 */
	protected void paintToolTip(Graphics g) {
		/**
		 * ToolTip which is visualized
		 */
		LinkToolTip toolTip = interactor.getToolTip();

		if (toolTip.isEnabled() && config.isShowLinkToolTips()) {
			/**
			 * free space around the ToolTipText
			 */
			int border = 2;
			/**
			 * Width of the text (multiLine later ?)
			 */
			int textWidth = g.getFontMetrics().stringWidth(toolTip.getText()) + 2 * border;
			/**
			 * Height of the Text
			 */
			int textHeigth = 12 + 2 * border;
			/**
			 * top left x position of the ToolTip
			 */
			int fixXPos = toolTip.getX();
			/**
			 * top left y position of the ToolTip
			 */
			int fixYPos = toolTip.getY();
			
			//Check position that toolTip stays in bound
			if (fixXPos < 0) {
				fixXPos = 0;
			} else if (fixXPos + textWidth >= this.getWidth()) {
				fixXPos -= (fixXPos + textWidth + 1) - this.getWidth();
			}
			if (fixYPos < 0){
				fixYPos = 0;
			}
			else if(fixYPos + textHeigth >= this.getHeight()) {
				fixYPos -= (fixYPos + textHeigth + 1) - this.getHeight();
			}
			//Paint background (toolTip Color (light yellow/brown))
			g.setColor(new Color(255, 255, 225));
			g.fillRect(fixXPos, fixYPos, textWidth, textHeigth);
			//Paint black surrounding rectangle
			g.setColor(Color.BLACK);
			g.drawRect(fixXPos, fixYPos, textWidth, textHeigth);
			//Draw the ToolTip text
			g.drawString(toolTip.getText(), fixXPos + 2, fixYPos + 12);
		}
	}
	
	@Override
	public void update(Observable o, Object arg) {
		repaint();
	}
	
	/**
	 * Fills a circle of radius radius around the given point (Radius includes middle), So width may be 2*radius-1
	 * @param g Graphics to be used
	 * @param x x Position of the middle
	 * @param y y Position of the middle
	 * @param radius radius of the circle
	 */
	public void fillCircle(Graphics g, int x, int y, int radius){
		g.fillOval(x-radius, y-radius, 2*radius+1, 2*radius+1);
	}
	
	/**
	 * Draws a circle of radius radius around the given point (Radius includes middle)
	 * @param g Graphics to be used
	 * @param x x Position of the middle
	 * @param y y Position of the middle
	 * @param radius radius of the circle
	 */
	public void drawCircle(Graphics g, int x, int y, int radius){
		g.drawOval(x-radius, y-radius, 2*radius-1, 2*radius-1);
	}

}
