/**
 * 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.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.Predicate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.patrikdufresne.managers.Exec;
import com.patrikdufresne.managers.IManagerObserver;
import com.patrikdufresne.managers.ManagerEvent;
import com.patrikdufresne.managers.ManagerException;
import com.patrikdufresne.managers.Query;
import com.patrikdufresne.managers.SafeExec;
import com.patrikdufresne.util.BidiMultiMap;
import com.planimod.core.Employee;
import com.planimod.core.EmployeePreference;
import com.planimod.core.PlanimodManagers;
import com.planimod.core.Position;
import com.planimod.core.Product;
import com.planimod.core.ProductPosition;
import com.planimod.core.ProductionEvent;
import com.planimod.core.Qualification;
import com.planimod.core.Shift;
import com.planimod.core.Task;
import com.planimod.core.TaskNonAvailabilityTable;
import com.planimod.core.TaskQualificationTable;
import com.planimod.core.TimeRanges;

/**
 * Private class to hold all context variable required for planif generation.
 * 
 * 
 * <p>
 * This class create a linear problem representing the employees, teams, and tasks and solve it.
 * <ul>
 * <li>x[i] take the value 1 if and only if the employee i is working.</li>
 * <li>x[i,j] take the value 1 if an only if the employee i is working on team j.</li>
 * <li>x[i,k] take the value 1 if an onl if the employee i is working on task k.</li>
 * </ul>
 * 
 * @author Patrik Dufresne
 * 
 */
@SuppressWarnings("unqualified-field-access")
public class GeneratePlanifContext {

    /**
     * Dirty property.
     */
    public static final String DIRTY = "dirty";

    /**
     * End date property.
     */
    public static final String END = "end";

    /**
     * Property name.
     */
    public static final String LIST_EMPLOYEE = "listEmployee";

    /**
     * Property name
     */
    public static final String LIST_TASK_BY_EMPLOYEE = "listTaskByEmployee";

    /**
     * Property name.
     */
    public static final String PROPOSED_TASKS = "proposedTasks";

    /**
     * Start date property.
     */
    public static final String START = "start";

    private static final int TOUCH_AVAIL = 4;

    private static final int TOUCH_PREFS = 8;

    private static final int TOUCH_PROPOSED = 16;

    private static final int TOUCH_QUALIF = 2;

    private static final int TOUCH_TASKS = 1;

    private static final int TOUCH_TIMERANGE = 32;

    /**
     * Property name.
     */
    public static final String WEEK = "week";

    /**
     * Check if any object within the array matches the given planif and or time range.
     * 
     * @param planif
     *            the planif to matches against
     * @param start
     *            the start time
     * @param end
     *            the end time
     * @param objects
     *            the list of object to check
     * @return True if any object matches the planif and time range.
     */
    private static boolean matchesPlanifAndTimeRange(Date start, Date end, Collection<Object> objects) {

        for (Object obj : objects) {

            if (obj instanceof Task && TimeRanges.intersectEquals((Task) obj, start, end)) {
                return true;
            } else if (obj instanceof ProductionEvent && TimeRanges.intersectEquals(((ProductionEvent) obj).getShift(), start, end)) {
                return true;
            }

        }

        return false;
    }

    /**
     * This function is used as a SELECT statement to retrieve the list of planification event related to a given event
     * and position.
     * 
     * @param tasks
     *            the collection of planification event to filter.
     * @param event
     *            the planif event
     * @param position
     *            the position or null for wildcard
     * @return the list of planif vent matching the criteria
     */
    @SuppressWarnings("unchecked")
    private static List<Task> selectTasksMatchingProduction(Collection<Task> tasks, final ProductionEvent event, final Position position) {
        return new ArrayList<Task>(CollectionUtils.select(tasks, new Predicate() {
            @Override
            public boolean evaluate(Object o1) {
                if (!(o1 instanceof Task)) {
                    return false;
                }
                Task e = (Task) o1;
                return e.getProductionEvent() != null
                        && e.getProductionEvent().equals(event)
                        && e.getPosition() != null
                        && (position == null || e.getPosition().equals(position));
            }
        }));
    }

