/*
 * 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.AbstractCollection;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
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.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 {

    /**
     * Immutable class representing a warning to be displayed to the user to
     * help him with the assignment. Some of the warning represent business rule
     * to be follows.
     * 
     * @author Patrik Dufresne
     */
    public static class ProposedWarning {

        /**
         * This code is used when the employee is assigned to a proposed
         * position by default, but the employee may refused to work on this
         * position and so return to his preferred team.
         */
        public static final int AS_SENIORITY_TO_WORK_ON_PREFERRED_TEAM = 0;

        /**
         * Define the warning code.
         */
        private int code;

        /**
         * Define the proposition with the warning.
         */
        private Entry<Employee, Task> entry;

        /**
         * Create a new proposition warning.
         * 
         * @param entry the proposition in warning.
         * @param code the warning code. Should be on of the constant of the
         * class {@link ProposedWarning}.
         */
        public ProposedWarning(Entry<Employee, Task> entry, int code) {
            if (entry == null) {
                throw new NullPointerException();
            }
            this.entry = entry;
            this.code = code;
        }

        /**
         * The warning code.
         * 
         * @return
         */
        public int getCode() {
            return this.code;
        }

        /**
         * Return the employee associated with this warning.
         * 
         * @return
         */
        public Employee getEmployee() {
            return this.entry.getKey();
        }

        /**
         * Return the task associated to this warning.
         * 
         * @return
         */
        public Task getTask() {
            return this.entry.getValue();
        }
    }

    /**
     * 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
        List<ProposedWarning> warnings = new ArrayList<ProposedWarning>();
        iter = table.entrySet().iterator();
        while (iter.hasNext()) {
            Entry<Employee, Task> entry = iter.next();
            if (employeeAssignedToClassifiedByDefault(prefTable, table, entry)
                    && employeeHasSeniorityToWorkOnPreferredTeam(prefTable,
                            qualificationTable, nonAvailabilityTable, table, entry)) {
                warnings.add(new ProposedWarning(entry,
                        ProposedWarning.AS_SENIORITY_TO_WORK_ON_PREFERRED_TEAM));
            }
        }
        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);
    }

    private static boolean employeeHasSeniorityToWorkOnPreferredTeam(
            Map<Employee, EmployeePreference> prefTable,
            TaskQualificationTable qualificationTable,
            TaskNonAvailabilityTable nonAvailabilityTable,
            BidiMultiMap<Employee, Task> proposedTasks, Entry<Employee, Task> context) {
        // 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 false;
        }
        int curTeamIndex =
                pref.getPreferredTeam().indexOf(
                        context.getValue().getProductionEvent().getShift().getTeam());
        // The employee is already assigned to preferred team.
        if (curTeamIndex == 0)
            return false;
        curTeamIndex = curTeamIndex < 0
                ? pref.getPreferredTeam().size()
                : curTeamIndex;
        for (Task t : qualificationTable.values()) {
            EmployeePreference pref2 = prefTable.get(t.getEmployee());
            if (compare(pref, pref2) < 0
                    && pref.getPreferredTeam().indexOf(
                            t.getProductionEvent().getShift().getTeam()) < curTeamIndex
                    && qualificationTable.containsEntry(context.getKey(), t)
                    && !nonAvailabilityTable.containsEntry(context.getKey(), t)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Check if the employee is assigned by default to the proposed classified
     * position.
     * 
     * @param prefTable
     * @param table
     * @param entry the proposition
     * @return
     */
    private static boolean employeeAssignedToClassifiedByDefault(
            Map<Employee, EmployeePreference> prefTable,
            BidiMultiMap<Employee, Task> table, Entry<Employee, Task> entry) {
        if (!entry.getValue().getPosition().getClassified()) {
            return false;
        }
        if (!entry.getKey().equals(entry.getValue().getEmployee())) {
            return false;
        }
        EmployeePreference pref = prefTable.get(entry.getValue().getEmployee());
        if (pref == null) {
            return true;
        }
        Position pos = pref.getPreferredPosition();
        Team team = pref.getPreferredPositionTeam();
        if (pos == null || team == null) {
            return true;
        }
        return !entry.getValue().getPosition().equals(pos)
                || !entry.getValue().getProductionEvent().getShift().getTeam()
                        .equals(team);
    }

    /**
     * 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 List<ProposedWarning> warnings;

    /**
     * Create the proposed tasks object.
     * 
     * @param table the proposed table
     * @param warnings the list of waring.
     */
    protected ProposedTasks(BidiMultiMap<Employee, Task> table,
            List<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();
    }
}
