package ttsolver;

import ifs.model.*;
import ifs.util.*;

import java.util.*;

import ttsolver.constraint.*;
import ttsolver.model.*;

/**
 * Timetable model.
 * 
 * @author <a href="mailto:muller@ktiml.mff.cuni.cz">Tomas Muller</a>
 * @version 1.0
 */

public class TimetableModel extends Model {
    private static org.apache.log4j.Logger sLogger = org.apache.log4j.Logger.getLogger(TimetableModel.class);
    private static java.text.DecimalFormat sDoubleFormat = new java.text.DecimalFormat("0.00",new java.text.DecimalFormatSymbols(Locale.US));
    private static java.text.SimpleDateFormat sDateFormat = new java.text.SimpleDateFormat("dd-MMM-yy_HHmmss",java.util.Locale.US);
    private long iGlobalRoomPreference = 0;
    private double iGlobalTimePreference = 0;
    private long iBestRoomPreference = 0;
    private long iBestInstructorDistancePreference = 0;
    private double iBestTimePreference = 0;
    private int iBestDepartmentSpreadPenalty = 0;
    private Counter iGlobalGroupConstraintPreference = new Counter();
    private Counter iViolatedStudentConflicts = new Counter();
    private FastVector iInstructorConstraints = new FastVector();
    private FastVector iJenrlConstraints = new FastVector();
    private FastVector iRoomConstraints = new FastVector();
    private FastVector iDepartmentSpreadConstraints = new FastVector();
    private FastVector iGroupConstraints = new FastVector();
    private FastVector iIgnoredClasses  = new FastVector();
    private boolean iSwitchStudents = false;
    private DataProperties iProperties = null;
    
    /** For MPP generator */
    public Vector ignoredClasses() { return iIgnoredClasses; }
    
    public TimetableModel(DataProperties properties) {
        super();
        iSwitchStudents            = properties.getPropertyBoolean("General.SwitchStudents",true);
        iProperties = properties;
        InstructorConstraint.sNoPreferenceLimit = iProperties.getPropertyDouble("General.Distance.NoPreferenceLimit",0.0);
        InstructorConstraint.sDiscouragedLimit = iProperties.getPropertyDouble("General.Distance.DiscouragedLimit",5.0);
        InstructorConstraint.sProhibitedLimit = iProperties.getPropertyDouble("General.Distance.ProhibitedLimit",20.0);
    }
    
    public DataProperties getProperties() { return iProperties; }

    /** Check validity of JENRL constraints from student enrollments */
    public void checkJenrl() {
        try {
            System.err.println("Checking jenrl ...");
            for (Enumeration i1=variables().elements();i1.hasMoreElements();) {
                Lecture l1 = (Lecture)i1.nextElement();
                System.err.println("Checking "+l1.getName()+" ...");
                for (Enumeration i2=variables().elements();i2.hasMoreElements();) {
                    Lecture l2 = (Lecture)i2.nextElement();
                    if (l1.getId()<l2.getId()) {
                        int jenrl = 0;
                        Vector jenrlStudents = new FastVector();
                        for (Enumeration i3=l1.students().elements(); i3.hasMoreElements(); ) {
                            String student = (String)i3.nextElement();
                            if (l2.students().contains(student)) {
                                jenrl++;
                                jenrlStudents.addElement(student);
                            }
                        }
                        boolean found = false;
                        for (Enumeration i3=iJenrlConstraints.elements();i3.hasMoreElements(); ) {
                            JenrlConstraint j = (JenrlConstraint)i3.nextElement();
                            Lecture a=(Lecture)j.first();
                            Lecture b=(Lecture)j.second();
                            if ((a.equals(l1) && b.equals(l2)) || (a.equals(l2) && b.equals(l1))) {
                                found = true;
                                if (j.getJenrl()!=jenrl) {
                                    sLogger.error("ERROR: Wrong jenrl between "+l1.getName()+" and "+l2.getName()+" (constraint="+j.getJenrl()+" != computed="+jenrl+").");
                                    sLogger.error("       "+l1.getName()+" has students: "+l1.students());
                                    sLogger.error("       "+l2.getName()+" has students: "+l2.students());
                                    sLogger.error("       intersection: "+jenrlStudents);
                                }
                            }
                        }
                        if (!found && jenrl>0) {
                            System.err.println("ERROR: Missing jenrl between "+l1.getName()+" and "+l2.getName()+" (computed="+jenrl+").");
                            sLogger.error("ERROR: Missing jenrl between "+l1.getName()+" and "+l2.getName()+" (computed="+jenrl+").");
                            sLogger.error("       "+l1.getName()+" has students: "+l1.students());
                            sLogger.error("       "+l2.getName()+" has students: "+l2.students());
                            sLogger.error("       intersection: "+jenrlStudents);
                        }
                    }
                }
            }
            System.err.println("Check done.");
        } catch (Exception e) {
			edu.purdue.smas.timetable.util.Debug.error(e);
            e.printStackTrace();
        }
    }
    
