/**
 * 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.planif.printing;

import java.util.AbstractCollection;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.Predicate;
import org.apache.commons.collections.PredicateUtils;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.iterators.FilterIterator;

import com.patrikdufresne.util.BidiMultiHashMap;
import com.patrikdufresne.util.BidiMultiMap;
import com.planimod.core.ConcreteTimeRange;
import com.planimod.core.Employee;
import com.planimod.core.EmployeePreference;
import com.planimod.core.NonAvailability;
import com.planimod.core.Position;
import com.planimod.core.Product;
import com.planimod.core.Section;
import com.planimod.core.Shift;
import com.planimod.core.Task;
import com.planimod.core.Team;
import com.planimod.core.TimeRange;
import com.planimod.core.TimeRanges;

/**
 * This class is used to restructure the list of tasks to be printed for a specific time range (a week or a day).
 * 
 * @author Patrik Dufresne
 * 
 */
class DataTable {

    private static <T> Collection<T> filteredCollection(final Collection<T> t, final Predicate predicate) {
        return new AbstractCollection<T>() {

            @Override
            public Iterator<T> iterator() {
                return new FilterIterator(t.iterator(), predicate);
            }

            /**
             * This implementation loop on the item using the iterator.
             */
            @Override
            public int size() {
                int count = 0;
                Iterator<T> iter = this.iterator();
                while (iter.hasNext()) {
                    iter.next();
                    count++;
                }
                return count;
            }

        };
    }

    /**
     * Compute the list of TimeRange for the tasks specified. This is used to known the days to be printed. Return a set of
     * day time range.
     * 
     * @return days
     */
    public static Collection<TimeRange> computeDayTimeRange(Collection<Task> tasks) {
        Set<TimeRange> set = new HashSet<TimeRange>();
        for (Task t : tasks) {
            // Add a new day time range.
            set.add(new ConcreteTimeRange(TimeRanges.getDayStart(t.getStartDate()), TimeRanges.getDayEnd(t.getStartDate())));
        }
        return set;
    }

    /**
     * Create a table of preferences.
     * 
     * @return the table.
     */
    protected static Map<Employee, EmployeePreference> computeEmployeePreferenceTable(Collection<EmployeePreference> preferences) {
        Map<Employee, EmployeePreference> prefs = null;
        if (preferences != null) {
            prefs = new HashMap<Employee, EmployeePreference>();
            for (EmployeePreference pref : preferences) {
                prefs.put(pref.getEmployee(), pref);
            }
        }
        return prefs;
    }

    /**
     * Compute the list of TimeRange for the current list of tasks. This is used to know the week to be printed. Return a
     * set of week time range.
     * 
     * @return weeks
     */
    public static Set<TimeRange> computeWeekTimeRange(Collection<Task> tasks, int firstDayOfWeek) {
        Set<TimeRange> set = new HashSet<TimeRange>(tasks.size() * 2);
        for (Task t : tasks) {
            // Add a new week time range.
            set.add(new ConcreteTimeRange(TimeRanges.getWeekStart(t.getStartDate(), firstDayOfWeek), TimeRanges.getWeekEnd(t.getStartDate(), firstDayOfWeek)));
        }
        return set;
    }

    /**
     * Regroup the tasks by positions.
     * 
     * @param tasks
     * @return the map
     */
    public static BidiMultiMap<Position, Task> groupByPosition(Collection<Task> tasks) {
        BidiMultiMap<Position, Task> table = new BidiMultiHashMap<Position, Task>();
        for (Task task : tasks) {
            table.put(task.getPosition(), task);
        }
        return table;
    }

    protected BidiMultiMap<Team, Employee> classifiedTable;
    protected BidiMultiMap<Section, Employee> employeeTable;
    protected Collection<NonAvailability> nonAvailabilities;
    protected TimeRange range;

    protected BidiMultiMap<Team, Section> sectionTable;

    protected Collection<Task> tasks;

    protected BidiMultiMap<Team, Employee> teamTable;

