/* 
 * Licensed to The Apereo Foundation under one or more contributor license
 * agreements. See the NOTICE file distributed with this work for
 * additional information regarding copyright ownership.
 *
 * The Apereo Foundation licenses this file to you under the Apache License,
 * Version 2.0 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at:
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 */ 
 
package org.unitime.timetable.model;

import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.TreeSet;


import org.cpsolver.ifs.util.ToolBox;
import org.unitime.timetable.model.base.BaseEvent;
import org.unitime.timetable.model.dao.EventDAO;
import org.unitime.timetable.util.Constants;
import org.unitime.timetable.util.Formats;


/**
 * @author Tomas Muller, Stephanie Schluttenhofer, Zuzana Mullerova
 */
public abstract class Event extends BaseEvent implements Comparable<Event> {
	private static final long serialVersionUID = 1L;

/*[CONSTRUCTOR MARKER BEGIN]*/
	public Event () {
		super();
	}

	/**
	 * Constructor for primary key
	 */
	public Event (java.lang.Long uniqueId) {
		super(uniqueId);
	}

/*[CONSTRUCTOR MARKER END]*/
	
	public static final int sEventTypeClass = 0;
	public static final int sEventTypeFinalExam = 1;
	public static final int sEventTypeMidtermExam = 2;
	public static final int sEventTypeCourse = 3;
	public static final int sEventTypeSpecial = 4;
	public static final int sEventTypeUnavailable = 5;
	public static final String[] sEventTypes = new String[] {
	    "Class Event",
	    "Final Examination Event",
	    "Midterm Examination Event",
	    "Course Related Event",
	    "Special Event",
	    "Not Available Event"
	};
    public static final String[] sEventTypesAbbv = new String[] {
        "Class",
        "Final Exam",
        "Midterm Exam",
        "Course",
        "Special",
        "Not Available"
    };

	public abstract int getEventType();
	
	public String getEventTypeLabel() { return sEventTypes[getEventType()]; }
	
	public String getEventTypeAbbv() { return sEventTypesAbbv[getEventType()]; }
	
    public abstract Set<Student> getStudents();
    
    public abstract Collection<StudentClassEnrollment> getStudentClassEnrollments();
    
    public abstract Collection<Long> getStudentIds();
    
    public abstract Set<DepartmentalInstructor> getInstructors();

    public static void deleteFromEvents(org.hibernate.Session hibSession, Integer ownerType, Long ownerId) {
        for (Iterator i=hibSession.createQuery("select r from CourseEvent e inner join e.relatedCourses r where "+
                "r.ownerType=:ownerType and r.ownerId=:ownerId")
                .setInteger("ownerType", ownerType)
                .setLong("ownerId", ownerId).list().iterator();i.hasNext();) {
            RelatedCourseInfo relatedCourse = (RelatedCourseInfo)i.next();
            CourseEvent event = relatedCourse.getEvent();
            event.getRelatedCourses().remove(relatedCourse);
            relatedCourse.setOwnerId(null);
            relatedCourse.setCourse(null);
            hibSession.delete(relatedCourse);
            hibSession.saveOrUpdate(event);
        }
    }
    
    public static void deleteFromEvents(org.hibernate.Session hibSession, Class_ clazz) {
    	deleteFromEvents(hibSession, ExamOwner.sOwnerTypeClass, clazz.getUniqueId());
    }
    public static void deleteFromEvents(org.hibernate.Session hibSession, InstrOfferingConfig config) {
    	deleteFromEvents(hibSession, ExamOwner.sOwnerTypeConfig, config.getUniqueId());
    }
    public static void deleteFromEvents(org.hibernate.Session hibSession, InstructionalOffering offering) {
    	deleteFromEvents(hibSession, ExamOwner.sOwnerTypeOffering, offering.getUniqueId());
    }
    public static void deleteFromEvents(org.hibernate.Session hibSession, CourseOffering course) {
    	deleteFromEvents(hibSession, ExamOwner.sOwnerTypeCourse, course.getUniqueId());
    }
    
	public String toString() {
		return (this.getEventName());
	}
	