    protected PropertyChangeSupport changeSupport = new PropertyChangeSupport(this);

    /**
     * True if this context is dirty.
     */
    private boolean clean;

    /**
     * True if this context is disposed.
     */
    private boolean disposed;

    /**
     * Define the ending date of the planif.
     * <p>
     * This value is cached between search. It's used to avoid recomputing the start date of the week. When null, this
     * value can be recomputed using {@link #computeTimeRange()}.
     */
    Date end;

    private boolean inUpdate;

    final transient Logger logger = LoggerFactory.getLogger(getClass());

    /**
     * Keep reference on the managers.
     */
    protected PlanimodManagers managers;

    /**
     * The non-availability table.
     * <p>
     * This map is cached between search. It's used to compute the proposed task.
     */
    TaskNonAvailabilityTable nonAvailabilityTable;

    /**
     * Used to observe the managers.
     */
    private IManagerObserver observer = new IManagerObserver() {
        @Override
        public void handleManagerEvent(ManagerEvent event) {
            GeneratePlanifContext.this.handleManagerEvent(event);
        }
    };

    /**
     * Hold data related to proposed tasks.
     */
    ProposedTasks proposedTasks;
    /**
     * The qualification table.
     * <p>
     * This map is cached between search. It's used to provide fast access to different list.
     */
    TaskQualificationTable qualificationTable;

    /**
     * The starting date of this planif context.
     * <p>
     * This value is cached between search. It's used to avoid recomputing the start date of the week. When null, this
     * value can be recomputed using {@link #computeTimeRange()}.
     */
    Date start;

    private boolean tasksDirty;

    /**
     * A date within the week. Start and End variable should be used.
     */
    Date week;

    /**
     * Create a new context.
     * 
     * @throws ManagerException
     */
    public GeneratePlanifContext(PlanimodManagers managers) throws ManagerException {
        if (managers == null) {
            throw new NullPointerException();
        }

        this.managers = managers;
        this.week = null;

        attachObservers();

    }

    /**
     * Add property change listener
     * 
     * @param listener
     */
    public synchronized void addPropertyChangeListener(PropertyChangeListener listener) {
        this.changeSupport.addPropertyChangeListener(listener);
    }

    /**
     * Add property change listener
     * 
     * @param propertyName
     * @param listener
     */
    public synchronized void addPropertyChangeListener(String propertyName, PropertyChangeListener listener) {
        this.changeSupport.addPropertyChangeListener(propertyName, listener);
    }

    /**
     * Attaches observers to manager events.
     */
    private void attachObservers() {
        this.managers.addObserver(ManagerEvent.ALL, Qualification.class, this.observer);
        this.managers.addObserver(ManagerEvent.ALL, ProductPosition.class, this.observer);
        this.managers.addObserver(ManagerEvent.ALL, ProductionEvent.class, this.observer);
        this.managers.addObserver(ManagerEvent.ALL, Task.class, this.observer);
        this.managers.addObserver(ManagerEvent.ALL, EmployeePreference.class, this.observer);
    }

    /**
     * Check the context state. This function throw an exception is it's disposed.
     * 
     * @throws ManagerException
     */
    private void checkContext() throws ManagerException {
        if (this.disposed) {
            throw new ManagerException("Context is disposed");
        }
    }

    /**
     * Compute the non availability table.
     * <p>
     * Does nothing if the week date is not sets.
     * <p>
     * 
     * @throws ManagerException
     */
    protected void computeNonAvailabilityTable() throws ManagerException {
        if (this.start == null || this.end == null) {
            return;
        }
        this.nonAvailabilityTable = this.managers.getTaskManager().getNonAvailabilityTable(this.start, this.end);
    }

