/**
 * ScheduleNotifier.java
 * Created Jul 2, 2008
 * Copyright (c) TANDBERG Television 2007-2008
 */
package com.tandbergtv.watchpoint.pmm.schedule.notify;

import static com.tandbergtv.watchpoint.pmm.schedule.search.ScheduleSearchKey.ACTIVE;
import static com.tandbergtv.watchpoint.pmm.schedule.search.ScheduleSearchKey.PITCH_DATE;
import static com.tandbergtv.workflow.driver.search.SearchType.DATE;
import static com.tandbergtv.workflow.driver.search.SearchType.NUMERIC;

import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashSet;
import java.util.Iterator;

import org.apache.log4j.Logger;

import com.tandbergtv.watchpoint.pmm.core.ProgressEvent;
import com.tandbergtv.watchpoint.pmm.entities.AssetList;
import com.tandbergtv.watchpoint.pmm.entities.DistributionSchedule;
import com.tandbergtv.watchpoint.pmm.entities.IAssetList;
import com.tandbergtv.watchpoint.pmm.entities.Planner;
import com.tandbergtv.watchpoint.pmm.entities.Schedule;
import com.tandbergtv.watchpoint.pmm.entities.TitleListType;
import com.tandbergtv.watchpoint.pmm.entities.event.AssetListEvent;
import com.tandbergtv.watchpoint.pmm.entities.event.TitleStatusUpdatedEvent;
import com.tandbergtv.watchpoint.pmm.schedule.ISchedulePersistenceService;
import com.tandbergtv.watchpoint.pmm.schedule.search.IScheduleSearchService;
import com.tandbergtv.watchpoint.search.Entity;
import com.tandbergtv.workflow.core.event.ColleaguePriority;
import com.tandbergtv.workflow.core.event.DefaultMediator;
import com.tandbergtv.workflow.core.event.IColleague;
import com.tandbergtv.workflow.core.event.WorkflowEvent;
import com.tandbergtv.workflow.core.service.ServiceRegistry;
import com.tandbergtv.workflow.core.service.thread.ISchedulerService;
import com.tandbergtv.workflow.core.service.thread.Scheduler;
import com.tandbergtv.workflow.driver.search.RangeParameter;
import com.tandbergtv.workflow.driver.search.SearchParameterBase;
import com.tandbergtv.workflow.driver.search.SortParameter;
import com.tandbergtv.workflow.driver.search.ValueParameter;
import com.tandbergtv.workflow.util.SearchCriteria;
import com.tandbergtv.workflow.util.SortingOrder;

/**
 * Implementation of the {@link IScheduleNotifier} service which writes out warning messages to the
 * log file
 * 
 * @author Sahil Verma
 */
public class ScheduleNotifierService implements IScheduleNotifier, IColleague {

	private ISchedulerService<Void> scheduler;
	
	/* FIXME Locking. And this is a VERY poor choice of data structure */
	private Collection<Notification> notifications;
	
	private static final long ONE_MINUTE_MILLIS = 60 * 1000L;
	
	private static final long ONE_HOUR_MILLIS = 60 * ONE_MINUTE_MILLIS;
	
	private static final long ONE_DAY_MILLIS = 24 * ONE_HOUR_MILLIS;
	
	private static final String SERVICE_NAME = "Schedule Notifier";
	
	private static final Logger logger = Logger.getLogger(ScheduleNotifierService.class);
	
	/**
	 * Creates a ScheduleNotifierService
	 */
	public ScheduleNotifierService() {
		this.notifications = new HashSet<Notification>();
	}

	/* (non-Javadoc)
	 * @see com.tandbergtv.watchpoint.pmm.schedule.notify.IScheduleNotifier#getNotification(com.tandbergtv.watchpoint.pmm.entities.Schedule)
	 */
	public Notification getNotification(Schedule schedule) {
		for (Notification notification : this.notifications) {
			if (notification.getSchedule().equals(schedule))
				return notification;
		}
		
		return null;
	}
	
