/**
 * 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.Calendar;
import java.util.Collection;
import java.util.Date;
import java.util.GregorianCalendar;

import org.apache.log4j.Logger;
import org.hibernate.Query;
import org.hibernate.Session;

import com.tandbergtv.cms.portal.util.transaction.Transactional;
import com.tandbergtv.watchpoint.pmm.dao.hibernate.HibernateContext;
import com.tandbergtv.watchpoint.pmm.entities.DistributionSchedule;
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.schedule.ScheduleRuntimeException;
import com.tandbergtv.watchpoint.search.Entity;
import com.tandbergtv.watchpoint.search.QueryBuilder;
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 implements IScheduleSearchService {
	
	private static final Logger logger = Logger.getLogger(ScheduleSearchService.class);

	/**
	 * Creates a ScheduleSearchService
	 */
	public ScheduleSearchService() {
		super();
	}

	/* (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 search(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 search(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 search(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 search(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 search(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 = search(criteria);
		
		return schedules;
	}

	/* (non-Javadoc)
	 * @see com.tandbergtv.watchpoint.pmm.schedule.search.IScheduleSearchService#search(com.tandbergtv.workflow.util.SearchCriteria)
	 */
	@Transactional
	public <T extends Schedule> Collection<T> search(SearchCriteria criteria) {
		String s = getQuery(criteria);
		
		logger.debug("Executing query :" + System.getProperty("line.separator") + s);
		
		Session session = getSession();
		Query query = session.createQuery(s);

		return (Collection<T>) query.list();
	}
	
	/* (non-Javadoc)
	 * @see com.tandbergtv.watchpoint.pmm.schedule.search.IScheduleSearchService#count(com.tandbergtv.workflow.util.SearchCriteria)
	 */
	@Transactional
	public int count(SearchCriteria criteria) {
		String s = QueryBuilder.newInstance().buildCountQuery(criteria);
		logger.debug("Executing query :" + System.getProperty("line.separator") + s);
		
		Session session = getSession();
		Query query = session.createQuery(s);

		long count = (Long) query.uniqueResult();
		return (int)count;
	}

	/* (non-Javadoc)
	 * @see com.tandbergtv.workflow.core.service.Service#getServiceName()
	 */
	public String getServiceName() {
		return "Schedule Search";
	}

	/* (non-Javadoc)
	 * @see com.tandbergtv.workflow.core.service.ServiceLifecycle#start()
	 */
	public void start() {
	}

	/* (non-Javadoc)
	 * @see com.tandbergtv.workflow.core.service.ServiceLifecycle#stop()
	 */
	public void stop() {
	}

	/**
	 * 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();
	}
	
	/**
	 * Returns the select HQL query for the specified criteria
	 * 
	 * @param criteria
	 * @return
	 */
	public String getQuery(SearchCriteria criteria) {
		return QueryBuilder.newInstance().buildQuery(criteria);
	}
	
	private Session getSession() {
		return HibernateContext.getContext().getCurrentSession();
	}
}