    /**
     * Compute the qualification table.
     * <p>
     * Does nothing if the week date is not sets.
     * <p>
     * 
     * @throws ManagerException
     */
    protected void computeQualificationTable() throws ManagerException {
        if (this.start == null || this.end == null) {
            return;
        }
        // Query the qualification table
        this.qualificationTable = this.managers.getTaskManager().getQualificationTable(this.start, this.end);
    }

    /**
     * Update the list of tasks associated to the given production events.
     * 
     * @param originalProductionEvents
     *            the original list of production events
     * @throws ManagerException
     * 
     */
    private void computeTasks() throws ManagerException {

        List<ProductionEvent> originalProductionEvents = this.managers.getProductionEventManager().list(this.start, this.end);
        /*
         * Query the manager to get a table with the production position.
         */
        BidiMultiMap<Product, ProductPosition> productPositionTable = this.managers.getTaskManager().listProductPositionTable(originalProductionEvents);

        List<Task> taskObsolets = this.managers.getTaskManager().list(start, end);

        List<Task> taskAddeds = new ArrayList<Task>();

        /*
         * From the production events list, we need to create (or find) planif events for each position.
         */
        for (ProductionEvent productionEvent : originalProductionEvents) {
            // The list of position required to produce the product
            Collection<ProductPosition> productPositions = productPositionTable.valueSet(productionEvent.getProduct());
            if (productPositions == null || productPositions.size() == 0) {
                // Leave the loop if no position are required
                continue;
            }

            // Get the list of existing related task for the given production
            // event.
            List<Task> relatedTasks = selectTasksMatchingProduction(taskObsolets, productionEvent, null);

            // if (splitedEvents.size() > 1 && relatedTasks.size() > 0) {
            // // In case the original production event is split, the
            // // associated task may required to be split too. Otherwise, the
            // // tasks may not be recycle properly and the locking information
            // // may be lost.
            // List<Task> splitedTasks = new ArrayList<Task>();
            // for (Task t : relatedTasks) {
            // // Check if the task start and end is matching the original
            // // shift's start and end.
            // if (!t.getStartDate().equals(
            // productionEvent.getShift().getStartDate())
            // || !t.getEndDate().equals(
            // productionEvent.getShift().getEndDate())) {
            // continue;
            // }
            // // Since it's matches, duplicate the task
            // for (int i = 0; i < splitedEvents.size(); i++) {
            // Task t2;
            // if (i == 0) {
            // t2 = t;
            // } else {
            // t2 = new Task();
            // t2.setEmployee(t.getEmployee());
            // t2.setLocked(t.getLocked());
            // t2.setPosition(t.getPosition());
            // t2.setProductionEvent(productionEvent);
            // splitedTasks.add(t2);
            // }
            // t.setStartDate(splitedEvents.get(i).getStartDate());
            // t.setEndDate(splitedEvents.get(i).getEndDate());
            // }
            // }
            // relatedTasks.addAll(splitedTasks);
            // }

            // for (Shift splited : splitedEvents) {

            for (ProductPosition productPosition : productPositions) {
                Position position = productPosition.getPosition();
                int positionCount = productPosition.getNumber().intValue();

                // Retrieve the matching planif events that already exists
                // in the database
                List<Task> matchingTasks = selectTasksMatchingProduction(relatedTasks, productionEvent, position);
                // Create or reuse planif events for each required position
                for (int i = 0; i < positionCount; i++) {
                    Task task;
                    if (matchingTasks.size() > 0) {
                        task = matchingTasks.remove(0);
                        if (task.getId() == null) {
                            taskAddeds.add(task);
                        }
                        // Since the planif event is reused, remove it from
                        // the existing planif events
                        relatedTasks.remove(task);
                        taskObsolets.remove(task);
                    } else {
                        // Create a new object
                        task = new Task();
                        taskAddeds.add(task);
                    }
                    task.setStartDate(productionEvent.getShift().getStartDate());
                    task.setEndDate(productionEvent.getShift().getEndDate());
                    task.setProductionEvent(productionEvent);
                    task.setPosition(position);
                }
            }
            // }
        }

        // Any remaining planif event in the list should be remove
        this.managers.getTaskManager().remove(taskObsolets);

        // Add planif event to database
        this.managers.getTaskManager().add(taskAddeds);

    }

