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

import java.util.AbstractCollection;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import org.apache.commons.collections.Predicate;
import org.apache.commons.collections.iterators.FilterIterator;

import com.patrikdufresne.managers.ManagerException;
import com.patrikdufresne.util.BidiMultiHashMap;
import com.patrikdufresne.util.BidiMultiMap;
import com.planimod.core.Employee;
import com.planimod.core.EmployeePreference;
import com.planimod.core.Position;
import com.planimod.core.Task;
import com.planimod.core.TaskNonAvailabilityTable;
import com.planimod.core.TaskQualificationTable;
import com.planimod.core.Team;
import com.planimod.core.comparators.EmployeePreferenceComparators;

/**
 * This class is used to compute the proposition to be made to employees for a given planif. Instance of this class are
 * created by the {@link GeneratePlanifContext} with the required data. One created this class cannot be change -- it's
 * an immutable class. So if any data related to the planif changed, a new instance of this class must be created to
 * take the modification in consideration.
 * <p>
 * Current implementation of this class hard code the process of determining the propositions.
 * 
 * @author Patrik Dufresne
 */
public class ProposedTasks {

    /**
     * Seniority comparator.
     */
    private static Comparator<EmployeePreference> seniorityComparator;

    /**
     * Compare the employees
     * 
     * @param emp1
     *            the first employee
     * @param emp2
     *            the second employee
     * @return Return a value smaller then 0 if <code>emp1</code> is more senior.
     */
    private static int compare(EmployeePreference emp1, EmployeePreference emp2) {
        if (seniorityComparator == null) {
            seniorityComparator = EmployeePreferenceComparators.bySeniority();
        }
        if (emp1 == null && emp2 == null) {
            return 0;
        } else if (emp1 == null && emp2 != null) {
            return 1;
        } else if (emp1 != null && emp2 == null) {
            return -1;
        }
        return seniorityComparator.compare(emp1, emp2);
    }

    /**
     * Compute two table related to task-employee.
     * 
     * @throws ManagerException
     */
    public static ProposedTasks create(
            Map<Employee, EmployeePreference> prefTable,
            TaskQualificationTable qualificationTable,
            TaskNonAvailabilityTable nonAvailabilityTable) {
        // Loop on each entry
        BidiMultiMap<Employee, Task> table = new BidiMultiHashMap<Employee, Task>();
        for (Entry<Employee, Task> entry : qualificationTable.entrySet()) {
            // Check if the tasks is classified.
            if (!nonAvailabilityTable.containsEntry(entry.getKey(), entry.getValue()) && entry.getValue().getPosition().getClassified()) {
                // For every employee available and qualify add it to the table.
                table.put(entry.getKey(), entry.getValue());
            }
        }
        // Compute the table
        Iterator<Entry<Employee, Task>> iter = table.entrySet().iterator();
        while (iter.hasNext()) {
            Entry<Employee, Task> entry = iter.next();
            // Remove locked
            if (entry.getValue().getLocked()
                    || taskAssignedWithPreferedPosition(prefTable, entry)
                    || employeeLockedOnDifferentTeam(qualificationTable.valueSet(), entry)
                    || employeeLockedOnSameTimeRange(qualificationTable.valueSet(), entry)
                    || employeeAssignToPreferredPositionOnDifferentTeam(prefTable, qualificationTable.valueSet(), entry)
                    || employeeAssignToPreferredPositionOnSameTimeRange(prefTable, qualificationTable.valueSet(), entry)
                    || employeeWithPreferredSeniority(prefTable, entry)) {
                iter.remove();
            }
        }
        // Compute warning
        Set<ProposedWarning> warnings = new HashSet<ProposedWarning>();
        iter = table.entrySet().iterator();
        while (iter.hasNext()) {
            Entry<Employee, Task> entry = iter.next();
            warnings.addAll(employeeHasSeniorityToWorkOnPreferredTeam(prefTable, qualificationTable, nonAvailabilityTable, table, entry));
        }
        return new ProposedTasks(table, warnings);
    }

    /**
     * Return True if the proposition doesn't matches the preferred team of an employee with preferred seniority.
     * 
     * @param prefTable
     * @param qualificationAndAvailabilityTable
     * @param entry
     *            the proposition
     * @return
     */
    private static boolean employeeWithPreferredSeniority(Map<Employee, EmployeePreference> prefTable, Entry<Employee, Task> entry) {
        if (!entry.getKey().getPreferencialSeniority()) {
            return false;
        }
        EmployeePreference pref = prefTable.get(entry.getKey());
        if (pref == null || pref.getPreferredTeam() == null || pref.getPreferredTeam().size() == 0) {
            return false;
        }
        Team team = pref.getPreferredTeam().get(0);
        return !team.equals(entry.getValue().getProductionEvent().getShift().getTeam());
    }

