/*******************************************************************************
 * Copyright 2019 Observational Health Data Sciences and Informatics
 * 
 * This file is part of WhiteRabbit
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *     http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 ******************************************************************************/
package org.ohdsi.rabbitInAHat;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Hashtable;

import javax.swing.AbstractAction;
import javax.swing.JPanel;
import javax.swing.KeyStroke;

import org.ohdsi.rabbitInAHat.Arrow.HighlightStatus;
import org.ohdsi.rabbitInAHat.dataModel.*;
import org.ohdsi.utilities.collections.IntegerComparator;

public class MappingPanel extends JPanel implements MouseListener, MouseMotionListener {

	private static final long		serialVersionUID			= 4589294949568810155L;

	public static final int				ITEM_HEIGHT					= 50;
	public static final int				ITEM_WIDTH					= 200;
	public static final int 			STEM_ITEM_WIDTH 			= 80;
	public static final int				MARGIN						= 10;
	public static final int				HEADER_HEIGHT				= 25;
	public static final int				HEADER_TOP_MARGIN			= 0;
	public static final int				MIN_SPACE_BETWEEN_COLUMNS	= 200;
	public static final int				ARROW_START_WIDTH			= 50;
	public static final int				BORDER_HEIGHT				= 25;

	private int						sourceX;
	private int						cdmX;
	private int						stemX;

	private Mapping<?>				mapping;
	private List<LabeledRectangle>	sourceComponents			= new ArrayList<LabeledRectangle>();
	private List<LabeledRectangle>	cdmComponents				= new ArrayList<LabeledRectangle>();
	private List<Arrow>				arrows						= new ArrayList<Arrow>();
	private LabeledRectangle		dragRectangle				= null;
	private LabeledRectangle		lastSelectedRectangle		= null;
	private Arrow					dragArrow					= null;
	private Arrow					zoomArrow					= null;
	private Arrow					selectedArrow				= null;
	private LabeledRectangle		dragArrowPreviousTarget		= null;
	private int						dragOffsetY;
	private int						maxHeight					= Integer.MAX_VALUE;
	private boolean					minimized					= false;
	private MappingPanel			slaveMappingPanel;
	private boolean					showOnlyConnectedItems		= false;

	private int						shortcutMask				= Toolkit.getDefaultToolkit().getMenuShortcutKeyMask();

	private String					lastSourceFilter			= "";
	private String					lastTargetFilter			= "";

	private boolean					showingArrowStarts			= false;

	private DetailsListener			detailsListener;

