package com.planimod.ui.views.nonavailabilities;

import java.util.AbstractCollection;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import net.sf.paperclips.AlignPrint;
import net.sf.paperclips.DefaultGridLook;
import net.sf.paperclips.GridPrint;
import net.sf.paperclips.LineBorder;
import net.sf.paperclips.Print;
import net.sf.paperclips.ScalePrint;
import net.sf.paperclips.TextPrint;

import org.apache.commons.collections.Predicate;
import org.apache.commons.collections.iterators.FilterIterator;
import org.eclipse.swt.SWT;

import com.patrikdufresne.printing.ColorUtil;
import com.patrikdufresne.printing.FontDataUtil;
import com.patrikdufresne.util.DateFormatRegistry;
import com.patrikdufresne.util.Localized;
import com.planimod.core.ConcreteTimeRange;
import com.planimod.core.TimeRanges;
import com.planimod.core.Employee;
import com.planimod.core.NonAvailability;
import com.planimod.core.TimeRange;
import com.planimod.core.comparators.TimeRangeComparators;
import com.planimod.ui.printing.PlanimodPrintFactory;
import com.planimod.ui.printing.PlanimodPrintUtil;

/**
 * This class create the print to present the non-availabilities.
 * 
 * @author Patrik Dufresne
 * 
 */
public class NonAvailabilityPrintFactory extends PlanimodPrintFactory {

	/**
	 * Class used to organized the data.
	 * 
	 * @author Patrik Dufresne
	 * 
	 */
	private static class DataTable {

		private Collection<NonAvailability> nonAvailabilities;

		/**
		 * Create a new data table.
		 * 
		 * @param nonAvailabilities
		 */
		public DataTable(Collection<NonAvailability> nonAvailabilities) {
			this.nonAvailabilities = nonAvailabilities;
		}

		public Collection<NonAvailability> listNonAvailabilitiesByDay(
				final TimeRange day) {

			return new AbstractCollection<NonAvailability>() {

				@Override
				public Iterator<NonAvailability> iterator() {
					return new FilterIterator(nonAvailabilities.iterator(),
							new Predicate() {
								@Override
								public boolean evaluate(Object object) {
									NonAvailability na = (NonAvailability) object;
									return na.isVisible()
											&& TimeRanges.intersectEquals(
													day.getStartDate(),
													day.getEndDate(),
													na.getStartDate(),
													na.getEndDate());
								}
							});
				}

				@Override
				public int size() {
					int count = 0;
					Iterator<NonAvailability> iter = iterator();
					while (iter.hasNext()) {
						iter.next();
						count++;
					}
					return count;
				}

			};

		}

		/**
		 * Return a collection of non-availabilities containing the week.
		 * 
		 * @param range
		 *            the range to intersect with.
		 * @return collection of non-availabilities
		 */
		public Collection<NonAvailability> listNonAvailabilitiesByWeek(
				final TimeRange week) {

			return new AbstractCollection<NonAvailability>() {

				@Override
				public Iterator<NonAvailability> iterator() {
					return new FilterIterator(nonAvailabilities.iterator(),
							new Predicate() {
								@Override
								public boolean evaluate(Object object) {
									NonAvailability na = (NonAvailability) object;
									return na.isVisible()
											&& TimeRanges.containsEquals(
													na.getStartDate(),
													na.getEndDate(),
													week.getStartDate(),
													week.getEndDate());
								}
							});
				}

				@Override
				public int size() {
					int count = 0;
					Iterator<NonAvailability> iter = iterator();
					while (iter.hasNext()) {
						iter.next();
						count++;
					}
					return count;
				}

			};

		}

		/**
		 * Generate a collection of ordered time range.
		 * 
		 * @param month
		 * @return
		 */
		public static List<TimeRange> listDays(TimeRange month) {
			List<TimeRange> days = new ArrayList<TimeRange>();
			Calendar cal = Calendar.getInstance();
			cal.setTime(month.getStartDate());
			while (cal.getTime().compareTo(month.getEndDate()) < 0) {
				Date start = cal.getTime();
				cal.add(Calendar.DAY_OF_MONTH, 1);
				Date end = cal.getTime();
				days.add(new ConcreteTimeRange(start, end));
			}
			return days;
		}

		/**
		 * Return a collection of distincts employees.
		 * 
		 * @param nonAvailabilities
		 * 
		 * @return
		 */
		public static Collection<Employee> listEmployees(
				Collection<NonAvailability> nonAvailabilities) {
			Set<Employee> employees = new HashSet<Employee>();
			for (NonAvailability na : nonAvailabilities) {
				employees.add(na.getEmployee());
			}
			return employees;
		}

