/*
 * Copyright (c) 2011, Patrik Dufresne. All rights reserved.
 * Patrik Dufresne PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 */
package com.planimod.core;

import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.hibernate.criterion.Criterion;
import org.hibernate.criterion.Restrictions;

import com.patrikdufresne.managers.AbstractManager;
import com.patrikdufresne.managers.ManagerContext;
import com.patrikdufresne.managers.ManagerException;
import com.patrikdufresne.managers.Query;

/**
 * This manager is used to manage production event.
 * 
 * @author Patrik Dufresne
 * 
 */
public class ProductionEventManager extends AbstractManager<ProductionEvent> {

	private static Criterion startEndCriterion(Date start, Date end) {
		// (start < event.start < end)
		// OU
		// (start < event.end < end)
		// OU
		// (event.start < start ET event.end > end)
		return Restrictions.or(Restrictions.or(Restrictions.between(
				AbstractCalendarEvent.START_DATE, start, end), Restrictions
				.between(AbstractCalendarEvent.END_DATE, start, end)),
				Restrictions.and(Restrictions.lt(
						AbstractCalendarEvent.START_DATE, start), Restrictions
						.gt(AbstractCalendarEvent.END_DATE, end)));
	}

	public ProductionEventManager(PlanimodManagers managers) {
		super(managers);
	}

	/**
	 * Used to copy production events from one week to another week.
	 * 
	 * @param events
	 *            the production event to copy
	 * @param destWeek
	 *            the destination week
	 * @param overwrite
	 *            True to overwrite the destination week
	 * @return A map between the old event and the new event
	 * @throws ManagerException
	 */
	public Map<ProductionEvent, ProductionEvent> copy(
			final Collection<ProductionEvent> events, final Date destWeek,
			final boolean overwrite) throws ManagerException {
		return copy(null, events, destWeek, overwrite);
	}

	/**
	 * Used to copy the production event occurring within the given time range.
	 * <p>
	 * This function will merge the source and destination IF the event already
	 * exists at the destination.
	 * 
	 * @param shifts
	 *            the shifts to also copy or null
	 * @param events
	 *            the planif events object to be copied.
	 * @param destWeek
	 *            the destination week
	 * @param overwrite
	 *            True to overwrite existing events
	 * @throws ManagerException
	 */
	public Map<ProductionEvent, ProductionEvent> copy(
			final Collection<Shift> shifts,
			final Collection<ProductionEvent> events, final Date destWeek,
			final boolean overwrite) throws ManagerException {
		return getManagers().query(
				new Query<Map<ProductionEvent, ProductionEvent>>() {
					@Override
					public Map<ProductionEvent, ProductionEvent> run()
							throws ManagerException {
						/*
						 * Check if the destination week is empty.
						 */
						int firstDayOfWeek = getManagers()
								.getApplicationSettingManager()
								.getFirstDayOfWeek();
						List<ProductionEvent> existings = list(TimeRanges
								.getWeekStart(destWeek, firstDayOfWeek),
								TimeRanges.getWeekEnd(destWeek,
										firstDayOfWeek));
						if (existings.size() > 0) {
							if (!overwrite) {
								throw new ManagerException(
										"Destination week is not empty."); //$NON-NLS-1$	
							}
							// Remove existing
							remove(existings);
						}

						/*
						 * Copy shift
						 */
						Set<Shift> set = shifts != null ? new HashSet<Shift>(
								shifts) : new HashSet<Shift>();
						for (ProductionEvent proEvent : events) {
							set.add(proEvent.getShift());
						}
						Map<Shift, Shift> shiftMap = getManagers()
								.getShiftManager().copy(set, destWeek,
										overwrite);

						/*
						 * Copy production events
						 */
						Map<ProductionEvent, ProductionEvent> map = new HashMap<ProductionEvent, ProductionEvent>();
						for (ProductionEvent prodEvent : events) {
							Shift shift = shiftMap.get(prodEvent.getShift());
							// Proceed with the copy
							ProductionEvent newProdEvent = new ProductionEvent();
							newProdEvent.setProduct(prodEvent.getProduct());
							newProdEvent.setShift(shift);
							map.put(prodEvent, newProdEvent);
						}
						add(map.values());
						return map;
					}
				});
	}