	@SuppressWarnings("serial")
	public MappingPanel(Mapping<?> mapping) {
		super();
		this.mapping = mapping;
		this.setFocusable(true);
		addMouseListener(this);
		addMouseMotionListener(this);

		// Add keybindings to delete arrows
		this.getInputMap(WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0, false), "del pressed");
		this.getInputMap(WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_BACK_SPACE, 0, false), "del pressed");
		this.getActionMap().put("del pressed", new AbstractAction() {
			@Override
			public void actionPerformed(ActionEvent e) {
				if (selectedArrow != null) {
					removeArrow(selectedArrow);
				}
			}
		});

		renderModel();
	}

	public String getLastSourceFilter() {
		return lastSourceFilter;
	}

	public String getLastTargetFilter() {
		return lastTargetFilter;
	}

	public boolean isMinimized() {
		return minimized;
	}

	public void setMapping(Mapping<?> mapping) {
		maximize();
		this.mapping = mapping;
		renderModel();
	}

	public List<LabeledRectangle> getVisibleSourceComponents() {
		return getVisibleRectangles(sourceComponents);
	}

	public List<LabeledRectangle> getVisibleTargetComponents() {
		return getVisibleRectangles(cdmComponents);
	}


	public List<Arrow> getArrows() {
		return arrows;
	}

	public void setSlaveMappingPanel(MappingPanel mappingPanel) {
		this.slaveMappingPanel = mappingPanel;
	}

	public MappingPanel getSlaveMappingPanel() {
		return this.slaveMappingPanel;
	}

	public void setShowOnlyConnectedItems(boolean value) {
		showOnlyConnectedItems = value;
		renderModel();
	}

	private void renderModel() {
		sourceComponents.clear();
		cdmComponents.clear();
		arrows.clear();
		for (MappableItem item : mapping.getSourceItems()) {
			if (!showOnlyConnectedItems || isConnected(item)) {
				if (item.isStem())
					sourceComponents.add(new LabeledRectangle(0, 400, ITEM_WIDTH, ITEM_HEIGHT, item, new Color(160, 0, 160)));
				else
					sourceComponents.add(new LabeledRectangle(0, 400, ITEM_WIDTH, ITEM_HEIGHT, item, new Color(255, 128, 0)));
			}
		}
		for (MappableItem item : mapping.getTargetItems()) {
			if (!showOnlyConnectedItems || isConnected(item)) {
				if (item.isStem())
					cdmComponents.add(new LabeledRectangle(0, 400, ITEM_WIDTH, ITEM_HEIGHT, item, new Color(160, 0, 160)));
				else
					cdmComponents.add(new LabeledRectangle(0, 400, ITEM_WIDTH, ITEM_HEIGHT, item, new Color(128, 128, 255)));
			}
		}
		for (ItemToItemMap map : mapping.getSourceToTargetMaps()) {
			Arrow component = new Arrow(getComponentWithItem(map.getSourceItem(), sourceComponents), getComponentWithItem(map.getTargetItem(), cdmComponents),
					map);
			arrows.add(component);
		}
		layoutItems();
		repaint();
	}

	private boolean isConnected(MappableItem item) {
		for (ItemToItemMap map : mapping.getSourceToTargetMaps())
			if (map.getSourceItem() == item || map.getTargetItem() == item)
				return true;
		return false;
	}

	public void markCompleted() {
		for (Arrow arrow : arrows) {
			if (arrow.isSelected() || arrow.getHighlightStatus() == HighlightStatus.BOTH_SELECTED) {
				arrow.getItemToItemMap().setCompleted(true);
			}
		}
		repaint();
	}

	public void unmarkCompleted() {
		for (Arrow arrow : arrows) {
			if (arrow.isSelected() || arrow.getHighlightStatus() == HighlightStatus.BOTH_SELECTED) {
				arrow.getItemToItemMap().setCompleted(false);
			}
		}
		repaint();
	}

	private LabeledRectangle getComponentWithItem(MappableItem item, List<LabeledRectangle> components) {
		for (LabeledRectangle component : components)
			if (component.getItem().equals(item))
				return component;
		return null;
	}

	// Layout visible LabeledRectangles
	private void layoutItems() {
		if (minimized) { // Only update x coordinate
			for (LabeledRectangle targetComponent : getVisibleTargetComponents()) {
				targetComponent.setLocation(cdmX, targetComponent.getY());
			}
		} else {
			setLabeledRectanglesLocation(getVisibleSourceComponents(), sourceX);
			setLabeledRectanglesLocation(getVisibleTargetComponents(), cdmX);
		}
	}

	// Sets the location of the Labeled Rectangles
	private void setLabeledRectanglesLocation(List<LabeledRectangle> components, int xpos) {
		int avoidY = Integer.MAX_VALUE;
		if (dragRectangle != null && dragRectangle.getX() == xpos)
			avoidY = dragRectangle.getY();
		int y = HEADER_HEIGHT + HEADER_TOP_MARGIN;
		if (ObjectExchange.etl.hasStemTable()) {
			// Move all non-stem items
			y = HEADER_TOP_MARGIN + ITEM_HEIGHT;
		}
		for (LabeledRectangle component : components) {
			// Exception for laying out the stem table
			if (component.getItem().isStem() && component.getItem() instanceof Table) {
				component.setLocation(stemX, HEADER_TOP_MARGIN);
				continue;
			}

			// All other tables and fields
			if (y > avoidY - ITEM_HEIGHT && y <= avoidY + MARGIN)
				y += MARGIN + ITEM_HEIGHT;

			if (dragRectangle == null || component != dragRectangle) {
				component.setLocation(xpos, y);
				y += MARGIN + ITEM_HEIGHT;
			}
		}

	}

	public Dimension getMinimumSize() {
		Dimension dimension = new Dimension();
		dimension.width = 2 * (ITEM_WIDTH + MARGIN) + MIN_SPACE_BETWEEN_COLUMNS;
		dimension.height = Math.min(HEADER_HEIGHT + HEADER_TOP_MARGIN + Math.max(sourceComponents.size(), cdmComponents.size()) * (ITEM_HEIGHT + MARGIN),
				maxHeight);

		return dimension;
	}

	public Dimension getPreferredSize() {
		return getMinimumSize();
	}

	public void setSize(Dimension dimension) {
		setSize(dimension.width, dimension.height);
	}

	public void setSize(int width, int height) {
		sourceX = MARGIN;
		cdmX = width - MARGIN - ITEM_WIDTH;
		stemX = (sourceX + cdmX) / 2;

		layoutItems();
		super.setSize(width, height);
	}

	// Set visibility of components based on a search term
	public void filterComponents(String searchTerm, boolean filterTarget) {
		List<LabeledRectangle> components;

		if (filterTarget) {
			components = cdmComponents;
			lastTargetFilter = searchTerm;
		} else {
			components = sourceComponents;
			lastSourceFilter = searchTerm;
		}

		for (LabeledRectangle c : components) {
			c.filter(searchTerm);
		}

		layoutItems();
		repaint();
	}

	public void paint(Graphics g) {
		Image offscreen = createVolatileImage(getWidth(), getHeight());
		Graphics2D g2d;

		if (offscreen == null) {
			g2d = (Graphics2D) g;
		} else {
			g2d = (Graphics2D) offscreen.getGraphics();
		}

		g2d.setBackground(Color.WHITE);
		g2d.clearRect(0, 0, getWidth(), getHeight());

		RenderingHints rh = new RenderingHints(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
		g2d.setRenderingHints(rh);

		g2d.setColor(Color.BLACK);
		addLabel(g2d, this.getSourceDbName(), sourceX + ITEM_WIDTH / 2, HEADER_TOP_MARGIN + HEADER_HEIGHT / 2);
		addLabel(g2d, this.getTargetDbName(), cdmX + ITEM_WIDTH / 2, HEADER_TOP_MARGIN + HEADER_HEIGHT / 2);

		if (showingArrowStarts && dragRectangle == null) {
			for (LabeledRectangle item : getVisibleSourceComponents())
				Arrow.drawArrowHead(g2d, Math.round(item.getX() + item.getWidth() + Arrow.headThickness), item.getY() + item.getHeight() / 2);
		}

		for (LabeledRectangle component : getVisibleSourceComponents())
			if (component != dragRectangle)
				component.paint(g2d);

		for (LabeledRectangle component : getVisibleTargetComponents())
			if (component != dragRectangle)
				component.paint(g2d);

		for (int i = HighlightStatus.values().length - 1; i >= 0; i--) {
			HighlightStatus status = HighlightStatus.values()[i];
			for (Arrow arrow : arrowsByStatus(status)) {
				if (arrow != dragArrow) {
					arrow.paint(g2d);
				}
			}
		}

		if (dragRectangle != null)
			dragRectangle.paint(g2d);

		if (dragArrow != null)
			dragArrow.paint(g2d);

		if (offscreen != null)
			g.drawImage(offscreen, 0, 0, this);
	}

	private void addLabel(Graphics2D g2d, String string, int x, int y) {
		g2d.setFont(new Font("default", Font.PLAIN, 20));
		FontMetrics fm = g2d.getFontMetrics();
		Rectangle2D r = fm.getStringBounds(string, g2d);
		g2d.drawString(string, x - Math.round(r.getWidth() / 2), y - Math.round(r.getHeight() / 2) + fm.getAscent());
	}

	@Override
	public void mouseClicked(MouseEvent event) {
		// Save away which arrows are currently highlighted vs normal before we
		// de-select all the tables and arrows
		Hashtable<HighlightStatus, List<Arrow>> currentArrowStatus = new Hashtable<HighlightStatus, List<Arrow>>();
		for (HighlightStatus status : HighlightStatus.values()) {
			currentArrowStatus.put(status, arrowsByStatus(status));
		}

		if (selectedArrow != null) {
			selectedArrow.setSelected(false);
			detailsListener.showDetails(null);
			selectedArrow = null;
		}

		if (!event.isShiftDown() && !((event.getModifiersEx() & shortcutMask) == shortcutMask)) {
			for (LabeledRectangle component : cdmComponents) {
				component.setSelected(false);
			}

			for (LabeledRectangle component : sourceComponents) {
				component.setSelected(false);
			}
		}

		boolean clickInSource = event.getX() > sourceX && event.getX() < sourceX + ITEM_WIDTH;
		boolean clickInSourceStem = event.getX() > stemX && event.getX() < stemX + STEM_ITEM_WIDTH;
		boolean clickInTarget = event.getX() > cdmX && event.getX() < cdmX + ITEM_WIDTH;
		boolean clickInTargetStem = event.getX() > stemX && event.getX() < stemX + STEM_ITEM_WIDTH;
		boolean clickInArrow = event.getX() > sourceX + ITEM_WIDTH && event.getX() < cdmX;

		if (clickInSource || clickInSourceStem) {
			LabeledRectangleClicked(event, getVisibleSourceComponents());
		} else if (clickInTarget || clickInTargetStem) {
			LabeledRectangleClicked(event, getVisibleTargetComponents());
		} else if (clickInArrow) {
			lastSelectedRectangle = null;
			Arrow clickedArrow = null;
			for (HighlightStatus status : HighlightStatus.values()) {
				for (Arrow arrow : currentArrowStatus.get(status)) {
					if (arrow.contains(event.getPoint())) {
						clickedArrow = arrow;
						break;
					}
				}
				if (clickedArrow != null) {
					break;
				}
			}

			if (clickedArrow != null) {
				if (event.getClickCount() == 2) { // double click
					zoomArrow = clickedArrow;
					if (slaveMappingPanel != null) {
						slaveMappingPanel.setMapping(ObjectExchange.etl.getFieldToFieldMapping((Table) zoomArrow.getSource().getItem(), (Table) zoomArrow
								.getTarget().getItem()));
						new AnimateThread(true).start();

						slaveMappingPanel.filterComponents("", false);
						slaveMappingPanel.filterComponents("", true);
					}

				} else { // single click
					if (!clickedArrow.isSelected()) {
						clickedArrow.setSelected(true);
						selectedArrow = clickedArrow;
						detailsListener.showDetails(mapping.getSourceToTargetMap(selectedArrow.getSource().getItem(), selectedArrow.getTarget().getItem()));
					}
					repaint();
				}
			} else {
				detailsListener.showDetails(null);
			}
		} else {
			lastSelectedRectangle = null;
			detailsListener.showDetails(null);
		}

	}

	private class AnimateThread extends Thread {
		public int		nSteps	= 10;
		private boolean	minimizing;

		public AnimateThread(boolean minimizing) {
			this.minimizing = minimizing;
		}

		public void run() {
			if (minimizing) {
				LabeledRectangle sourceComponent = zoomArrow.getSource();
				LabeledRectangle targetComponent = zoomArrow.getTarget();

				for (LabeledRectangle component : sourceComponents)
					if (component != sourceComponent)
						component.setVisible(false);

				for (LabeledRectangle component : cdmComponents)
					if (component != targetComponent)
						component.setVisible(false);

				for (Arrow component : arrows)
					if (component != zoomArrow)
						component.setVisible(false);
				minimized = true;
				Path heightPath = new Path(getHeight(), HEADER_TOP_MARGIN + HEADER_HEIGHT + MARGIN + ITEM_HEIGHT + BORDER_HEIGHT);
				Path sourcePath = new Path(sourceComponent.getY(), HEADER_TOP_MARGIN + HEADER_HEIGHT);
				Path targetPath = new Path(targetComponent.getY(), HEADER_TOP_MARGIN + HEADER_HEIGHT);
				for (int i = 0; i < nSteps; i++) {
					maxHeight = heightPath.getValue(i);
					sourceComponent.setLocation(sourceX, sourcePath.getValue(i));
					targetComponent.setLocation(cdmX, targetPath.getValue(i));
					for (ResizeListener resizeListener : resizeListeners)
						resizeListener.notifyResized(maxHeight, false, false);
					try {
						sleep(20);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
				maxHeight = heightPath.getEnd();
				sourceComponent.setLocation(sourceX, sourcePath.getEnd());
				targetComponent.setLocation(cdmX, targetPath.getEnd());
				for (ResizeListener resizeListener : resizeListeners)
					resizeListener.notifyResized(maxHeight, true, false);
			} else { // maximizing

			}
		}

		private class Path {
			private int	start;
			private int	end;
			private int	stepSize;

			public Path(int start, int end) {
				this.start = start;
				this.end = end;
				this.stepSize = (end - start) / nSteps;
			}

			public int getValue(int step) {
				return start + stepSize * step;
			}

			public int getEnd() {
				return end;
			}
		}
	}

	@Override
	public void mouseEntered(MouseEvent event) {

	}

	@Override
	public void mouseExited(MouseEvent event) {
		if (showingArrowStarts) {
			showingArrowStarts = false;
			repaint();
		}
	}

	private void maximize() {
		maxHeight = Integer.MAX_VALUE;
		minimized = false;
		for (ResizeListener resizeListener : resizeListeners)
			resizeListener.notifyResized(maxHeight, false, true);

		filterComponents(lastSourceFilter, false);

		filterComponents(lastTargetFilter, true);

		for (Arrow component : arrows)
			component.setVisible(true);

		this.requestFocusInWindow();
	}

	@Override
	public void mousePressed(MouseEvent event) {
		if (minimized) {
			maximize();
			return;
		}

		if (event.getX() > sourceX + ITEM_WIDTH && event.getX() < sourceX + ITEM_WIDTH + ARROW_START_WIDTH) { // Arrow starts
			for (LabeledRectangle item : getVisibleSourceComponents()) {
				if (event.getY() >= item.getY() && event.getY() <= item.getY() + item.getHeight()) {
					dragArrow = new Arrow(item);
					dragArrow.setTargetPoint(new Point(item.getX() + item.getWidth() + Arrow.headThickness, item.getY() + item.getHeight() / 2));
					showingArrowStarts = false;
					repaint();
					break;
				}
			}
		} else if (event.getX() > cdmX - ARROW_START_WIDTH && event.getX() < cdmX && dragArrow == null) { // Existing arrowheads
			for (Arrow arrow : arrows) {
				if (event.getY() >= arrow.getTarget().getY() && event.getY() <= arrow.getTarget().getY() + arrow.getTarget().getHeight()
						&& arrow.isSourceAndTargetVisible()) {
					dragArrow = arrow;
					dragArrowPreviousTarget = dragArrow.getTarget();
					dragArrow.setTarget(null);
					break;
				}
			}
			if (dragArrow != null) {
				arrows.remove(dragArrow);
			}
			repaint();
		} else {
			for (LabeledRectangle item : getVisibleSourceComponents()) {
				if (item.contains(event.getPoint()) && !isBeingFiltered()) {
					dragRectangle = item;
					dragOffsetY = event.getY() - item.getY();
					break;
				}
			}

			for (LabeledRectangle item : getVisibleTargetComponents()) {
				if (item.contains(event.getPoint()) && !isBeingFiltered()) {
					dragRectangle = item;
					dragOffsetY = event.getY() - item.getY();
					break;
				}
			}
		}
	}

	@Override
	public void mouseReleased(MouseEvent event) {

		if (dragRectangle != null) { // Dragging rectangles to reorder
			if (!isSorted(sourceComponents, new YComparator())) {
				Collections.sort(sourceComponents, new YComparator());
				mapping.setSourceItems(getItemsList(sourceComponents));
			}
			if (!isSorted(cdmComponents, new YComparator())) {
				Collections.sort(cdmComponents, new YComparator());
				mapping.setTargetItems(getItemsList(cdmComponents));
			}
			dragRectangle = null;
			layoutItems();
		} else if (dragArrow != null) { // dragging arrow to set source and target
			boolean arrowInCdm = event.getX() > cdmX - ARROW_START_WIDTH && event.getX() < cdmX + ITEM_WIDTH;
			boolean arrowInStem = event.getX() > stemX - ARROW_START_WIDTH && event.getX() < stemX + STEM_ITEM_WIDTH;
			if (arrowInCdm || arrowInStem) {
				for (LabeledRectangle component : getVisibleRectangles(cdmComponents)) {
					if (component.contains(event.getPoint(), ARROW_START_WIDTH, 0)) {
						dragArrow.setTarget(component);
						if (dragArrow.getTarget() == dragArrowPreviousTarget) {
							arrows.add(dragArrow);
							break;
						}

						makeMapSourceToTarget(dragArrow.getSource(), dragArrow.getTarget());
						break;
					}
				}
			}
			if (dragArrowPreviousTarget != null && dragArrow.getTarget() != dragArrowPreviousTarget) { // Retargeted an existing arrow, remove old map from
																										// model
				mapping.removeSourceToTargetMap(dragArrow.getSource().getItem(), dragArrowPreviousTarget.getItem());
			}
			dragArrowPreviousTarget = null;
			dragArrow = null;
		}
		repaint();
	}

	private List<MappableItem> getItemsList(List<LabeledRectangle> components) {
		List<MappableItem> items = new ArrayList<MappableItem>();
		for (LabeledRectangle component : components)
			items.add(component.getItem());
		return items;
	}

	private boolean isSorted(List<LabeledRectangle> sourceComponents2, Comparator<LabeledRectangle> comparator) {
		for (int i = 0; i < sourceComponents2.size() - 1; i++)
			if (comparator.compare(sourceComponents2.get(i), sourceComponents2.get(i + 1)) < 0)
				return false;
		return true;
	}

	@Override
	public void mouseDragged(MouseEvent event) {
		if (dragRectangle != null) {
			dragRectangle.setLocation(dragRectangle.getX(), event.getY() - dragOffsetY);
			layoutItems();
			repaint();
		}
		if (dragArrow != null) {
			if (event.getX() < sourceX + ITEM_WIDTH + Arrow.headThickness)
				dragArrow.setTargetPoint(null);
			else
				dragArrow.setTargetPoint(event.getPoint());
			repaint();
		}
		this.scrollRectToVisible(new Rectangle(event.getX() - 40, event.getY() - 40, 80, 80));

	}

	@Override
	public void mouseMoved(MouseEvent event) {
		if (event.getX() > sourceX + ITEM_WIDTH && event.getX() < sourceX + ITEM_WIDTH + ARROW_START_WIDTH && dragArrow == null) {
			if (!showingArrowStarts) {
				showingArrowStarts = true;
				repaint();
			}
		} else {
			if (showingArrowStarts) {
				showingArrowStarts = false;
				repaint();
			}
		}
	}

	private class YComparator implements Comparator<LabeledRectangle> {

		@Override
		public int compare(LabeledRectangle o1, LabeledRectangle o2) {
			return IntegerComparator.compare(o1.getY(), o2.getY());
		}

	}

	private List<ResizeListener>	resizeListeners	= new ArrayList<ResizeListener>();

	public void addResizeListener(ResizeListener resizeListener) {
		resizeListeners.add(resizeListener);
	}

	public void removeResizeListener(ResizeListener resizeListener) {
		resizeListeners.remove(resizeListener);
	}

	public void setDetailsListener(DetailsListener detailsListener) {
		this.detailsListener = detailsListener;
	}

	private void removeArrow(Arrow a) {
		arrows.remove(a);
		mapping.removeSourceToTargetMap(a.getSource().getItem(), a.getTarget().getItem());
		repaint();
	}

	private List<Arrow> arrowsByStatus(HighlightStatus status) {
		List<Arrow> matchingArrows = new ArrayList<Arrow>();
		for (Arrow arrow : arrows) {
			if (arrow.getHighlightStatus() == status) {
				matchingArrows.add(arrow);
			}
		}
		return matchingArrows;
	}

	private void LabeledRectangleClicked(MouseEvent event, List<LabeledRectangle> components) {
		int startIndex = 0;
		int endIndex = 0;

		for (LabeledRectangle component : components) {

			if (component.contains(event.getPoint())) {

				if ((event.getModifiersEx() & shortcutMask) == shortcutMask) { // Add one at a time
					component.toggleSelected();
				} else if (event.isShiftDown()) { // Add in consecutive order

					startIndex = Math.min(components.indexOf(lastSelectedRectangle), components.indexOf(component));
					endIndex = Math.max(components.indexOf(lastSelectedRectangle), components.indexOf(component));

					if (startIndex >= 0 && endIndex >= 0) {
						for (int i = startIndex; i <= endIndex; i++) {
							components.get(i).setSelected(true);
						}
					} else {
						component.toggleSelected();
					}

				} else {
					component.setSelected(true);
				}

				if (component.isSelected()) {
					lastSelectedRectangle = component;
				} else {
					lastSelectedRectangle = null;
				}

				boolean isSourceComponent = sourceComponents.contains(component);
				detailsListener.showDetails(component.getItem(), isSourceComponent);
				repaint();

				if (event.getClickCount() == 2 && isSourceComponent && !component.getItem().isStem()) {
					if (slaveMappingPanel != null) {
						// Create dummy mapping
						slaveMappingPanel.setMapping(
								new Mapping<>(((Table) component.getItem()).getFields(), new ArrayList<>(), new ArrayList<>())
						);
						// Zoom arrow has to be set, but will not be shown
						zoomArrow = new Arrow(
								component,
								components.get(0) // random target for the arrow
						);

						new AnimateThread(true).start();

						slaveMappingPanel.filterComponents("", false);
						slaveMappingPanel.filterComponents("", true);
					}
				}
				break;
			}
		}
	}

	private List<LabeledRectangle> getSelectedRectangles(List<LabeledRectangle> components) {

		List<LabeledRectangle> selected = new ArrayList<LabeledRectangle>();

		for (LabeledRectangle c : components) {
			if (c.isSelected()) {
				selected.add(c);
			}
		}

		return selected;
	}

	public void makeMapSelectedSourceAndTarget() {

		for (LabeledRectangle source : getSelectedRectangles(sourceComponents)) {
			for (LabeledRectangle target : getSelectedRectangles(cdmComponents)) {
				makeMapSourceToTarget(source, target);
			}
		}

	}

	private void makeMapSourceToTarget(LabeledRectangle source, LabeledRectangle target) {
		boolean isNew = true;

		for (Arrow other : arrows) {
			if (source == other.getSource() && target == other.getTarget()) {
				isNew = false;
				break;
			}
		}

		if (isNew) {
			Arrow arrow = new Arrow(source);
			arrow.setTarget(target);
			mapping.addSourceToTargetMap(source.getItem(), target.getItem());
			arrow.setItemToItemMap(mapping.getSourceToTargetMap(source.getItem(), target.getItem()));
			arrows.add(arrow);
		}
		repaint();
	}

	public void removeMapSelectedSourceAndTarget() {

		for (LabeledRectangle source : getSelectedRectangles(sourceComponents)) {
			for (LabeledRectangle target : getSelectedRectangles(cdmComponents)) {
				removeMapSourceToTarget(source, target);
			}
		}
	}

	private void removeMapSourceToTarget(LabeledRectangle source, LabeledRectangle target) {

		for (Iterator<Arrow> iterator = arrows.iterator(); iterator.hasNext();) {
			Arrow arrow = iterator.next();
			if (source == arrow.getSource() && target == arrow.getTarget()) {
				iterator.remove();
			}
		}

		mapping.removeSourceToTargetMap(source.getItem(), target.getItem());
		repaint();
	}

	public boolean isMaximized() {
		return !minimized;
	}

	public List<LabeledRectangle> getVisibleRectangles(List<LabeledRectangle> components) {
		List<LabeledRectangle> visible = new ArrayList<LabeledRectangle>();

		for (LabeledRectangle component : components) {

			if (component.isVisible())
				visible.add(component);
		}

		return visible;
	}

	public String getSourceDbName() {
		String resString = "Source";

		if (this.mapping.getSourceItems().size() > 0) {

			if (this.mapping.getSourceItems().get(0).getDb() != null) {
				resString = this.mapping.getSourceItems().get(0).getDb().getDbName();
			}
		}

		return resString;

	}

	public String getTargetDbName() {
		String resString = "Target";

		if (this.mapping.getTargetItems().size() > 0) {

			if (this.mapping.getTargetItems().get(0).getDb() != null) {
				resString = this.mapping.getTargetItems().get(0).getDb().getDbName();
			}
		}

		return resString;
	}

	public boolean isBeingFiltered() {
		return lastSourceFilter != "" || lastTargetFilter != "";
	}
}