    /**
     * Calculate the planif time range (start date and ending date) according to application settings.
     * <p>
     * This function does nothing if the week date it not set.
     * 
     * @param week
     *            the week date
     * @throws ManagerException
     */
    protected void computeTimeRange() throws ManagerException {
        if (this.week == null) {
            return;
        }

        int firstDayOfWeek = this.managers.getApplicationSettingManager().getFirstDayOfWeek();
        this.start = TimeRanges.getWeekStart(this.week, firstDayOfWeek);
        this.end = TimeRanges.getWeekEnd(this.week, firstDayOfWeek);

    }

    /**
     * Create the task without computing the planif.
     * 
     * @throws ManagerException
     */
    public synchronized void createTasks() throws ManagerException {
        if (!isDirty()) {
            return;
        }
        if (isTimeRangeDirty()) {
            computeTimeRange();
        }
        if (isTasksDirty()) {
            computeTasks();
            // Clear the qualification and availability table.
            this.qualificationTable = null;
            this.nonAvailabilityTable = null;
        }
    }

    /**
     * This function remove all the observer previously attach by {@link #attachObservers()}.
     */
    private void detachObservers() {
        this.managers.removeObserver(ManagerEvent.ALL, Qualification.class, this.observer);
        this.managers.removeObserver(ManagerEvent.ALL, ProductPosition.class, this.observer);
        this.managers.removeObserver(ManagerEvent.ALL, ProductionEvent.class, this.observer);
        this.managers.removeObserver(ManagerEvent.ALL, Task.class, this.observer);
        this.managers.removeObserver(ManagerEvent.ALL, EmployeePreference.class, this.observer);
    }

    /**
     * This function should be called to dispose this context.
     */
    public synchronized void dispose() {

        // Detach
        detachObservers();

        // Release all objects
        this.start = null;
        this.end = null;
        this.managers = null;

        // Mark as disposed
        this.disposed = true;
    }

    public Date getEnd() throws ManagerException {
        checkContext();
        if (isTimeRangeDirty()) {
            computeTimeRange();
        }
        return this.end;
    }

    /**
     * Return the managers used by this context.
     * 
     * @return
     */
    // This method doesn't need to be synchronized since this field is not
    // writable.
    public/* synchronized */PlanimodManagers getManagers() {
        return this.managers;
    }

    /**
     * For the current planif and week, return a list of task offer to the given employee.
     * 
     * @param employee
     *            the employee
     * @return the list of task offered
     * @throws ManagerException
     */
    public synchronized ProposedTasks getProposedTasks() throws ManagerException {
        checkContext();
        if (isTaskProposedDirty()) {
            managers.exec(new Exec() {
                @Override
                public void run() throws ManagerException {

                    // Build preference table
                    Map<Employee, EmployeePreference> prefTable = new HashMap<Employee, EmployeePreference>();
                    for (EmployeePreference pref : listEmployeePreferences()) {
                        prefTable.put(pref.getEmployee(), pref);
                    }
                    if (isQualificationTableDirty()) {
                        computeQualificationTable();
                    }
                    if (isNonAvailabilityDirty()) {
                        computeNonAvailabilityTable();
                    }

                    proposedTasks = ProposedTasks.create(prefTable, qualificationTable, nonAvailabilityTable);
                }
            });
        }
        return this.proposedTasks;
    }

    public Date getStart() throws ManagerException {
        checkContext();
        if (isTimeRangeDirty()) {
            computeTimeRange();
        }
        return this.start;
    }

    /**
     * Return the current week value.
     * 
     * @return the week value or null if not set.
     */
    public synchronized Date getWeek() {
        return this.week;
    }