	/* (non-Javadoc)
	 * @see com.tandbergtv.workflow.core.event.IColleague#getColleagueName()
	 */
	public String getColleagueName() {
		return SERVICE_NAME;
	}

	/* (non-Javadoc)
	 * @see com.tandbergtv.workflow.core.event.IColleague#getColleaguePriority()
	 */
	public ColleaguePriority getColleaguePriority() {
		return ColleaguePriority.LOW;
	}

	/* (non-Javadoc)
	 * @see com.tandbergtv.workflow.core.event.IColleague#receive(com.tandbergtv.workflow.core.event.WorkflowEvent)
	 */
	public void receive(WorkflowEvent event) {
		// only expects these 3 types of events
		if (!(event instanceof AssetListEvent || event instanceof ProgressEvent || 
				event instanceof TitleStatusUpdatedEvent))
			return;
		
		if (event instanceof TitleStatusUpdatedEvent) {
			Collection<IAssetList> associatedAssetLists = ((TitleStatusUpdatedEvent) event)
					.getTitle().getTitlelists();
			if (associatedAssetLists == null) {
				return;
			}
			/* For each planner associated with this title, recalculate the notification */
			for (IAssetList associatedAssetList : associatedAssetLists) {
				// a title may be associated with multiple planners.
				if (associatedAssetList.getType() == TitleListType.PLANNER) {
					ISchedulePersistenceService service = ServiceRegistry.getDefault().lookup(
									ISchedulePersistenceService.class);
					Schedule s = service.get(associatedAssetList.getId());								
					reCalculateNotification(s);
				}
			}
		} else {
			Schedule schedule = getSchedule(event);
			if (schedule == null)
				return;
			reCalculateNotification(schedule);
		}
	}	

	/* 
	 * Recalculate. Several cases - title got added and is late, a late title is removed, 
	 * progress for a late title arrives, pitch date of a list with late title(s) changes and 
	 * now falls within the 'alert' window etc etc
	 */	
	private void reCalculateNotification(Schedule schedule) {
		logger.debug(schedule + ", recalculating notification");
		
		removeNotification(schedule);

		if (fallsWithinWindow(schedule)) {
			Notification notification = generate(schedule);

			// Next scheduler run will take care of logging the warning
			if (notification != null)
				addNotification(notification);
		}
	}
	
	/* (non-Javadoc)
	 * @see com.tandbergtv.workflow.core.service.Service#getServiceName()
	 */
	public String getServiceName() {
		return SERVICE_NAME;
	}

	/* (non-Javadoc)
	 * @see com.tandbergtv.workflow.core.service.ServiceLifecycle#start()
	 */
	public void start() {
		scheduler = new Scheduler<Void>("Notifications", 1, 1);
		scheduler.start();
		
		DefaultMediator.getInstance().register(this);
		
		this.scheduler.schedule(new Runnable() {
			/* (non-Javadoc)
			 * @see java.lang.Runnable#run()
			 */
			public void run() {
				try {
					generate();
				} catch (Exception e) {
					logger.error("Failure during generating notifications", e);
				}
			}
		}, 0, ONE_DAY_MILLIS); /* Hmmmm so ugly. API should take TimeUnit parameter. */
	}

	/* (non-Javadoc)
	 * @see com.tandbergtv.workflow.core.service.ServiceLifecycle#stop()
	 */
	public void stop() {
		scheduler.stop();
	}
	
	/**
	 * Extracts the schedule using information in the event
	 * 
	 * @param event
	 * @return the schedule, or null if the event is not associated with a schedule
	 */
	private Schedule getSchedule(WorkflowEvent event) {
		if (event instanceof AssetListEvent) {
			AssetListEvent e = AssetListEvent.class.cast(event);
			AssetList list = e.getList();
			
			if (list instanceof Schedule)
				return Schedule.class.cast(list);
		} else {
			ProgressEvent e = ProgressEvent.class.cast(event);
			Long scheduleId = e.getScheduleId();
			
			if (scheduleId != null) {
				ISchedulePersistenceService service = 
					ServiceRegistry.getDefault().lookup(ISchedulePersistenceService.class); 
				
				return service.get(scheduleId);
			}
		}
		
		return null;
	}
	