    /**
     * Create the data object
     * 
     * @param tasks
     *            the raw data
     * @param shifts
     *            the raw data
     */
    public DataTable(TimeRange range, Collection<Task> tasks, Collection<EmployeePreference> preferences, Collection<NonAvailability> nonAvailabilities) {

        this.range = range;
        this.tasks = tasks;
        this.nonAvailabilities = nonAvailabilities;

        Map<Employee, EmployeePreference> prefs = computeEmployeePreferenceTable(preferences);

        // For the time range specified, build a data table to identify the
        // Teams, Sections and Employee
        this.teamTable = new BidiMultiHashMap<Team, Employee>();
        this.sectionTable = new BidiMultiHashMap<Team, Section>();
        this.employeeTable = new BidiMultiHashMap<Section, Employee>();
        this.classifiedTable = new BidiMultiHashMap<Team, Employee>();

        for (Task t : listTasks()) {
            Employee emp = t.getEmployee();
            Team team = t.getProductionEvent().getShift().getTeam();
            if (emp == null || !this.teamTable.containsValue(emp)) {
                this.teamTable.put(team, emp);
            } else if (!t.getPosition().getSwappable()) {
                // If the employee is already assign to a team, remove it
                // and place the employee into the non-swappable team.
                this.teamTable.removeValue(emp);
                this.teamTable.put(team, emp);
            }

            if (emp != null && t.getPosition().getClassified()) {
                this.classifiedTable.put(team, emp);
            }

            Section section = t.getPosition().getSection();
            this.sectionTable.put(team, section);

            // Check if the employee assignment is matching it's
            // preferred section.
            if (emp != null) {
                EmployeePreference pref;
                if (prefs != null && (pref = prefs.get(emp)) != null && pref.getPreferredSection() != null && pref.getPreferredSection().equals(section)) {
                    // Remove previous section assignment
                    this.employeeTable.removeValue(emp);
                }
                if (!this.employeeTable.containsValue(emp)) {
                    this.employeeTable.put(section, emp);
                }
            }
        }

    }

    /**
     * Return the team for an employee
     * 
     * @param e
     *            the employee
     * @return the team.
     */

    public Team getEmployeeTeam(Employee e) {
        Collection<Team> teams = this.teamTable.keySet(e);
        if (teams != null && teams.size() > 0) {
            return teams.iterator().next();
        }
        return null;
    }

    public TimeRange getRange() {
        return this.range;
    }

    /**
     * Return a vies on the employees that work at least once on a team. This list represent the employee working for the
     * given time range.
     * <p>
     * This collectionm does not include null employee.
     * 
     * @return the employees.
     */
    public Collection<Employee> listEmployee() {
        return filteredCollection(this.teamTable.valueSet(), PredicateUtils.notNullPredicate());
    }

    /**
     * Return the list of employees working on the given team.
     * <p>
     * This list does not include null.
     * 
     * @param team
     * @return
     */
    public Collection<Employee> listEmployeeByTeam(final Team team) {

        return filteredCollection(this.teamTable.valueSet(team), PredicateUtils.notNullPredicate());

    }

    /**
     * Return the list of employee asigned to the team and section specified.
     * 
     * @param team
     * @param section
     * @return
     */
    public Collection<Employee> listEmployeeByTeamAndSection(Team team, Section section) {
        Set<Employee> set = new HashSet<Employee>(listEmployeeByTeam(team));
        set.retainAll(this.employeeTable.valueSet(section));
        return set;
    }

    /**
     * Return a view of employees not available for the team specified.
     * 
     * @param team
     *            the team
     * @return the employees.
     */
    @SuppressWarnings("unchecked")
    public Collection<Employee> listEmployeeNotAvailableByTeam(final Team team) {

        return CollectionUtils.transformedCollection(listVisibleNonAvailabilityByTeam(team), new Transformer() {
            @Override
            public Object transform(Object input) {
                return ((NonAvailability) input).getEmployee();
            }
        });

    }

    /**
     * Return a collection of employee assign to classified position for a given team.
     * 
     * @param team
     *            the team
     * @return collection of employee.
     */
    public Collection<Employee> listEmployeeWithClassifiedPositionByTeam(Team team) {
        return Collections.unmodifiableSet(this.classifiedTable.valueSet(team));
    }

    /**
     * Return a view of employee not available.
     * 
     * @return
     */
    public Collection<Employee> listEmployeeWithNonAvailabilityWithoutTeam() {

        final Collection<NonAvailability> set = listNonAvailabilityWithoutTeam();

        return CollectionUtils.collect(set, new Transformer() {
            @Override
            public Object transform(Object input) {
                return ((NonAvailability) input).getEmployee();
            }
        });
    }

    /**
     * Return a view of the non-availabilities related to current time range.
     * 
     * @param team
     *            the team
     * @return list of non-availabilities.
     */
    public Collection<NonAvailability> listVisibleNonAvailabilities() {

        return filteredCollection(nonAvailabilities, new Predicate() {
            @Override
            public boolean evaluate(Object object) {
                NonAvailability na = (NonAvailability) object;
                // Loop on tasks to check if the NonAvailability intersect with one of the task.
                for (Task t : listTasks()) {
                    if (na.isVisible() && TimeRanges.intersectEquals(na, t.getStartDate(), t.getEndDate())) {
                        return true;
                    }
                }
                return false;
            }
        });

    }