    /**
     * Notify this class about manager events.
     * 
     * @param event
     *            the event
     */
    void handleManagerEvent(ManagerEvent event) {
        // If the planif or the week date is not set, don't handle the event.
        if (this.week == null) {
            return;
        }

        // Check if one of the function is already updating the database.
        if (this.inUpdate) {
            return;
        }

        // In case the time range is not computed, let to it now.
        if (isTimeRangeDirty()) {
            try {
                computeTimeRange();
            } catch (ManagerException e) {
                return;
            }
        }

        // Check the
        if (event.clazz.equals(ProductionEvent.class) && matchesPlanifAndTimeRange(this.start, this.end, event.objects)) {

            makeTasksDirty(TOUCH_TASKS, true);

        } else if (event.clazz.equals(ProductPosition.class)) {

            makeTasksDirty(TOUCH_TASKS, true);

        } else if (event.clazz.equals(Qualification.class)) {

            makeTasksDirty(TOUCH_QUALIF, true);

        } else if (event.clazz.equals(Task.class) && matchesPlanifAndTimeRange(this.start, this.end, event.objects)) {

            if (event.type == ManagerEvent.ADD || event.type == ManagerEvent.REMOVE) {
                makeTasksDirty(TOUCH_PROPOSED | TOUCH_TASKS, true);
            } else {
                makeTasksDirty(TOUCH_PROPOSED, true);
            }

        } else if (event.clazz.equals(EmployeePreference.class)) {

            makeTasksDirty(TOUCH_PREFS, true);

        }

    }

    /**
     * Compute the optimal solution.
     * 
     * @throws ManagerException
     *             if no solution as been found.
     */
    void internalSearchSolution(IGeneratePlanifProgressMonitor monitor) throws ManagerException {
        checkContext();

        /*
         * Re-Compute dirty data
         */
        if (!isDirty()) {
            // Nothing to compute.
            return;
        }
        if (isTimeRangeDirty()) {
            computeTimeRange();
        }
        if (isTasksDirty()) {
            computeTasks();
            this.qualificationTable = null;
            this.nonAvailabilityTable = null;
        }
        // Compute availability
        if (isQualificationTableDirty()) {
            computeQualificationTable();
        }
        if (isNonAvailabilityDirty()) {
            computeNonAvailabilityTable();
        }

        List<Shift> shifts = this.managers.getShiftManager().list(this.start, this.end);

        List<EmployeePreference> preferences = this.managers.getEmployeePreferenceManager().listByEmployees(this.qualificationTable.keySet());

        ProblemBuilder problem = new ProblemBuilder(monitor, shifts, this.qualificationTable, this.nonAvailabilityTable, preferences);

        problem.createAndSolveProblem();

        // Update the database, avoid to make stuff dirty.
        this.logger.info("Save the solution.");
        this.managers.getTaskManager().update(this.qualificationTable.valueSet());
        setDirty(false);

    }

    /**
     * Check if this context is dirty. This context become dirty whenever the state of the planif event doesn't match
     * the state of the database. To clear this context, the function
     * {@link #searchSolution(IGeneratePlanifProgressMonitor)} should be called to compute the optimal solution.
     * 
     * @return True if this context is dirty
     */
    public synchronized boolean isDirty() {
        return !this.clean;
    }

    /**
     * Check if the context is disposed.
     * 
     * @return True if the context is disposed
     */
    public synchronized boolean isDisposed() {
        return this.disposed;
    }

    /**
     * Check if the non availability table is dirty.
     * 
     * @return True if the table is dirty.
     */
    protected boolean isNonAvailabilityDirty() {
        return this.nonAvailabilityTable == null;
    }

    /**
     * Check if the qualification table is dirty.
     * 
     * @return True if the table is dirty
     */
    protected boolean isQualificationTableDirty() {
        return this.qualificationTable == null;
    }

    /**
     * Check if the offer table are dirties.
     * 
     * @return True if dirty
     */
    private boolean isTaskProposedDirty() {
        return this.proposedTasks == null;
    }