	public String eventCapacityDisplayString(){
		String s = "";
		if (getMinCapacity() != null){
			s += getMinCapacity().toString();
			if (!getMaxCapacity().equals(getMinCapacity())){
				s = s + "-" + getMaxCapacity().toString();
			}
		}
		return(s);
	}
	public int compareTo (Event e) {
		if (getEventName()!=e.getEventName()) {
			return getEventName().compareTo(e.getEventName());
		} else return (getUniqueId() == null ? new Long(-1) : getUniqueId()).compareTo(e.getUniqueId() == null ? -1 : e.getUniqueId());
	}

	public static List findAll() {
	    return new EventDAO().getSession().createQuery(
	            "select e from Event e"
	            )
	            .setCacheable(true)
	            .list();
	}
	
	public TreeSet<MultiMeeting> getMultiMeetings() {
	    return getMultiMeetings(getMeetings());
	}
	
    public static TreeSet<MultiMeeting> getMultiMeetings(Collection meetings) {
        Date now = new Date();
        TreeSet<MultiMeeting> ret = new TreeSet<MultiMeeting>();
        HashSet<Meeting> meetingSet = new HashSet<Meeting>(meetings);
        while (!meetingSet.isEmpty()) {
            Meeting meeting = null;
            for (Meeting m : meetingSet)
                if (meeting==null || meeting.getMeetingDate().compareTo(m.getMeetingDate())>0)
                    meeting = m;
            meetingSet.remove(meeting);
            Hashtable<Long,Meeting> similar = new Hashtable(); 
            TreeSet<Integer> dow = new TreeSet<Integer>(); dow.add(meeting.getDayOfWeek());
            boolean past = meeting.getStartTime().before(now);
            for (Meeting m : meetingSet) {
                if (ToolBox.equals(m.getEvent().getUniqueId(),meeting.getEvent().getUniqueId()) &&
                    ToolBox.equals(m.getStartPeriod(),meeting.getStartPeriod()) &&
                    ToolBox.equals(m.getStartOffset(),meeting.getStartOffset()) &&
                    ToolBox.equals(m.getStopPeriod(),meeting.getStopPeriod()) &&
                    ToolBox.equals(m.getStopOffset(),meeting.getStopOffset()) &&
                    ToolBox.equals(m.getLocationPermanentId(),meeting.getLocationPermanentId()) &&
                    past==m.getStartTime().before(now) &&
                    m.isApproved()==meeting.isApproved()) {
                    dow.add(m.getDayOfWeek());
                    similar.put(m.getMeetingDate().getTime(),m);
                }
            }
            TreeSet<Meeting> multi = new TreeSet<Meeting>(); multi.add(meeting);
            if (!similar.isEmpty()) {
                Calendar c = Calendar.getInstance(Locale.US);
                c.setTimeInMillis(meeting.getMeetingDate().getTime());
                while (true) {
                    do {
                        c.add(Calendar.DAY_OF_YEAR, 1);
                    } while (!dow.contains(c.get(Calendar.DAY_OF_WEEK)));
                    Meeting m = similar.get(c.getTimeInMillis()); 
                    if (m==null) break;
                    multi.add(m);
                    meetingSet.remove(m);
                }
            }
            ret.add(new MultiMeeting(multi,past));
        }
        return ret;
    }
	
	public static class MultiMeeting implements Comparable<MultiMeeting> {
	    private TreeSet<Meeting> iMeetings;
	    private boolean iPast = false;
	    
	    public MultiMeeting(TreeSet<Meeting> meetings, boolean past) {
	        iMeetings = meetings;
	        iPast = past;
	    }
	    
	    public boolean isPast() { return iPast; }
	    
	    public TreeSet<Meeting> getMeetings() { return iMeetings; }

	    public int compareTo(MultiMeeting m) {
	        return getMeetings().first().compareTo(m.getMeetings().first());
	    }
	    
	    public String getDays() {
	        return getDays(Constants.DAY_NAME, Constants.DAY_NAMES_SHORT);
	    }
	    
