/***************************************************************************
 * Copyright 2014 Kieker Project (http://kieker-monitoring.net)
 *
 * 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 kieker.webgui.web.utility;

import java.awt.Point;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.annotation.Nullable;

import com.google.common.base.Predicate;
import com.google.common.collect.Collections2;

import kieker.analysis.model.analysisMetaModel.MIDisplayConnector;
import kieker.analysis.model.analysisMetaModel.MIProject;
import kieker.analysis.model.analysisMetaModel.MIView;

/**
 * This is a helper class to manage and modify the layout of the cockpit.
 * 
 * @author Nils Christian Ehmke
 */
public final class CockpitLayout {

	private final Map<MIView, List<List<MIDisplayConnector>>> layout = new HashMap<MIView, List<List<MIDisplayConnector>>>();
	private final int numberColumns;
	private final MIProject project;

	/**
	 * Creates a new instance of this class using the given parameters.
	 * 
	 * @param project
	 *            The corresponding project to the layout.
	 * @param layoutString
	 *            The initial layout string. This parameter is optional and can be null.
	 * @param numberColumns
	 *            The number of columns for the layout. It is assumed that this value is not zero or negative.
	 */
	public CockpitLayout(final MIProject project, final String layoutString, final int numberColumns) {
		this.numberColumns = numberColumns;
		this.project = project;

		if (layoutString != null) {
			this.addProjectViewsAndDisplayConnectors(layoutString);
		} else {
			this.addProjectViewsAndDisplayConnectors();
		}
	}

	/**
	 * Delivers the current layout of the given view. The result is a list of lists. Each of the lists represent a column. Each column contains the display
	 * connectors in the correct order.
	 * 
	 * @param view
	 *            The view whose layout will be delivered.
	 * 
	 * @return A copy of the current layout.
	 */
	public List<List<MIDisplayConnector>> getCurrentLayout(final MIView view) {
		final List<List<MIDisplayConnector>> original = this.layout.get(view);
		final List<List<MIDisplayConnector>> copy = new ArrayList<List<MIDisplayConnector>>();

		if (view != null) {
			for (final List<MIDisplayConnector> columnOriginal : original) {
				final List<MIDisplayConnector> columnCopy = new ArrayList<MIDisplayConnector>();
				columnCopy.addAll(columnOriginal);

				copy.add(columnCopy);
			}
		}

		return copy;
	}

	/**
	 * Adds a view to manage.
	 * 
	 * @param view
	 *            The new view.
	 */
	public final void addView(final MIView view) {
		final List<List<MIDisplayConnector>> columns = new ArrayList<List<MIDisplayConnector>>();
		this.layout.put(view, columns);

		for (int i = 0; i < this.numberColumns; i++) {
			columns.add(new ArrayList<MIDisplayConnector>());
		}
	}

	/**
	 * Removes a view and its layout from this object.
	 * 
	 * @param view
	 *            The view to remove.
	 */
	public final void removeView(final MIView view) {
		this.layout.remove(view);
	}

	/**
	 * Adds a display connector to the layout. It will be added at the end of the given column.
	 * 
	 * @param view
	 *            The corresponding view.
	 * @param displayConnector
	 *            The display connector to add.
	 * @param column
	 *            The column index. It is assumed that this is a valid index.
	 */
	public final void addDisplayConnector(final MIView view, final MIDisplayConnector displayConnector, final int column) {
		this.layout.get(view).get(column).add(displayConnector);
	}

	/**
	 * Moves a display connector from one column to another.
	 * 
	 * @param view
	 *            The corresponding view.
	 * @param displayConnector
	 *            The display connector to move.
	 * @param fromColumn
	 *            The source column.
	 * @param column
	 *            The new column.
	 * @param row
	 *            The new row of the connector.
	 */
	public final void moveDisplayConnector(final MIView view, final MIDisplayConnector displayConnector, final int fromColumn, final int column, final int row) {
		this.layout.get(view).get(fromColumn).remove(displayConnector);
		this.layout.get(view).get(column).add(row, displayConnector);
	}

	/**
	 * Removes a display connector and its layout from this object.
	 * 
	 * @param view
	 *            The corresponding view.
	 * @param displayConnector
	 *            The display connector to remove.
	 */
	public final void removeDisplayConnector(final MIView view, final MIDisplayConnector displayConnector) {
		// Find the correct column and remove the connector
		for (final List<MIDisplayConnector> column : this.layout.get(view)) {
			if (column.contains(displayConnector)) {
				column.remove(displayConnector);
				break;
			}
		}
	}