	/**
	 * Return the managers
	 * 
	 * @return
	 */
	@Override
	public PlanimodManagers getManagers() {
		return (PlanimodManagers) super.getManagers();
	}

	/**
	 * List all production events.
	 * 
	 * @return
	 * @throws ManagerException
	 */
	public List<ProductionEvent> list(final Date start, final Date end)
			throws ManagerException {
		return getManagers().query(new Query<List<ProductionEvent>>() {
			@Override
			public List<ProductionEvent> run() throws ManagerException {
				return ManagerContext.getDefaultSession()
						.createCriteria(ProductionEvent.class)
						.createCriteria(ProductionEvent.SHIFT)
						.add(startEndCriterion(start, end)).list();
			}
		});
	}

	/**
	 * This function is used to list any matching production event for the given
	 * planif calendar entry and the given shift event.
	 * 
	 * @param planif
	 *            the planif calendar entry
	 * @param shift
	 *            the shift
	 * @return the list of matching production event
	 * @throws ManagerException
	 */
	public List<ProductionEvent> listByShift(final Shift shift)
			throws ManagerException {
		return getManagers().query(new Query<List<ProductionEvent>>() {
			@Override
			public List<ProductionEvent> run() throws ManagerException {
				return ManagerContext.getDefaultSession()
						.createCriteria(ProductionEvent.class)
						.add(Restrictions.eq(ProductionEvent.SHIFT, shift))
						.list();
			}
		});
	}

	public List<ProductionEvent> listByShifts(
			final Collection<? extends Shift> shifts) throws ManagerException {
		return getManagers().query(new Query<List<ProductionEvent>>() {
			@Override
			public List<ProductionEvent> run() throws ManagerException {
				return ManagerContext.getDefaultSession()
						.createCriteria(ProductionEvent.class)
						.add(Restrictions.in(ProductionEvent.SHIFT, shifts))
						.list();
			}
		});
	}

	/**
	 * Return a table of ProductionEvent with the number of required position.Fs
	 * 
	 * @param shifts
	 * @throws ManagerException
	 */
	public Map<ProductionEvent, Long> listProductionEventTableByShifts(
			final Collection<Shift> shifts) throws ManagerException {

		return getManagers().query(new Query<Map<ProductionEvent, Long>>() {

			@Override
			public Map<ProductionEvent, Long> run() throws ManagerException {

				org.hibernate.Query query = ManagerContext.getDefaultSession()
						.getNamedQuery("listProductionEventTableByShifts"); //$NON-NLS-1$
				query.setParameterList("shifts", shifts); //$NON-NLS-1$
				List data = query.list();

				// Hash this data using a bidi-multi-map
				Map<ProductionEvent, Long> table = new HashMap<ProductionEvent, Long>();
				for (Object obj : data) {
					ProductionEvent event = (ProductionEvent) ((Object[]) obj)[0];
					Long sum = (Long) ((Object[]) obj)[1];
					table.put(event, sum);
				}

				return table;
			}

		});

	}

	@Override
	public Class<ProductionEvent> objectClass() {
		return ProductionEvent.class;
	}

	/**
	 * This implementation override the default behavior by also removing any
	 * related tasks.
	 * 
	 * @param s
	 *            list of production event to remove
	 * 
	 * @throws ManagerException
	 */
	@Override
	protected void preRemoveObjects(Collection<? extends ProductionEvent> s)
			throws ManagerException {
		// Load the Production events into this session, otherwise the Tasks
		// return will not reference the same object.
		for (ProductionEvent e : s) {
			ManagerContext.getDefaultSession().refresh(e);
		}

		// Get the list of task to remove.
		List<Task> tasks = getManagers().getTaskManager()
				.listByProductionEvents(s);
		if (tasks != null && tasks.size() > 0) {
			getManagers().getTaskManager().remove(tasks);
		}
	}

}