	    public String getDays(String[] dayNames, String[] shortDyNames) {
	        int nrDays = 0;
	        int dayCode = 0;
	        for (Meeting meeting : getMeetings()) {
	            int dc = 0;
	            switch (meeting.getDayOfWeek()) {
                case Calendar.MONDAY    : dc = Constants.DAY_CODES[Constants.DAY_MON]; break;
                case Calendar.TUESDAY   : dc = Constants.DAY_CODES[Constants.DAY_TUE]; break;
                case Calendar.WEDNESDAY : dc = Constants.DAY_CODES[Constants.DAY_WED]; break;
                case Calendar.THURSDAY  : dc = Constants.DAY_CODES[Constants.DAY_THU]; break;
                case Calendar.FRIDAY    : dc = Constants.DAY_CODES[Constants.DAY_FRI]; break;
                case Calendar.SATURDAY  : dc = Constants.DAY_CODES[Constants.DAY_SAT]; break;
                case Calendar.SUNDAY    : dc = Constants.DAY_CODES[Constants.DAY_SUN]; break;
	            }
	            if ((dayCode & dc)==0) nrDays++;
	            dayCode |= dc;
	        }
            String ret = "";
	        for (int i=0;i<Constants.DAY_CODES.length;i++) {
	            if ((dayCode & Constants.DAY_CODES[i])!=0)
	                ret += (nrDays==1?dayNames:shortDyNames)[i];
	        }
            return ret;
	    }
	    
	    public String toString() {
	        return getDays()+" "+
	        	Formats.getDateFormat(Formats.Pattern.DATE_EVENT_SHORT).format(getMeetings().first().getMeetingDate())+
	            (getMeetings().size()>1?" - "+Formats.getDateFormat(Formats.Pattern.DATE_EVENT_SHORT).format(getMeetings().last().getMeetingDate()):"")+" "+
	            (getMeetings().first().isAllDay()?"All Day":getMeetings().first().startTime()+" - "+getMeetings().first().stopTime())+
	            (getMeetings().first().getLocation()==null?"":" "+getMeetings().first().getLocation().getLabel());
	    }
	    
	    public String toShortString() {
	    	return getDays(Constants.DAY_NAMES_SHORT, Constants.DAY_NAMES_SHORT)+" "+
	            	Formats.getDateFormat(Formats.Pattern.DATE_EVENT_SHORT).format(getMeetings().first().getMeetingDate())+
	                (getMeetings().size()>1?" - "+Formats.getDateFormat(Formats.Pattern.DATE_EVENT_SHORT).format(getMeetings().last().getMeetingDate()):"")+" "+
	                (getMeetings().first().isAllDay()?"All Day":getMeetings().first().startTime())+
	                (getMeetings().first().getLocation()==null?"":" "+getMeetings().first().getLocation().getLabel());
	    }
	}
	
	public Session getSession() { return null; }
	