    /**
     * Return a view of the non-availabilities related to the given team and current time range. A non-availability is
     * related to a team if the employee is working on the team, but is not available once during the team hours.
     * 
     * @param team
     *            the team
     * @return list of non-availabilities.
     */
    public Collection<NonAvailability> listVisibleNonAvailabilityByTeam(Team team) {

        final Set<Employee> map = this.teamTable.valueSet(team);
        if (map == null) {
            return Collections.EMPTY_LIST;
        }

        return filteredCollection(nonAvailabilities, new Predicate() {
            @Override
            public boolean evaluate(Object object) {
                NonAvailability na = (NonAvailability) object;
                // Loop on tasks to check if the NonAvailability intersect with one of the task.
                for (Task t : listTasks()) {
                    if (na.isVisible() && TimeRanges.intersectEquals(na, t.getStartDate(), t.getEndDate()) && map.contains(na.getEmployee())) {
                        return true;
                    }
                }
                return false;
            }
        });

    }

    /**
     * Return the non-availability of the employee that are not working on a team. Most likely, the employee is not
     * available for the curent time range.
     * 
     * @return the non-availabilities
     */
    public Collection<NonAvailability> listNonAvailabilityWithoutTeam() {
        Set<NonAvailability> set = new HashSet<NonAvailability>();
        nextNa: for (NonAvailability na : nonAvailabilities) {
            // Loop on tasks to check if the NonAvailability intersect with one of the task.
            for (Task t : listTasks()) {
                if (TimeRanges.intersectEquals(na, t.getStartDate(), t.getEndDate())) {

                    if (this.teamTable.containsValue(na.getEmployee())) {
                        continue nextNa;
                    }

                    set.add(na);
                }
            }
        }
        return set;
    }

    public Map<Product, Integer> listProductionByTeam(Team team) {

        HashMap<Product, Integer> table = new HashMap<Product, Integer>();

        for (Task t : listTaskByTeam(team)) {

            if (t.getStartDate().equals(t.getProductionEvent().getShift().getStartDate())) {

                Integer value = table.get(t.getProductionEvent().getProduct());
                if (value == null) {
                    table.put(t.getProductionEvent().getProduct(), Integer.valueOf(1));
                } else {
                    table.put(t.getProductionEvent().getProduct(), Integer.valueOf(value.intValue() + 1));
                }
            }

        }

        return table;

    }

    /**
     * Returns a list of section for a given team. A section will be list if there is at least one task in the given team
     * matching the section.
     * 
     * @param team
     *            the team
     * @return list of section
     */
    public Collection<Section> listSectionByTeam(final Team team) {

        return Collections.unmodifiableSet(this.sectionTable.valueSet(team));

    }

    /**
     * Return a view on the tasks matching the current time range and the team specified.
     * 
     * @return
     */
    public Collection<Task> listTaskByTeam(final Team team) {

        return filteredCollection(this.tasks, new Predicate() {
            @Override
            public boolean evaluate(Object object) {
                Task t = (Task) object;
                return t.getProductionEvent().getShift().getTeam().equals(team)
                        && (DataTable.this.range.getStartDate().before(t.getStartDate()) || DataTable.this.range.getStartDate().equals(t.getStartDate()))
                        && DataTable.this.range.getEndDate().after(t.getStartDate());
            }
        });

    }

    /**
     * Return a view on the tasks matching the current time range.
     * 
     * @return
     */
    public Collection<Task> listTasks() {

        return filteredCollection(this.tasks, new Predicate() {
            @Override
            public boolean evaluate(Object object) {
                Task t = (Task) object;
                return (DataTable.this.range.getStartDate().before(t.getStartDate()) || DataTable.this.range.getStartDate().equals(t.getStartDate()))
                        && DataTable.this.range.getEndDate().after(t.getStartDate());
            }
        });

    }

    public Collection<Shift> listShiftByTeam(Team team) {
        Set<Shift> shifts = new HashSet<Shift>();
        for (Task t : this.listTaskByTeam(team)) {
            shifts.add(t.getProductionEvent().getShift());
        }
        return shifts;
    }

    /**
     * Return the list of team.
     * 
     * @return
     */
    public Collection<Team> listTeam() {
        return Collections.unmodifiableSet(this.teamTable.keySet());
    }

    /**
     * Return a view on the vacant tasks. A vacant task is not assign to an employee.
     * 
     * @return the collection
     */
    public Collection<Task> listVacantTasksByTeam(final Team team) {

        return filteredCollection(listTaskByTeam(team), new Predicate() {
            @Override
            public boolean evaluate(Object object) {
                return ((Task) object).getEmployee() == null;
            }
        });
    }

    /**
     * Return a collection of visible non-availabilities without team
     * 
     * @param team
     * @return
     */
    public Collection<? extends NonAvailability> listVisibleNonAvailabilityWithoutTeam() {

        return filteredCollection(listNonAvailabilityWithoutTeam(), new Predicate() {
            @Override
            public boolean evaluate(Object object) {
                NonAvailability na = (NonAvailability) object;
                return na.isVisible();
            }
        });

    }

}