		/**
		 * Return an ordered collection of week time range for the given month
		 * time range. Notice, the month time range should start at
		 * firstdayofweek.
		 * 
		 * @param month
		 *            the month time range.
		 * 
		 * @return
		 */
		public static List<TimeRange> listWeeks(TimeRange month,
				int firstDayOfWeek) {
			List<TimeRange> weeks = new ArrayList<TimeRange>();
			Calendar cal = Calendar.getInstance();
			cal.setTime(month.getStartDate());

			// Check start.
			if (cal.get(Calendar.DAY_OF_WEEK) != firstDayOfWeek) {
				throw new IllegalArgumentException(
						"given time range doesn't start at first day of week"); //$NON-NLS-1$
			}

			// Create the ranges.
			while (cal.getTime().compareTo(month.getEndDate()) < 0) {
				Date start = cal.getTime();
				cal.add(Calendar.WEEK_OF_YEAR, 1);
				Date end = cal.getTime();
				weeks.add(new ConcreteTimeRange(start, end));
			}
			return weeks;
		}

		/**
		 * Return an ordered collection of month time range.
		 * 
		 * @param month
		 * 
		 * @return
		 */
		public static List<TimeRange> listMonths(TimeRange range,
				int firstDayOfWeek) {
			List<TimeRange> months = new ArrayList<TimeRange>();
			Calendar cal = Calendar.getInstance();
			cal.setTime(range.getStartDate());

			// Check start.
			if (cal.get(Calendar.DAY_OF_WEEK) != firstDayOfWeek) {
				throw new IllegalArgumentException(
						"given time range doesn't start at first day of week"); //$NON-NLS-1$
			}

			// Create the ranges.
			while (cal.getTime().compareTo(range.getEndDate()) < 0) {
				Date start = cal.getTime();
				cal.add(Calendar.WEEK_OF_MONTH, 1);
				Date end = TimeRanges.getLastDayOfLastWeekOfMonth(
						cal.getTime(), firstDayOfWeek);
				cal.setTime(end);
				months.add(new ConcreteTimeRange(start, end));
			}
			return months;
		}

	}

	private int firstDayOfWeek;

	/**
	 * Hold employee's non-availability.
	 */
	private Collection<NonAvailability> nonAvailabilities = null;

	private TimeRange range;

	/**
	 * Create a new Factory.
	 * 
	 * @param jobName
	 */
	public NonAvailabilityPrintFactory() {
		super(Localized.get(NonAvailabilityPrintFactory.class,
				"NonAvailabilityPrintFactory.jobName")); //$NON-NLS-1$
		setFirstDayOfWeek(Calendar.getInstance().getFirstDayOfWeek());
	}

	/**
	 * Create the print for a single day of a week.
	 * 
	 * @param dayNonAvailabilities
	 *            the list of non availabilities for the day.
	 * @return a new print.
	 */
	private Print createListPrint(TimeRange day, Collection<Employee> employees) {

		// Create the print
		GridPrint grid = new GridPrint();
		grid.addColumn("left:default:grow"); //$NON-NLS-1$

		// Add the day or month value
		if (day != null) {
			Calendar cal = Calendar.getInstance();
			cal.setTime(day.getStartDate());
			grid.add(new TextPrint(Integer.valueOf(
					cal.get(Calendar.DAY_OF_MONTH)).toString(),
					FontDataUtil.BOLD_SMALL));
		}

		// add the employees
		for (Employee emp : employees) {
			grid.add(new TextPrint(PlanimodPrintUtil.getName(emp),
					FontDataUtil.SMALL));
		}

		return grid;
	}

