/*
 * Copyright 2012 s_wolff.
 *
 * 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.aegee.runanddine.pathfinder.optimizers;

import static org.aegee.runanddine.pathfinder.optimizers.AbstractGroups.getGuest1;
import static org.aegee.runanddine.pathfinder.optimizers.AbstractGroups.getGuest2;
import static org.aegee.runanddine.pathfinder.optimizers.AbstractGroups.getMainCourseLocation;
import static org.aegee.runanddine.pathfinder.optimizers.GLPK.invokeGLPK;
import static org.aegee.runanddine.pathfinder.optimizers.GLPK.writeGLPKProgram;

import java.io.File;
import java.io.IOException;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.aegee.runanddine.pathfinder.Course;
import org.aegee.runanddine.pathfinder.DistanceMatrix;
import org.aegee.runanddine.pathfinder.DistanceMatrixManager;
import org.aegee.runanddine.pathfinder.OptGroup;
import org.aegee.runanddine.runanddine.RunAndDine;
import org.aegee.runanddine.util.data.ModelNotExistingException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.wicket.request.http.flow.AbortWithHttpErrorCodeException;

/**
 * Takes groups and optimizes the assignment: mapping abs to opt groups
 * 
 * @author s_wolff
 */
public class PathOptimizer
{
	private static final Log LOG = LogFactory.getLog(PathOptimizer.class);

	private PathOptimizer()
	{
	}

	/**
	 * Optimize group assignment for the saved OptGroups of the given RunAndDine
	 * 
	 * @param event
	 *            RunAndDine the group assignment shall be optimized for
	 */
	public static void optimizeGroupAssignment(RunAndDine event)
	{
		try
		{
			Collection<OptGroup> groups = OptGroup.OBJECTS.getAllByRunAndDineId(event.getId());
			DistanceMatrix dm = DistanceMatrixManager.getInstance().getByRunAndDineId(event.getId());
			optimizeGroupAssignment(dm, groups);
		}
		catch (IOException e)
		{
			// TODO: do something clever
			LOG.error("IOException: " + e.getMessage());
		}
		catch (ModelNotExistingException e)
		{
			LOG.error("ModelNotExistingException");
			throw new AbortWithHttpErrorCodeException(500);
		}
	}

	/**
	 * Optimize group assignment based on the given DistanceMatrix and the list of OptGroups
	 * 
	 * @param dm
	 *            DistanceMatrix containing the distances between all OptGroups
	 * @param groups
	 *            OptGroups to be used for optimization
	 * @throws IOException
	 */
	private static void optimizeGroupAssignment(DistanceMatrix dm, Collection<OptGroup> groups) throws IOException
	{
		// create opt group wrappers
		List<OptGroupWrapper> optGroupWrappers = OptGroupWrappers.makeWrappers(groups);

		// create distance matrix for optimization
		double[][] dists = new double[groups.size()][groups.size()];
		for (int i = 0; i < dists.length; i++)
		{
			for (int j = 0; j < dists[i].length; j++)
			{
				dists[i][j] = dm.getDistance(optGroupWrappers.get(i).getWrappedOptGroup(), optGroupWrappers.get(j)
					.getWrappedOptGroup());
			}
		}

		// solve lp
		optimize(groups.size(), dists, optGroupWrappers);
	}

	/**
	 * Optimize group assignment for OptGroupWrappers
	 * 
	 * @param numberOfGroups
	 *            Number of groups to optimized
	 * @param dists
	 *            Matrix containing the distances between groups
	 * @param optGroupWrappers
	 *            List with all OptGroupWrappers to be optimized
	 * @throws IOException
	 */
	private static void optimize(int numberOfGroups, double[][] dists, List<OptGroupWrapper> optGroupWrappers)
		throws IOException
	{
		Collection<AbstractGroup> abstractGroups = AbstractGroups.makeAbstractGroups(numberOfGroups);

		// and create known mapping for GLPK action
		Map<Integer, Integer> known = getKnownMapping(abstractGroups, optGroupWrappers);

		// create walking distances matrix
		double[][] wd = new double[numberOfGroups][numberOfGroups];
		for (int i = 0; i < numberOfGroups; i++)
		{
			for (int j = 0; j < numberOfGroups; j++)
			{
				if (i == j)
				{
					wd[i][j] = 0;
				}
				else
				{
					wd[i][j] = dists[j][known.get(getMainCourseLocation(i, abstractGroups))];
					wd[i][j] = dists[j][known.get(getMainCourseLocation(getGuest1(i, abstractGroups), abstractGroups))];
					wd[i][j] = dists[j][known.get(getMainCourseLocation(getGuest2(i, abstractGroups), abstractGroups))];
				}
			}
		}

		// write program and solve it
		File programFile = writeGLPKProgram(numberOfGroups, wd, known);
		Map<Integer, Integer> mapping = invokeGLPK(programFile);

		OptGroupWrappers.setOptGroupWrapperMappings(optGroupWrappers, mapping);
		OptGroupWrappers.setOptGroupAssignments(optGroupWrappers, abstractGroups);
	}

	/**
	 * @TODO: add JavaDoc
	 * @param abstractGroups
	 * @param optGroupWrappers
	 * @return
	 */
	private static Map<Integer, Integer> getKnownMapping(Collection<AbstractGroup> abstractGroups,
		Collection<OptGroupWrapper> optGroupWrappers)
	{
		// get abstract groups cooking main course
		Collection<AbstractGroup> abstractMainCourseChefs = AbstractGroups.getMainCourseChefs(abstractGroups);
		Iterator<AbstractGroup> abstractMainCourseChefsIterator = abstractMainCourseChefs.iterator();

		// create known mapping
		HashMap<Integer, Integer> known = new HashMap<Integer, Integer>(abstractGroups.size());
		for (OptGroupWrapper og : optGroupWrappers)
		{
			if (og.getWrappedOptGroup().getCourse() == Course.MAIN)
			{
				int absId = abstractMainCourseChefsIterator.next().getId();
				known.put(absId, og.getId());
			}
		}
		return known;
	}
}