/**
 * Copyright (C) 2019 Patrik Dufresne Service Logiciel <info@patrikdufresne.com>
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 3 of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */
package com.planimod.ui.views.nonavailabilities;

import static com.planimod.ui.Localized._;

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 org.eclipse.nebula.paperclips.core.AlignPrint;
import org.eclipse.nebula.paperclips.core.ColumnPrint;
import org.eclipse.nebula.paperclips.core.grid.DefaultGridLook;
import org.eclipse.nebula.paperclips.core.grid.GridPrint;
import org.eclipse.nebula.paperclips.core.border.LineBorder;
import org.eclipse.nebula.paperclips.core.Print;
import org.eclipse.nebula.paperclips.core.ScalePrint;
import org.eclipse.nebula.paperclips.core.text.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.planimod.core.ConcreteTimeRange;
import com.planimod.core.Employee;
import com.planimod.core.NonAvailability;
import com.planimod.core.TimeRange;
import com.planimod.core.TimeRanges;
import com.planimod.core.comparators.EmployeeComparators;
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 {

    /**
     * Define the maximum number of employee in one column.
     */
    private static final int EMP_MAX = 8;

    /**
     * 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");
            }
            // 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");
            }
            // 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(_("Calendrier d'absence"));
        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();
        GridPrint grid1 = new GridPrint();

        // Set a minimal size for the cells with no employees
        if (employees.size() != 0) {
            grid1.addColumn("left:default:grow");
        } else {
            grid1.addColumn("left:72:grow");
        }

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

        // Use a simple grid for instances under the maximum number of employees on a single column
        if (employees.size() <= EMP_MAX) {
            grid = grid1;
        } else {
            // Use 2 columns for instances of equal or greater value of number of employees
            GridPrint grid2 = new GridPrint();
            grid2.addColumn("left:default:grow");
            grid1.add(new ColumnPrint(grid2, 2, 1));
            grid = grid2;
        }

        // Add the employees
        List<Employee> list = new ArrayList<Employee>(employees);
        Collections.sort(list, EmployeeComparators.bySeniority());
        int i = 1;
        for (Employee emp : list) {
            grid.add(new TextPrint(getEmployeeName(i, emp), FontDataUtil.SMALL));
            i++;
        }

        return grid1;
    }

    /**
     * Format the employee name as follow. &lt;count&gt; &lt;employee name&gt;
     * 
     * @param count
     *            the employee count
     * @param e
     *            the employee
     * @return
     */
    private static String getEmployeeName(int count, Employee e) {
        StringBuffer buf = new StringBuffer();
        buf.append(count);
        buf.append(PlanimodPrintUtil.SPACE);
        buf.append(PlanimodPrintUtil.getName(e));
        return buf.toString();
    }

    /**
     * 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");
        }
        grid.addColumn("left:default:grow");
        // 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));
        /*
         * 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("");
        }
        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 subtitle = DateFormatRegistry.format(new Date(month.getEndDate().getTime() - 1), DateFormatRegistry.MONTH | DateFormatRegistry.LONG, true);
            addSection(new PlanimodSection(getTitle(), subtitle) {

                @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");
        }
        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;
    }
}