    /** Overall room preference */
    public long getGlobalRoomPreference() { return iGlobalRoomPreference; }
    /** Overall time preference */
    public double getGlobalTimePreference() { return iGlobalTimePreference; }
    /** Number of student conflicts */
    public long getViolatedStudentConflicts() { return iViolatedStudentConflicts.get(); }
    /** Number of student conflicts */
    public long countViolatedStudentConflicts() {
        long studentConflicts = 0;
        for (Enumeration it1=iJenrlConstraints.elements();it1.hasMoreElements();) {
            JenrlConstraint jenrl = (JenrlConstraint)it1.nextElement();
            Lecture a = (Lecture)jenrl.first();
            Lecture b = (Lecture)jenrl.second();
            if (a.getAssignment()!=null && b.getAssignment()!=null && JenrlConstraint.isInConflict((Placement)a.getAssignment(),(Placement)b.getAssignment()))
                studentConflicts+=jenrl.getJenrl();
        }
        return studentConflicts;
    }
    /** Number of student conflicts */
    public Counter getViolatedStudentConflictsCounter() { return iViolatedStudentConflicts; }
    /** Overall group constraint preference */
    public long getGlobalGroupConstraintPreference() { return iGlobalGroupConstraintPreference.get(); }
    /** Overall group constraint preference */
    public Counter getGlobalGroupConstraintPreferenceCounter() { return iGlobalGroupConstraintPreference; }
    /** Overall room preference of the best solution ever found */
    public long getBestRoomPreference() { return iBestRoomPreference; }
    /** Overall time preference of the best solution ever found */
    public double getBestTimePreference() { return iBestTimePreference; }
    /** Sets overall room preference of the best solution ever found */
    public void setBestRoomPreference(long bestRoomPreference) { iBestRoomPreference = bestRoomPreference; }
    /** Sets overall time preference of the best solution ever found */
    public void setBestTimePreference(double bestTimePreference) { iBestTimePreference = bestTimePreference; }
    /** Overall instructor distance (back-to-back) preference */
    public long getInstructorDistancePreference() {
        long pref = 0;
        for (Enumeration it1=iInstructorConstraints.elements();it1.hasMoreElements();) {
            InstructorConstraint constraint = (InstructorConstraint)it1.nextElement();
            pref+=constraint.getPreference();
        }
        return pref;
    }
    /** The worst instructor distance (back-to-back) preference */
    public long getInstructorWorstDistancePreference() {
        long pref = 0;
        for (Enumeration it1=iInstructorConstraints.elements();it1.hasMoreElements();) {
            InstructorConstraint constraint = (InstructorConstraint)it1.nextElement();
            pref+=constraint.getWorstPreference();
        }
        return pref;
    }
    /** Overall number of useless time slots */
    public long getUselessSlots() {
        long uselessSlots = 0;
        for (Enumeration it1=iRoomConstraints.elements();it1.hasMoreElements();) {
            RoomConstraint constraint = (RoomConstraint)it1.nextElement();
            uselessSlots+=((RoomConstraint)constraint).countUselessSlots();
        }
        return uselessSlots;
    }
    /** Overall number of student conflicts caused by distancies (back-to-back classes are too far)*/
    public long getStudentDistanceConflicts() {
        long nrConflicts = 0;
        for (Enumeration it1=iJenrlConstraints.elements();it1.hasMoreElements();) {
            JenrlConstraint jenrl = (JenrlConstraint)it1.nextElement();
            if (jenrl.isInConflict() && 
                    !((Placement)jenrl.first().getAssignment()).getTimeLocation().hasIntersection(((Placement)jenrl.second().getAssignment()).getTimeLocation()))
                    nrConflicts += jenrl.getJenrl();
        }
        return nrConflicts;
    }
    /** Overall hard student conflicts (student conflict between single section classes) */ 
    public long getHardStudentConflicts() {
        if (!iSwitchStudents) return 0;
        long hardStudentConflicts = 0;
        for (Enumeration it1=iJenrlConstraints.elements();it1.hasMoreElements();) {
            JenrlConstraint jenrl = (JenrlConstraint)it1.nextElement();
            if (jenrl.isInConflict()) {
                Lecture l1 = (Lecture)jenrl.first();
                Lecture l2 = (Lecture)jenrl.second();
                if (l1.sameLectures()!=null && l1.sameLectures().size()==1 &&
                l2.sameLectures()!=null && l2.sameLectures().size()==1)
                    hardStudentConflicts+=jenrl.getJenrl();
            }
        }
        return hardStudentConflicts;
    }
    