	private void generate() {
		Collection<Schedule> schedules = getSchedules();

		/*
		 * Get rid of everything. We might be transitioning to a new month, don't want to display
		 * notifications beyond window start.
		 */
		removeNotifications();

		for (Schedule schedule : schedules) {
			Notification notification = generate(schedule);
			
			if (notification != null)
				notify(notification);
		}
	}
	
	private Notification generate(Schedule schedule) {
		/* Check for types of schedules for which notifications are supported */
		if (!(schedule instanceof Planner) && !(schedule instanceof DistributionSchedule))
			return null;
		
		NotificationGeneratorFactory factory = NotificationGeneratorFactory.newInstance();
		INotificationGenerator generator = factory.newGenerator(schedule);
		Notification notification = generator.getNotification(schedule);

		return notification;
	}
	
	/**
	 * Adds the specified notification and also emits its string representation to the log
	 * 
	 * @param notification
	 */
	private void notify(Notification notification) {
		addNotification(notification);
		
		for (String message : notification.getMessages())
			logger.warn(message);
	}
	
	/**
	 * Adds the specified notification
	 * 
	 * @param notification
	 */
	private void addNotification(Notification notification) {
		this.notifications.add(notification);
	}
	
	private void removeNotifications() {
		this.notifications.clear();
	}
	
	/**
	 * Removes notification for the specified schedule
	 * 
	 * @param schedule
	 */
	private void removeNotification(Schedule schedule) {
		Iterator<Notification> i = this.notifications.iterator();
		
		while (i.hasNext()) {
			Notification notification = i.next();
			
			if (schedule.equals(notification.getSchedule())) {
				i.remove();
				break;
			}
		}
	}
	
	private boolean fallsWithinWindow(Schedule schedule) {
		Date date = schedule.getDate();
		
		return (date.after(getWindowStart()) && date.before(getWindowEnd()));
	}
	
	private Collection<Schedule> getSchedules() {
		IScheduleSearchService service = ServiceRegistry.getDefault().lookup(IScheduleSearchService.class);
		Collection<Schedule> schedules = service.search(getSearchCriteria());
		
		return schedules;
	}
	
	private SearchCriteria getSearchCriteria() {
		SearchCriteria criteria = new SearchCriteria();
		Entity e = new Entity("schedule", Schedule.class, "s");
		
		e.addParameter(new ValueParameter(ACTIVE.toString(), NUMERIC, 1));
		e.addParameter(getDateRangeCriterion());
		e.addParameter(new SortParameter(PITCH_DATE.toString(), SortingOrder.DESCENDING));
		
		criteria.addParameter(e);
		
		return criteria;
	}
	
	private SearchParameterBase getDateRangeCriterion() {
		/* Search for schedules within the (arbitrary :) time window */
		DateFormat formatter = new SimpleDateFormat("yyyy-MM-dd");
		Date from = getWindowStart();
		RangeParameter range = new RangeParameter(PITCH_DATE.toString(), DATE, formatter.format(from));
		Date to = getWindowEnd();
		
		range.setTo(formatter.format(to));
		
		return range;
	}
	
	private Date getWindowStart() {
		Date today = new Date();
		Calendar calendar = new GregorianCalendar();
		
		calendar.setTime(today);
		calendar.add(Calendar.MONTH, -1); /* FIXME Configurable */
		calendar.set(Calendar.DATE, 1);
		
		return calendar.getTime();
	}
	
	private Date getWindowEnd() {
		Date today = new Date();
		
		Calendar calendar = new GregorianCalendar();
		
		calendar.setTime(today);
		calendar.add(Calendar.DAY_OF_MONTH, 7);
		
		return calendar.getTime();
	}
}