    /**
     * Return True if the current entry is assigned to an employee matchings it's preferred position.
     * 
     * @param prefTable
     * @param entry
     * @return
     */
    private static boolean taskAssignedWithPreferedPosition(Map<Employee, EmployeePreference> prefTable, Entry<Employee, Task> entry) {
        Employee emp = entry.getValue().getEmployee();
        if (emp == null) return false;
        EmployeePreference pref = prefTable.get(emp);
        if (pref == null) return false;
        Position pos = pref.getPreferredPosition();
        Team team = pref.getPreferredPositionTeam();
        if (pos == null || team == null) return false;
        return entry.getValue().getPosition().equals(pos) && entry.getValue().getProductionEvent().getShift().getTeam().equals(team);
    }

    /**
     * Creates a warning when an employee is planned to work on a classified position that is not in his preferences and
     * an other employee with less seniority is planned to work on one of this employee preferred teams.
     * 
     * @param prefTable
     * @param qualificationTable
     * @param nonAvailabilityTable
     * @param proposedTasks
     * @param context
     * @return
     */
    private static Collection<ProposedWarning> employeeHasSeniorityToWorkOnPreferredTeam(
            Map<Employee, EmployeePreference> prefTable,
            TaskQualificationTable qualificationTable,
            TaskNonAvailabilityTable nonAvailabilityTable,
            BidiMultiMap<Employee, Task> proposedTasks,
            Entry<Employee, Task> context) {
        // If the position being treated is not classified or if the employee is already assign to the task, then no
        // warning will be created.
        if (!context.getValue().getPosition().getClassified() || !context.getKey().equals(context.getValue().getEmployee())) {
            return Collections.emptyList();
        }
        // If the employee doesn't have preferences, then we can't assign preferred team.
        EmployeePreference pref = prefTable.get(context.getKey());
        if (pref == null || pref.getPreferredTeam() == null || pref.getPreferredTeam().size() == 0) {
            return Collections.emptyList();
        }
        // If the task being treated is not the preferred task of the employee, then no warning will be created.
        if (context.getValue().getPosition().equals(pref.getPreferredPosition())
                && context.getValue().getProductionEvent().getShift().getTeam().equals(pref.getPreferredPositionTeam())) {
            return Collections.emptyList();
        }
        int curTeamIndex = pref.getPreferredTeam().indexOf(context.getValue().getProductionEvent().getShift().getTeam());
        // The employee is already assigned to preferred team.
        if (curTeamIndex == 0) {
            return Collections.emptyList();
        }
        Collection<ProposedWarning> warnings = new ArrayList<ProposedWarning>();
        curTeamIndex = curTeamIndex < 0 ? pref.getPreferredTeam().size() : curTeamIndex;
        for (Task t : qualificationTable.values()) {
            EmployeePreference pref2 = prefTable.get(t.getEmployee());
            if (compare(pref, pref2) < 0
                    && !t.getLocked()
                    && pref.getPreferredTeam().indexOf(t.getProductionEvent().getShift().getTeam()) < curTeamIndex
                    && qualificationTable.containsEntry(context.getKey(), t)
                    && !nonAvailabilityTable.containsEntry(context.getKey(), t)) {
                warnings.add(new HasSeniorityToWorkOnPreferedTeamWarning(context.getKey(), t.getEmployee(), t.getProductionEvent().getShift().getTeam()));
            }
        }
        return warnings;
    }