    private boolean isTasksDirty() {
        return this.tasksDirty;
    }

    /**
     * Check if the time range is dirty.
     * 
     * @return True if the time range is dirty
     */
    protected boolean isTimeRangeDirty() {
        return this.start == null || this.end == null;
    }

    /**
     * For the current planif and week, return a list of employee available and qualify at least for one task.
     * 
     * @return list of employee
     * @throws ManagerException
     */
    public synchronized List<Employee> listEmployee() throws ManagerException {
        checkContext();
        if (isTimeRangeDirty() || isQualificationTableDirty()) {
            managers.exec(new Exec() {

                @Override
                public void run() throws ManagerException {
                    if (isTimeRangeDirty()) {
                        computeTimeRange();
                    }
                    computeQualificationTable();
                }
            });
        }
        if (this.qualificationTable == null) {
            return Collections.EMPTY_LIST;
        }
        return new ArrayList<Employee>(this.qualificationTable.keySet());

    }

    /**
     * For the current planif and week, return a list of employee's preference.
     * 
     * @return
     * @throws ManagerException
     */
    public List<EmployeePreference> listEmployeePreferences() throws ManagerException {
        checkContext();
        return managers.query(new Query<List<EmployeePreference>>() {
            @Override
            public List<EmployeePreference> run() throws ManagerException {
                if (isQualificationTableDirty()) {
                    computeQualificationTable();
                }
                return managers.getEmployeePreferenceManager().listByEmployees(qualificationTable.keySet());
            }
        });
    }

    /**
     * For the current planif and week date, return a list of tasks currently in database.
     * 
     * @return the tasks
     * @throws ManagerException
     */
    public synchronized List<Task> listTask() throws ManagerException {
        checkContext();
        // If the context is dirty, query the database directly.
        if (isTimeRangeDirty() || isQualificationTableDirty()) {
            return managers.query(new Query<List<Task>>() {
                @Override
                public List<Task> run() throws ManagerException {
                    if (isTimeRangeDirty()) {
                        computeTimeRange();
                    }
                    if (start == null || end == null) {
                        return Collections.EMPTY_LIST;
                    }
                    return managers.getTaskManager().list(start, end);
                }
            });
        }
        return new ArrayList<Task>(qualificationTable.valueSet());
    }

    /**
     * For the current planif and week date, return a list of tasks assigned to the given employee.
     * 
     * @param employee
     *            the employee
     * @return tasks assigned to the given employee
     * @throws ManagerException
     */
    public synchronized List<Task> listTaskByEmployee(final Employee employee) throws ManagerException {

        // If the context is dirty, query the database directly.
        if (isTimeRangeDirty() || isQualificationTableDirty()) {
            return managers.query(new Query<List<Task>>() {
                @Override
                public List<Task> run() throws ManagerException {
                    if (isTimeRangeDirty()) {
                        computeTimeRange();
                    }
                    if (start == null || end == null) {
                        return Collections.EMPTY_LIST;
                    }
                    return managers.getTaskManager().listByEmployee(start, end, employee);
                }
            });
        }

        // If the context is not dirty, compute the list ourself.
        List<Task> list = new ArrayList<Task>();
        for (Task t : qualificationTable.valueSet()) {
            if (employee.equals(t.getEmployee())) {
                list.add(t);
            }
        }
        return list;

    }