    /** When a value is assigned to a variable -- update gloval preferences */
    public void afterAssigned(long iteration, Value value) {
        super.afterAssigned(iteration, value);
        if (value==null) return;
        Placement placement = (Placement)value;
        iGlobalRoomPreference += placement.getRoomLocation().getPreference();
        iGlobalTimePreference += placement.getTimeLocation().getNormalizedPreference();
    }
    /** When a value is unassigned from a variable -- update gloval preferences */
    public void afterUnassigned(long iteration, Value value) {
        super.afterUnassigned(iteration, value);
        if (value==null) return;
        Placement placement = (Placement)value;
        iGlobalRoomPreference -= placement.getRoomLocation().getPreference();
        iGlobalTimePreference -= placement.getTimeLocation().getNormalizedPreference();
    }

    /** Student final sectioning (switching students between sections of the same class in order to minimize overall number of student conflicts) */
    public void switchStudents() {
        if (iSwitchStudents) {
            long before;
            Progress.getInstance().save();
            Collection variables = variables();
            do {
                //sLogger.debug("Shifting students ...");
                Progress.getInstance().setPhase("Shifting students ...",variables().size());
                before = getViolatedStudentConflictsCounter().get();
                HashSet lecturesToRecompute = new HashSet(variables.size());
                for (Iterator i=variables.iterator();i.hasNext();) {
                    Lecture lecture = (Lecture)i.next();
                    //sLogger.debug("Shifting students for "+lecture);
                    Set lects = lecture.swapStudents();
                    //sLogger.debug("Lectures to recompute: "+lects);
                    if (lects!=null) lecturesToRecompute.addAll(lects);
                    Progress.getInstance().incProgress();
                }
                //sLogger.debug("Shifting done, "+getViolatedStudentConflictsCounter().get()+" conflicts.");
                variables = lecturesToRecompute;
            } while (!variables.isEmpty() && before>getViolatedStudentConflictsCounter().get());
            Progress.getInstance().restore();
        }
    }

    public String toString() {
        return "TimetableModel{"+
        "\n  super="+super.toString()+
        "\n  studentConflicts="+iViolatedStudentConflicts.get()+
//        "\n  studentConflicts(violated room distance)="+iViolatedRoomDistanceStudentConflicts.get()+
//        "\n  studentPreferences="+iRoomDistanceStudentPreference.get()+
        "\n  roomPreferences="+iGlobalRoomPreference+"/"+iBestRoomPreference+
        "\n  timePreferences="+iGlobalTimePreference+"/"+iBestTimePreference+
        "\n  groupConstraintPreferences="+iGlobalGroupConstraintPreference.get()+
        "\n}";
    }
    
    /** Overall number of too big rooms (rooms with more than 3/2 seats than needed) */
    public int countTooBigRooms() {
        int tooBigRooms=0;
        for (Enumeration it1=assignedVariables().elements();it1.hasMoreElements();) {
            Lecture lecture = (Lecture)it1.nextElement();
            if (lecture.getAssignment()==null) continue;
            Placement placement = (Placement)lecture.getAssignment();
            long roomSize = placement.getRoomLocation().getRoomSize();
            long students = lecture.countStudents();
            if (roomSize>TimetableModel.getMaxCapacity(students)) tooBigRooms++;
        }
        return tooBigRooms;
    }

    /** Overall departmental spread penalty */
    public int getDepartmentSpreadPenalty() {
        if (iDepartmentSpreadConstraints.isEmpty()) return 0;
        int penalty = 0;
        for (Enumeration e=iDepartmentSpreadConstraints.elements();e.hasMoreElements();) {
            DepartmentSpreadConstraint c = (DepartmentSpreadConstraint)e.nextElement();
            penalty += ((DepartmentSpreadConstraint)c).getPenalty();
        }
        return penalty;
    }
    
    /** Global info */
    public java.util.Hashtable getInfo() {
        Hashtable ret = super.getInfo();
        ret.put("Memory usage", edu.purdue.smas.timetable.util.Debug.getMem());
        ret.put("Room preferences", iGlobalRoomPreference+" / "+iBestRoomPreference);
        ret.put("Time preferences", sDoubleFormat.format(iGlobalTimePreference)+" / "+sDoubleFormat.format(iBestTimePreference));
        ret.put("Group constraint preferences", new Long(iGlobalGroupConstraintPreference.get()));
        if (getProperties().getPropertyBoolean("General.UseDistanceConstraints",false)) {
            ret.put("Distance student conflicts", new Long(getStudentDistanceConflicts()));
            ret.put("Distance instructor preferences", getInstructorDistancePreference()+" / "+getInstructorWorstDistancePreference());
        }
        if (getProperties().getPropertyBoolean("General.UseDepartmentSpreadConstraints", false)) {
            ret.put("Department balancing penalty", new Integer(getDepartmentSpreadPenalty()));
        }
        ret.put("Student conflicts", new Long(getViolatedStudentConflicts()));
        ret.put("Too big rooms", new Integer(countTooBigRooms()));
        ret.put("Hard student conflicts", new Long(getHardStudentConflicts()));
        ret.put("Useless half-hours", new Long(getUselessSlots()));
        return ret;
    }
    