	/**
	 * Create the print for a single month.
	 * 
	 * @param month
	 *            the month time range.
	 * @param data
	 *            the data
	 * @return a new print.
	 */
	protected Print createMonthGrid(TimeRange month, DataTable data) {

		Calendar cal = Calendar.getInstance();

		// Set Grid look
		GridPrint grid = new GridPrint();
		grid.setCellClippingEnabled(false);
		DefaultGridLook look = new DefaultGridLook();
		look.setCellBorder(new LineBorder());
		look.setHeaderBackground(ColorUtil.LIGHT_GRAY);
		grid.setLook(look);

		// Create the columns
		for (int i = 0; i < 7; i++) {
			grid.addColumn("left:default:grow(1)"); //$NON-NLS-1$
		}
		grid.setColumnGroups(new int[][] { { 0, 2, 3, 4, 5, 6 } });
		grid.addColumn("left:default:grow"); //$NON-NLS-1$

		// Create the headers
		cal.setTime(month.getStartDate());
		for (int i = 0; i < 7; i++) {
			grid.addHeader(new AlignPrint(new TextPrint(DateFormatRegistry
					.format(cal.getTime(), DateFormatRegistry.DAY_OF_WEEK
							| DateFormatRegistry.SHORT, true),
					FontDataUtil.BOLD_SMALL), SWT.CENTER, SWT.TOP));
			cal.add(Calendar.DAY_OF_WEEK, 1);
		}
		grid.addHeader(new TextPrint("", FontDataUtil.BOLD_SMALL)); //$NON-NLS-1$

		/*
		 * Loop on weeks.
		 */
		for (TimeRange week : DataTable.listWeeks(month, this.firstDayOfWeek)) {

			Collection<Employee> weekNonAvailabilities = DataTable
					.listEmployees(data.listNonAvailabilitiesByWeek(week));

			/*
			 * Loop on days
			 */
			for (TimeRange day : DataTable.listDays(week)) {
				Collection<Employee> dayNonAvailabilities = DataTable
						.listEmployees(data.listNonAvailabilitiesByDay(day));
				dayNonAvailabilities.removeAll(weekNonAvailabilities);
				grid.add(createListPrint(day, dayNonAvailabilities));

			}

			grid.add(createListPrint(null, weekNonAvailabilities));

		}

		return new ScalePrint(grid);
	}

	/**
	 * This implementation check if the time range is set. If not, create a
	 * single empty print.
	 */
	@Override
	public Print createPrint() {
		if (this.range == null) {
			return new TextPrint(""); //$NON-NLS-1$
		}
		return super.createPrint();
	}

	/**
	 * This implementation create a new print body.
	 */
	@Override
	protected void createSections() {

		final DataTable data = new DataTable(this.nonAvailabilities);

		/*
		 * Loop on week time range
		 */
		List<TimeRange> months = new ArrayList<TimeRange>(DataTable.listMonths(
				this.range, this.firstDayOfWeek));
		Collections.sort(months, TimeRangeComparators.byDate());
		for (final TimeRange month : months) {
			String title = DateFormatRegistry.format(new Date(month.getEndDate()
					.getTime() - 1),
					DateFormatRegistry.MONTH | DateFormatRegistry.LONG, true);
			addSection(new PlanimodSection(title, getSubTitle()) {
				@Override
				public Print createBodyArea() {
					return createMonthGrid(month, data);
				}
			});

		}
	}

	/**
	 * Returns the employee's non-availability to be print by this factory.
	 * 
	 * @return qualification list or null
	 */
	public Collection<NonAvailability> getCalendarEvents() {
		return this.nonAvailabilities;
	}

	public int getFirstDayOfWeek() {
		return this.firstDayOfWeek;
	}

	/**
	 * Return the print time range.
	 * 
	 * @return
	 */
	public TimeRange geTimeRange() {
		return this.range;
	}

	/**
	 * Sets the list of events to print.
	 * 
	 * @param events
	 *            a list or null
	 */
	public void setCalendarEvents(Collection<NonAvailability> events) {
		this.nonAvailabilities = new ArrayList<NonAvailability>(events);
	}

	/**
	 * Sets the first day of the week. Must be once of the Calendar constants.
	 * 
	 * @param firstDayOfWeek
	 */
	public void setFirstDayOfWeek(int firstDayOfWeek) {
		if (firstDayOfWeek != Calendar.SUNDAY
				&& firstDayOfWeek != Calendar.MONDAY
				&& firstDayOfWeek != Calendar.TUESDAY
				&& firstDayOfWeek != Calendar.WEDNESDAY
				&& firstDayOfWeek != Calendar.THURSDAY
				&& firstDayOfWeek != Calendar.FRIDAY
				&& firstDayOfWeek != Calendar.SATURDAY) {
			throw new IllegalArgumentException("firstDayOfWeek"); //$NON-NLS-1$
		}
		this.firstDayOfWeek = firstDayOfWeek;
	}

	/**
	 * Sets the print time range. Used to define the data to be printed.
	 * 
	 * @param range
	 *            the new time range
	 */
	public void setTimeRange(TimeRange range) {
		this.range = range;
	}
}
