/**
 * ScheduleSearchService.java
 * Created Jun 8, 2008
 * Copyright (c) TANDBERG Television 2007-2008
 */
package com.tandbergtv.watchpoint.pmm.schedule.search;

import static com.tandbergtv.watchpoint.pmm.schedule.search.ScheduleSearchKey.ACTIVE;
import static com.tandbergtv.watchpoint.pmm.schedule.search.ScheduleSearchKey.CONTEXT;
import static com.tandbergtv.watchpoint.pmm.schedule.search.ScheduleSearchKey.PARTNER_ID;
import static com.tandbergtv.watchpoint.pmm.schedule.search.ScheduleSearchKey.PITCH_DATE;
import static com.tandbergtv.workflow.driver.search.SearchParameterBase.DATE_FORMAT;
import static com.tandbergtv.workflow.driver.search.SearchType.DATE;
import static com.tandbergtv.workflow.driver.search.SearchType.NUMERIC;

import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.List;

import com.tandbergtv.watchpoint.pmm.assetlist.AssetListSearchService;
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.ScheduleStatus;
import com.tandbergtv.watchpoint.pmm.entities.Title;
import com.tandbergtv.watchpoint.pmm.entities.TitleListType;
import com.tandbergtv.watchpoint.pmm.schedule.ScheduleRuntimeException;
import com.tandbergtv.watchpoint.search.Entity;
import com.tandbergtv.workflow.driver.search.ListParameter;
import com.tandbergtv.workflow.driver.search.RangeParameter;
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;

/**
 * Schedule search service implementation
 * 
 * @author Sahil Verma
 */
public class ScheduleSearchService extends AssetListSearchService<Schedule> implements IScheduleSearchService {

	/**
	 * Creates a ScheduleSearchService
	 */
	public ScheduleSearchService() {
		super("Schedule Search");
	}
	
	@SuppressWarnings("unchecked")
	private <T extends Schedule> Collection<T> searchSchedules(SearchCriteria criteria) {
		Collection<T> schedules = (Collection<T>) super.search(criteria);
		return schedules;
	}

	/* (non-Javadoc)
	 * @see com.tandbergtv.watchpoint.pmm.schedule.search.IScheduleSearchService#getPlannersByDateRange(java.lang.Long, java.util.Date, java.util.Date)
	 */
	public Collection<Planner> getPlannersByDateRange(Long partnerId, Date start, Date end) {
		if (start == null && end == null)
			throw new ScheduleRuntimeException("At least one bound of the interval must be specified");
		
		SearchCriteria criteria = new SearchCriteria();
		
		Entity e = new Entity("planner", Planner.class, "p");
		
		e.addParameter(new ValueParameter(ACTIVE.toString(), NUMERIC, 1));
		e.addParameter(new ValueParameter(PARTNER_ID.toString(), NUMERIC, partnerId));
		
		SimpleDateFormat formatter = new SimpleDateFormat(DATE_FORMAT);
		RangeParameter range = new RangeParameter(PITCH_DATE.toString(), DATE);
		
		if (start != null)
			range.setTo(formatter.format(start));

		if (end != null)
			range.setTo(formatter.format(end));
		
		e.addParameter(range);
		
		criteria.addParameter(e);
		
		return searchSchedules(criteria);
	}

	/* (non-Javadoc)
	 * @see com.tandbergtv.watchpoint.pmm.schedule.search.IScheduleSearchService#getPlannersByDate(java.lang.Long, java.util.Date)
	 */
	public Collection<Planner> getPlannersByDate(Long partnerId, Date date) {
		if (date == null)
			throw new ScheduleRuntimeException("Planner arrival date must be specified");
		
		SearchCriteria criteria = new SearchCriteria();
		
		Entity e = new Entity("planner", Planner.class, "p");
		
		e.addParameter(new ValueParameter(ACTIVE.toString(), NUMERIC, 1));
		e.addParameter(new ValueParameter(PARTNER_ID.toString(), NUMERIC, partnerId));
		
		SimpleDateFormat formatter = new SimpleDateFormat(DATE_FORMAT);
		e.addParameter(new ValueParameter(PITCH_DATE.toString(), DATE, formatter.format(date)));
		
		criteria.addParameter(e);
		
		return searchSchedules(criteria);
	}