	public static Hashtable<Event,Set<Long>> findStudentConflicts(Date meetingDate, int startSlot, int endSlot, Set<Long> studentIds) {
        Hashtable<Event,Set<Long>> ret = new Hashtable();
        if (studentIds==null || studentIds.isEmpty()) return ret;

        String students = "";
        int nrStudents = 0;
        for (Long id: studentIds) {
        	students += (students.length()==0?"":",")+id;
        	nrStudents++;
        	if (nrStudents == 1000) {
                //class events
                for (Iterator i=EventDAO.getInstance().getSession().createQuery(
                        "select e, s.student.uniqueId from "+
                        "ClassEvent e inner join e.meetings m inner join e.clazz.studentEnrollments s where "+
                        "m.meetingDate=:meetingDate and m.startPeriod < :endSlot and m.stopPeriod > :startSlot and s.student.uniqueId in ("+students+")")
                        .setDate("meetingDate", meetingDate)
                        .setInteger("startSlot", startSlot)
                        .setInteger("endSlot", endSlot)
                        .setCacheable(true).list().iterator();i.hasNext();) {
                    Object[] o = (Object[])i.next();
                    Event event = (Event)o[0];
                    long studentId = (Long)o[1];
                    Set<Long> conf = ret.get(event);
                    if (conf==null) { conf = new HashSet(); ret.put(event, conf); }
                    conf.add(studentId);
                }
                
                //examination events
                for (Iterator i=EventDAO.getInstance().getSession().createQuery(
                        "select e, s.student.uniqueId from "+
                        "ExamEvent e inner join e.meetings m inner join e.exam.owners o, StudentClassEnrollment s where "+
                        "m.meetingDate=:meetingDate and m.startPeriod < :endSlot and m.stopPeriod > :startSlot and s.student.uniqueId in ("+students+") and ("+
                        "(o.ownerType=:classType and s.clazz.uniqueId=o.ownerId) or "+
                        "(o.ownerType=:configType and s.clazz.schedulingSubpart.instrOfferingConfig.uniqueId=o.ownerId) or "+
                        "(o.ownerType=:courseType and s.courseOffering.uniqueId=o.ownerId) or "+
                        "(o.ownerType=:offeringType and s.courseOffering.instructionalOffering.uniqueId=o.ownerId))")
                        .setDate("meetingDate", meetingDate)
                        .setInteger("startSlot", startSlot)
                        .setInteger("endSlot", endSlot)
                        .setInteger("classType", ExamOwner.sOwnerTypeClass)
                        .setInteger("configType", ExamOwner.sOwnerTypeConfig)
                        .setInteger("courseType", ExamOwner.sOwnerTypeCourse)
                        .setInteger("offeringType", ExamOwner.sOwnerTypeOffering)
                        .setCacheable(true).list().iterator();i.hasNext();) {
                    Object[] o = (Object[])i.next();
                    Event event = (Event)o[0];
                    long studentId = (Long)o[1];
                    Set<Long> conf = ret.get(event);
                    if (conf==null) { conf = new HashSet(); ret.put(event, conf); }
                    conf.add(studentId);
                }
                
                //course events with required attendance
                for (Iterator i=EventDAO.getInstance().getSession().createQuery(
                        "select e, s.student.uniqueId from "+
                        "CourseEvent e inner join e.meetings m inner join e.relatedCourses o, StudentClassEnrollment s where e.reqAttendance=true and m.approvalStatus = 1 and "+
                        "m.meetingDate=:meetingDate and m.startPeriod < :endSlot and m.stopPeriod > :startSlot and s.student.uniqueId in ("+students+") and ("+
                        "(o.ownerType=:classType and s.clazz.uniqueId=o.ownerId) or "+
                        "(o.ownerType=:configType and s.clazz.schedulingSubpart.instrOfferingConfig.uniqueId=o.ownerId) or "+
                        "(o.ownerType=:courseType and s.courseOffering.uniqueId=o.ownerId) or "+
                        "(o.ownerType=:offeringType and s.courseOffering.instructionalOffering.uniqueId=o.ownerId))")
                        .setDate("meetingDate", meetingDate)
                        .setInteger("startSlot", startSlot)
                        .setInteger("endSlot", endSlot)
                        .setInteger("classType", ExamOwner.sOwnerTypeClass)
                        .setInteger("configType", ExamOwner.sOwnerTypeConfig)
                        .setInteger("courseType", ExamOwner.sOwnerTypeCourse)
                        .setInteger("offeringType", ExamOwner.sOwnerTypeOffering)
                        .setCacheable(true).list().iterator();i.hasNext();) {
                    Object[] o = (Object[])i.next();
                    Event event = (Event)o[0];
                    long studentId = (Long)o[1];
                    Set<Long> conf = ret.get(event);
                    if (conf==null) { conf = new HashSet(); ret.put(event, conf); }
                    conf.add(studentId);
                }
                nrStudents = 0; students = "";
        	}
        }
        
        if (nrStudents > 0) {
            //class events
            for (Iterator i=EventDAO.getInstance().getSession().createQuery(
                    "select e, s.student.uniqueId from "+
                    "ClassEvent e inner join e.meetings m inner join e.clazz.studentEnrollments s where "+
                    "m.meetingDate=:meetingDate and m.startPeriod < :endSlot and m.stopPeriod > :startSlot and s.student.uniqueId in ("+students+")")
                    .setDate("meetingDate", meetingDate)
                    .setInteger("startSlot", startSlot)
                    .setInteger("endSlot", endSlot)
                    .setCacheable(true).list().iterator();i.hasNext();) {
                Object[] o = (Object[])i.next();
                Event event = (Event)o[0];
                long studentId = (Long)o[1];
                Set<Long> conf = ret.get(event);
                if (conf==null) { conf = new HashSet(); ret.put(event, conf); }
                conf.add(studentId);
            }
            
            //examination events
            for (Iterator i=EventDAO.getInstance().getSession().createQuery(
                    "select e, s.student.uniqueId from "+
                    "ExamEvent e inner join e.meetings m inner join e.exam.owners o, StudentClassEnrollment s where "+
                    "m.meetingDate=:meetingDate and m.startPeriod < :endSlot and m.stopPeriod > :startSlot and s.student.uniqueId in ("+students+") and ("+
                    "(o.ownerType=:classType and s.clazz.uniqueId=o.ownerId) or "+
                    "(o.ownerType=:configType and s.clazz.schedulingSubpart.instrOfferingConfig.uniqueId=o.ownerId) or "+
                    "(o.ownerType=:courseType and s.courseOffering.uniqueId=o.ownerId) or "+
                    "(o.ownerType=:offeringType and s.courseOffering.instructionalOffering.uniqueId=o.ownerId))")
                    .setDate("meetingDate", meetingDate)
                    .setInteger("startSlot", startSlot)
                    .setInteger("endSlot", endSlot)
                    .setInteger("classType", ExamOwner.sOwnerTypeClass)
                    .setInteger("configType", ExamOwner.sOwnerTypeConfig)
                    .setInteger("courseType", ExamOwner.sOwnerTypeCourse)
                    .setInteger("offeringType", ExamOwner.sOwnerTypeOffering)
                    .setCacheable(true).list().iterator();i.hasNext();) {
                Object[] o = (Object[])i.next();
                Event event = (Event)o[0];
                long studentId = (Long)o[1];
                Set<Long> conf = ret.get(event);
                if (conf==null) { conf = new HashSet(); ret.put(event, conf); }
                conf.add(studentId);
            }
            
            //course events with required attendance
            for (Iterator i=EventDAO.getInstance().getSession().createQuery(
                    "select e, s.student.uniqueId from "+
                    "CourseEvent e inner join e.meetings m inner join e.relatedCourses o, StudentClassEnrollment s where e.reqAttendance=true and m.approvalStatus = 1 and "+
                    "m.meetingDate=:meetingDate and m.startPeriod < :endSlot and m.stopPeriod > :startSlot and s.student.uniqueId in ("+students+") and ("+
                    "(o.ownerType=:classType and s.clazz.uniqueId=o.ownerId) or "+
                    "(o.ownerType=:configType and s.clazz.schedulingSubpart.instrOfferingConfig.uniqueId=o.ownerId) or "+
                    "(o.ownerType=:courseType and s.courseOffering.uniqueId=o.ownerId) or "+
                    "(o.ownerType=:offeringType and s.courseOffering.instructionalOffering.uniqueId=o.ownerId))")
                    .setDate("meetingDate", meetingDate)
                    .setInteger("startSlot", startSlot)
                    .setInteger("endSlot", endSlot)
                    .setInteger("classType", ExamOwner.sOwnerTypeClass)
                    .setInteger("configType", ExamOwner.sOwnerTypeConfig)
                    .setInteger("courseType", ExamOwner.sOwnerTypeCourse)
                    .setInteger("offeringType", ExamOwner.sOwnerTypeOffering)
                    .setCacheable(true).list().iterator();i.hasNext();) {
                Object[] o = (Object[])i.next();
                Event event = (Event)o[0];
                long studentId = (Long)o[1];
                Set<Long> conf = ret.get(event);
                if (conf==null) { conf = new HashSet(); ret.put(event, conf); }
                conf.add(studentId);
            }
        }

        return ret;
    }
}