    /**
     * Mark the list of tasks to be dirty.
     */
    synchronized void makeTasksDirty(int touch, boolean fireChange) {
        this.logger.trace("makeTasksDirty");

        if (touch == 0) {
            throw new IllegalArgumentException();
        }

        // Determine the fireEvents
        boolean fireDirty = (this.clean == true);
        boolean fireTask = false;
        boolean fireListEmployee = false;
        boolean fireProposeds = false;
        boolean fireWeek = false;

        // Mark list as dirty
        this.clean = false;

        if ((touch & (TOUCH_TASKS | TOUCH_QUALIF | TOUCH_AVAIL | TOUCH_PREFS | TOUCH_PROPOSED | TOUCH_TIMERANGE)) != 0) {
            fireProposeds = (this.proposedTasks != null);
            this.proposedTasks = null;
        }
        if ((touch & (TOUCH_TASKS | TOUCH_QUALIF | TOUCH_AVAIL | TOUCH_TIMERANGE)) != 0) {
            fireListEmployee = true;
            this.qualificationTable = null;
        }
        if ((touch & (TOUCH_TASKS | TOUCH_AVAIL | TOUCH_TIMERANGE)) != 0) {
            this.nonAvailabilityTable = null;
        }
        if ((touch & (TOUCH_TASKS | TOUCH_TIMERANGE)) != 0) {
            fireTask = true;
            this.tasksDirty = true;
        }
        if ((touch & (TOUCH_TIMERANGE)) != 0) {
            fireWeek = true;
            this.end = null;
            this.start = null;
        }

        // Fire events.
        if (fireDirty) {
            this.changeSupport.firePropertyChange(DIRTY, null, null);
        }
        if (fireWeek) {
            this.changeSupport.firePropertyChange(WEEK, null, null);
            this.changeSupport.firePropertyChange(START, null, null);
            this.changeSupport.firePropertyChange(END, null, null);
        }
        if (fireListEmployee) {
            this.changeSupport.firePropertyChange(LIST_EMPLOYEE, null, null);
        }
        if (fireTask || fireProposeds) {
            this.changeSupport.firePropertyChange(LIST_TASK_BY_EMPLOYEE, null, null);
        }
        if (fireProposeds) {
            this.changeSupport.firePropertyChange(PROPOSED_TASKS, null, null);
        }

    }

    /**
     * Add property change listener.
     * 
     * @param listener
     */
    public synchronized void removePropertyChangeListener(PropertyChangeListener listener) {
        this.changeSupport.removePropertyChangeListener(listener);
    }

    /**
     * Remove property change listener
     * 
     * @param propertyName
     * @param listener
     */
    public synchronized void removePropertyChangeListener(String propertyName, PropertyChangeListener listener) {
        this.changeSupport.removePropertyChangeListener(propertyName, listener);
    }

    /**
     * Compute the optimal solution.
     * 
     * @param monitor
     * @return
     * @throws ManagerException
     */
    public synchronized void searchSolution(final IGeneratePlanifProgressMonitor monitor) throws ManagerException {

        try {

            this.inUpdate = true;

            // Execute the search within one transaction.
            this.managers.exec(new SafeExec() {

                @Override
                public void handleException(Throwable exception) {
                    // Clear state of the context
                    qualificationTable = null;
                    nonAvailabilityTable = null;
                }

                @Override
                public void run() throws ManagerException {
                    internalSearchSolution(monitor);
                }

            });
            // Need to clear the proposed tasks.
            this.proposedTasks = null;

            // Fire events
            this.changeSupport.firePropertyChange(DIRTY, null, null);
            this.changeSupport.firePropertyChange(LIST_EMPLOYEE, null, null);
            this.changeSupport.firePropertyChange(LIST_TASK_BY_EMPLOYEE, null, null);
            this.changeSupport.firePropertyChange(PROPOSED_TASKS, null, null);

        } finally {
            this.inUpdate = false;
        }

    }

    /**
     * Called internally to sets the dirty state and fire property change.
     * 
     * @param dirty
     */
    private synchronized void setDirty(boolean dirty) {
        this.changeSupport.firePropertyChange(DIRTY, !this.clean, this.clean = (!dirty));
    }

    /**
     * Sets the week date value or null to unset.
     * 
     * @param week
     */
    public synchronized void setWeek(Date week) {
        if ((this.week == null && week == null) || (this.week != null && this.week.equals(week))) {
            return;
        }
        this.week = week;
        makeTasksDirty(TOUCH_TIMERANGE, true);
    }

}