	/* (non-Javadoc)
	 * @see com.tandbergtv.watchpoint.pmm.schedule.search.IScheduleSearchService#getDistributionSchedulesByDateRange(java.lang.Long, java.util.Date, java.util.Date)
	 */
	public Collection<DistributionSchedule> getPitchSchedulesByDateRange(Long contextId, Date start, Date end) {
		if (start == null && end == null)
			throw new ScheduleRuntimeException("At least one bound of the interval must be specified");
		
		SearchCriteria criteria = new SearchCriteria();
		Entity e = new Entity("schedule", DistributionSchedule.class, "d");
		
		e.addParameter(new ValueParameter(ACTIVE.toString(), NUMERIC, 1));
		e.addParameter(new ValueParameter(CONTEXT.toString(), NUMERIC, contextId));
		addDateCriterion(e, start, end);
		
		criteria.addParameter(e);
		
		return searchSchedules(criteria);
	}

	/* (non-Javadoc)
	 * @see com.tandbergtv.watchpoint.pmm.schedule.search.IScheduleSearchService#getDistributionSchedulesByPitchDate(java.lang.Long, java.util.Date)
	 */
	public Collection<DistributionSchedule> getPitchSchedulesByPitchDate(Long contextId, Date date) {
		if (date == null)
			throw new ScheduleRuntimeException("Pitch date must be specified");
		
		SearchCriteria criteria = new SearchCriteria();
		Entity e = new Entity("schedule", DistributionSchedule.class, "d");
		
		e.addParameter(new ValueParameter(ACTIVE.toString(), NUMERIC, 1));
		e.addParameter(new ValueParameter(CONTEXT.toString(), NUMERIC, contextId));
		addDateCriterion(e, date, null);
		
		criteria.addParameter(e);
		
		return searchSchedules(criteria);
	}
	
	public Collection<DistributionSchedule> getPitchSchedulesByPitchDate(Long contextId, Date date,
			ScheduleStatus... status) {
		SearchCriteria criteria = new SearchCriteria();
		Entity e = new Entity("schedule", DistributionSchedule.class, "d");

		e.addParameter(new ValueParameter(ACTIVE.toString(), NUMERIC, 1));
		e.addParameter(new ValueParameter(CONTEXT.toString(), NUMERIC, contextId));
		addDateCriterion(e, date, null);

		ListParameter scheduleList = new ListParameter(ScheduleSearchKey.STATUS.toString(), NUMERIC);
		Object[] statusOrdinals = new Object[status.length];
		for (int i = 0; i < status.length; i++) {
			statusOrdinals[i] = status[i].ordinal();
		}
		scheduleList.addValues((Object[]) statusOrdinals);
		e.addParameter(scheduleList);

		criteria.addParameter(e);

		return searchSchedules(criteria);
	}

	/* (non-Javadoc)
	 * @see com.tandbergtv.watchpoint.pmm.schedule.search.IScheduleSearchService#getPreviousSchedule(com.tandbergtv.watchpoint.pmm.entities.Schedule)
	 */
	public <T extends Schedule> T getPreviousSchedule(T schedule) {
		Collection<T> schedules = getPreviousSchedules(schedule, 1);
		
		if (schedules == null || schedules.isEmpty())
			return null;
		
		return schedules.iterator().next();
	}