    private int iBestTooBigRooms;
    private long iBestUselessSlots;
    private double iBestGlobalTimePreference;
    private long iBestGlobalRoomPreference;
    private long iBestGlobalGroupConstraintPreference;
    private long iBestViolatedStudentConflicts;
    private long iBestHardStudentConflicts;

    /** Overall number of too big rooms of the best solution ever found */
    public int bestTooBigRooms() { return iBestTooBigRooms; }
    /** Overall number of useless slots of the best solution ever found */
    public long bestUselessSlots() { return iBestUselessSlots;}
    /** Overall time preference of the best solution ever found */
    public double bestGlobalTimePreference() { return iBestGlobalTimePreference;}
    /** Overall room preference of the best solution ever found */
    public long bestGlobalRoomPreference() { return iBestGlobalRoomPreference;}
    /** Overall group constraint preference of the best solution ever found */
    public long bestGlobalGroupConstraintPreference() { return iBestGlobalGroupConstraintPreference;}
    /** Overall number of student conflicts of the best solution ever found */
    public long bestViolatedStudentConflicts() { return iBestViolatedStudentConflicts;}
    /** Overall number of student conflicts between single section classes of the best solution ever found */
    public long bestHardStudentConflicts() { return iBestHardStudentConflicts;}
    /** Overall instructor distance preference of the best solution ever found */
    public long bestInstructorDistancePreference() { return iBestInstructorDistancePreference; }
    /** Overall departmental spread penalty of the best solution ever found */
    public int bestDepartmentSpreadPenalty() { return iBestDepartmentSpreadPenalty; }
    
    public void saveBest() {
        super.saveBest();
        iBestTooBigRooms = countTooBigRooms();
        iBestUselessSlots = getUselessSlots();
        iBestGlobalTimePreference = getGlobalTimePreference();
        iBestGlobalRoomPreference = getGlobalRoomPreference();
        iBestGlobalGroupConstraintPreference = getGlobalGroupConstraintPreference();
        iBestViolatedStudentConflicts = getViolatedStudentConflicts();
        iBestHardStudentConflicts = getHardStudentConflicts();
        iBestInstructorDistancePreference = getInstructorDistancePreference();
        iBestDepartmentSpreadPenalty = getDepartmentSpreadPenalty();
    }

    public void addConstraint(Constraint constraint) {
        super.addConstraint(constraint);
        if (constraint instanceof InstructorConstraint) {
            iInstructorConstraints.addElement(constraint);
        } else if (constraint instanceof JenrlConstraint) {
            iJenrlConstraints.addElement(constraint);
        } else if (constraint instanceof RoomConstraint) {
            iRoomConstraints.addElement(constraint);
        } else if (constraint instanceof DepartmentSpreadConstraint) {
            iDepartmentSpreadConstraints.addElement(constraint);
        } else if (constraint instanceof GroupConstraint) {
            iGroupConstraints.addElement(constraint);
        }
    }
    public void removeConstraint(Constraint constraint) {
        super.removeConstraint(constraint);
        if (constraint instanceof InstructorConstraint) {
            iInstructorConstraints.removeElement(constraint);
        } else if (constraint instanceof JenrlConstraint) {
            iJenrlConstraints.removeElement(constraint);
        } else if (constraint instanceof RoomConstraint) {
            iRoomConstraints.removeElement(constraint);
        } else if (constraint instanceof DepartmentSpreadConstraint) {
            iDepartmentSpreadConstraints.removeElement(constraint);
        } else if (constraint instanceof GroupConstraint) {
            iGroupConstraints.removeElement(constraint);
        }
    }

    /** The list of all instructor constraints */
    public Vector getInstructorConstraints() { return iInstructorConstraints; }
    /** The list of all group constraints */
    public Vector getGroupConstraints() { return iGroupConstraints; }
    /** The list of all jenrl constraints */
    public Vector getJenrlConstraints() { return iJenrlConstraints; }
    /** The list of all room constraints */
    public Vector getRoomConstraints() { return iRoomConstraints; }
    /** The list of all departmental spread constraints */
    public Vector getDepartmentSpreadConstraints() { return iDepartmentSpreadConstraints;  }
    /** Max capacity for too big rooms (3/2 of the number of students) */
    public static long getMaxCapacity(long nrStudents) {
        return (3*nrStudents)/2;
    }
}