	/**
	 * Serializes the current layout into a string. The string can be used in the constructor of this class to load a layout.
	 * 
	 * @return A string representation of this layout.
	 */
	public String serializeToString() {
		final Map<MIDisplayConnector, Point> positions = this.extractPositionOfEveryDisplayConnector();
		final StringBuilder builder = new StringBuilder();

		for (final MIView view : this.project.getViews()) {
			for (final MIDisplayConnector displayConnector : view.getDisplayConnectors()) {
				final Point position = positions.get(displayConnector);
				builder.append(position.x).append(' ').append(position.y).append(' ');
			}
		}

		return builder.toString();
	}

	private Map<MIDisplayConnector, Point> extractPositionOfEveryDisplayConnector() {
		final Map<MIDisplayConnector, Point> result = new HashMap<MIDisplayConnector, Point>();

		for (final MIView view : this.project.getViews()) {
			int col = 0;
			for (final List<MIDisplayConnector> column : this.layout.get(view)) {
				int row = 0;
				for (final MIDisplayConnector displayConnector : column) {
					result.put(displayConnector, new Point(col, row));
					row++;
				}
				col++;
			}
		}

		return result;
	}

	private void addProjectViewsAndDisplayConnectors() {
		for (final MIView view : this.project.getViews()) {
			this.addView(view);
			for (final MIDisplayConnector displayConnector : view.getDisplayConnectors()) {
				this.addDisplayConnector(view, displayConnector, 0);
			}
		}
	}

	private void addProjectViewsAndDisplayConnectors(final String layoutString) {
		final String[] layoutElements = layoutString.split(" ");

		final Map<MIView, Map<MIDisplayConnector, Point>> positions = new HashMap<MIView, Map<MIDisplayConnector, Point>>();
		// Extract the position of every connector
		int pos = 0;
		for (final MIView view : this.project.getViews()) {
			final Map<MIDisplayConnector, Point> positionSubMap = new HashMap<MIDisplayConnector, Point>();
			positions.put(view, positionSubMap);

			for (final MIDisplayConnector displayConnector : view.getDisplayConnectors()) {
				final int col = Integer.valueOf(layoutElements[pos]);
				final int row = Integer.valueOf(layoutElements[pos + 1]);
				positionSubMap.put(displayConnector, new Point(col, row));

				pos += 2;
			}
		}

		// We have to sort the display connectors so we can add them in the correct order
		for (final MIView view : this.project.getViews()) {
			this.addView(view);
			for (int col = 0; col < this.numberColumns; col++) {
				final List<MIDisplayConnector> displayConnectors = this.getSortedDisplayConnectorsFromColumn(positions.get(view), col);
				for (final MIDisplayConnector displayConnector : displayConnectors) {
					this.addDisplayConnector(view, displayConnector, col);
				}
			}
		}
	}

	private List<MIDisplayConnector> getSortedDisplayConnectorsFromColumn(final Map<MIDisplayConnector, Point> positions, final int column) {
		final List<MIDisplayConnector> result = new ArrayList<MIDisplayConnector>(Collections2.filter(positions.keySet(), new ColumnFilter(positions, column)));

		Collections.sort(result, new RowComparator(positions));

		return result;
	}

	/**
	 * A comparator to sort the entries of a column using the row indexes.
	 * 
	 * @author Nils Christian Ehmke
	 */
	private static final class RowComparator implements Comparator<MIDisplayConnector> {

		private final Map<MIDisplayConnector, Point> positions;

		/**
		 * Creates a new instance of this class using the given parameters.
		 * 
		 * @param positions
		 *            The map containing the positions of every connector.
		 */
		public RowComparator(final Map<MIDisplayConnector, Point> positions) {
			this.positions = positions;
		}

		@Override
		public int compare(final MIDisplayConnector o1, final MIDisplayConnector o2) {
			final int row1 = this.positions.get(o1).y;
			final int row2 = this.positions.get(o2).y;

			return row1 - row2;
		}

	}

	/**
	 * A filter to get only the entries within a specific column.
	 * 
	 * @author Nils Christian Ehmke
	 */
	private static final class ColumnFilter implements Predicate<MIDisplayConnector> {

		private final Map<MIDisplayConnector, Point> positions;
		private final int column;

		/**
		 * Creates a new instance of this class using the given parameters.
		 * 
		 * @param positions
		 *            The map containing the positions of every connector.
		 * @param column
		 *            The column to filter.
		 */
		public ColumnFilter(final Map<MIDisplayConnector, Point> positions, final int column) {
			this.positions = positions;
			this.column = column;
		}

		@Override
		public boolean apply(@Nullable final MIDisplayConnector displayConnector) {
			return this.positions.get(displayConnector).x == this.column;
		}

	}

}