	/* (non-Javadoc)
	 * @see com.tandbergtv.watchpoint.pmm.schedule.search.IScheduleSearchService#getPreviousSchedules(com.tandbergtv.watchpoint.pmm.entities.Schedule, int)
	 */
	public <T extends Schedule> Collection<T> getPreviousSchedules(T schedule, int count) {
		SearchCriteria criteria = new SearchCriteria();
		Entity e = new Entity("schedule", schedule.getClass(), "d");
		
		e.addParameter(new ValueParameter(ACTIVE.toString(), NUMERIC, 1));
		
		/* We'll need to filter by the source or destination for a planner or pitch, respectively */
		if (schedule instanceof Planner) {
			Planner planner = Planner.class.cast(schedule);
			
			e.addParameter(new ValueParameter(PARTNER_ID.toString(), NUMERIC, planner.getSourcePartnerID()));
		} else if (schedule instanceof DistributionSchedule) {
			DistributionSchedule pitch = DistributionSchedule.class.cast(schedule);
			
			e.addParameter(new ValueParameter(CONTEXT.toString(), NUMERIC, pitch.getContextID()));
		}
		
		/* Search up until the specified schedule's date */
		SimpleDateFormat formatter = new SimpleDateFormat(DATE_FORMAT);
		RangeParameter range = new RangeParameter(PITCH_DATE.toString(), DATE);
		Date to = decrementDay(schedule.getDate());
		
		range.setTo(formatter.format(to));
		
		e.addParameter(range);
		
		e.addParameter(new SortParameter(PITCH_DATE.toString(), SortingOrder.DESCENDING));
		
		criteria.addParameter(e);
		
		criteria.setRecordsCount(count);
		
		Collection<T> schedules = searchSchedules(criteria);
		
		return schedules;
	}

	/**
	 * Adds a range parameter to the specified entity
	 * 
	 * @param e
	 * @param start
	 * @param end
	 */
	private void addDateCriterion(Entity e, Date start, Date end) {
		SimpleDateFormat formatter = new SimpleDateFormat(DATE_FORMAT);
		
		if (end != null) {
			RangeParameter range = new RangeParameter(PITCH_DATE.toString(), DATE, formatter.format(start));
			
			/* Range Parameter adds a day to the end date, so adjust the end date to handle this */
			Calendar calendar = Calendar.getInstance();
			calendar.setTime(end);
			calendar.add(Calendar.DAY_OF_MONTH, -1);
			range.setTo(formatter.format(calendar.getTime()));
			
			e.addParameter(range);
		} else {
			ValueParameter parameter = 
				new ValueParameter(PITCH_DATE.toString(), DATE, new SimpleDateFormat(DATE_FORMAT).format(start));
			
			e.addParameter(parameter);
		}
	}
	
	/**
	 * This is a hack to workaround the {@link RangeParameter} braindamage which adds one day
	 * to the interval
	 * 
	 * @param date
	 * @return
	 */
	private Date decrementDay(Date date) {
		Calendar calendar = new GregorianCalendar();
		
		calendar.setTime(date);
		calendar.add(Calendar.DATE, -1);
		
		return calendar.getTime();
	}

	/**
	 * Gets id of the earliest upcoming (>= today) planner that is associated with
	 * the given title. Returns null, if no upcoming planner found.
	 */
	public Long findEarliestUpcomingPlannerId(Title title) {
		List<Schedule> upcomingPlanners = new ArrayList<Schedule>();
		
		//find associated upcoming planners
		Date now = getToday();
		Collection<IAssetList> associatedAssetLists = title.getTitlelists();
		if(associatedAssetLists != null) {
			for(IAssetList associatedAssetList : associatedAssetLists) {
				if(associatedAssetList.getType() == TitleListType.PLANNER) {
					Schedule associatedPlanner = (Schedule) associatedAssetList; 
					if(associatedPlanner.getDate().compareTo(now) >= 0) {
						upcomingPlanners.add(associatedPlanner);
					}
				}
			}
		}
		
		if(upcomingPlanners.isEmpty())
			return null;
		
		//find earliest in associated upcoming planners
		Collections.sort(upcomingPlanners, new Comparator<Schedule>() {
			public int compare(Schedule o1, Schedule o2) {
				return o1.getDate().compareTo(o2.getDate());
			}
		});
		
		return upcomingPlanners.get(0).getId(); 
	}
	
	/**
	 * Gets the date value of 0 hours today.
	 */
	private Date getToday() {
		Calendar c = Calendar.getInstance();
		
		//set time to start of today
		c.set(Calendar.HOUR_OF_DAY, 0);
		c.set(Calendar.MINUTE, 0);
		c.set(Calendar.SECOND, 0);
		c.set(Calendar.MILLISECOND, 0);
		
		return c.getTime();
	}
}