    /**
     * Check if the employee is assigned to his preferred position on a different team.
     * 
     * @param prefTable
     *            the preference table
     * @param data
     *            the data
     * @param context
     *            the proposition.
     * @return
     */
    private static boolean employeeAssignToPreferredPositionOnDifferentTeam(
            Map<Employee, EmployeePreference> prefTable,
            Collection<Task> tasks,
            Entry<Employee, Task> context) {
        // Check if employee as a preferred position
        EmployeePreference pref = prefTable.get(context.getKey());
        Position pos = pref.getPreferredPosition();
        Team team = pref.getPreferredPositionTeam();
        if (pos == null || team == null) {
            return false;
        }
        // Check if the proposition is on a different team.
        if (team.equals(context.getValue().getProductionEvent().getShift().getTeam())) {
            return false;
        }
        // Check if the employee is assign to preferred position
        for (Task t : tasks) {
            if (t.getPosition().getClassified()
                    && context.getKey().equals(t.getEmployee())
                    && t.getProductionEvent().getShift().getTeam().equals(team)
                    && t.getPosition().equals(pos)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Check if the employee is assigned to his preferred position on the same time range.
     * 
     * @param prefTable
     *            the preference table
     * @param data
     *            the data
     * @param context
     *            the proposition
     * @return
     */
    private static boolean employeeAssignToPreferredPositionOnSameTimeRange(
            Map<Employee, EmployeePreference> prefTable,
            Collection<Task> tasks,
            Entry<Employee, Task> context) {
        // Check if employee as a preferred position
        EmployeePreference pref = prefTable.get(context.getKey());
        Position pos = pref.getPreferredPosition();
        Team team = pref.getPreferredPositionTeam();
        if (pos == null || team == null) {
            return false;
        }
        // Check if the proposition is on a different team.
        if (!team.equals(context.getValue().getProductionEvent().getShift().getTeam())) {
            return false;
        }
        for (Task t : tasks) {
            if (t.getEmployee() != null
                    && t.getEmployee().equals(context.getKey())
                    && t.getPosition().getClassified()
                    && t.getProductionEvent().getShift().getTeam().equals(team)
                    && t.getPosition().equals(pos)
                    && t.getProductionEvent().getShift().equals(context.getValue().getProductionEvent().getShift())
                    && t.getStartDate().equals(context.getValue().getStartDate())
                    && t.getEndDate().equals(context.getValue().getEndDate())) {
                return true;
            }
        }
        return false;
    }

    /**
     * Check if the employee is locked on a different team.
     * 
     * @param assignments
     * @param context
     * @return
     */
    private static boolean employeeLockedOnDifferentTeam(Collection<Task> tasks, Entry<Employee, Task> context) {
        if (context.getKey() == null) {
            return false;
        }
        for (Task t : tasks) {
            if (t.getLocked()
                    && t.getEmployee() != null
                    && t.getEmployee().equals(context.getKey())
                    && !t.getProductionEvent().getShift().getTeam().equals(context.getValue().getProductionEvent().getShift().getTeam())) {
                return true;
            }
        }
        return false;
    }

    private static boolean employeeLockedOnSameTimeRange(Collection<Task> tasks, Entry<Employee, Task> context) {
        for (Task t : tasks) {
            if (t.getLocked()
                    && t.getEmployee() != null
                    && t.getProductionEvent().getShift().equals(context.getValue().getProductionEvent().getShift())
                    && t.getEmployee().equals(context.getKey())
                    && t.getStartDate().equals(context.getValue().getStartDate())
                    && t.getEndDate().equals(context.getValue().getEndDate())) {
                return true;
            }
        }
        return false;
    }

    /**
     * The proposition table.
     */
    BidiMultiMap<Employee, Task> data;

    /**
     * Define the list of warning for the current proposition.
     */
    private Collection<ProposedWarning> warnings;

    /**
     * Create the proposed tasks object.
     * 
     * @param table
     *            the proposed table
     * @param warnings
     *            the list of waring.
     */
    protected ProposedTasks(BidiMultiMap<Employee, Task> table, Collection<ProposedWarning> warnings) {
        this.data = table;
        this.warnings = warnings;
    }

    /**
     * Return a collection of employees.
     * 
     * @return
     */
    public Collection<Employee> employees() {
        return Collections.unmodifiableCollection(this.data.keySet());
    }

    /**
     * Return a collection of employees with the propose task.
     * 
     * @param task
     * @return
     */
    public Collection<Employee> employees(Task task) {
        return Collections.unmodifiableCollection(this.data.keySet(task));
    }

    public Collection<Task> tasks(Employee employee) {
        return Collections.unmodifiableCollection(this.data.valueSet(employee));
    }

    /**
     * Return the collection of warning associated to the propositions.
     * 
     * @return
     */
    public Collection<ProposedWarning> warnings() {
        return Collections.unmodifiableCollection(this.warnings);
    }

    /**
     * Return a view on the warning filtered by the given employee.
     * 
     * @param employee
     * @return
     */
    public Collection<ProposedWarning> warnings(final Employee employee) {
        return new AbstractCollection<ProposedWarning>() {

            @Override
            public Iterator<ProposedWarning> iterator() {
                return new FilterIterator(warnings().iterator(), new Predicate() {

                    @Override
                    public boolean evaluate(Object object) {
                        return ((ProposedWarning) object).getEmployee().equals(employee);
                    }
                });
            }

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

    public Collection<Task> tasks() {
        return this.data.values();
    }

    public int size() {
        return this.data.size();
    }
}
