/**
 * ScheduleValidator.java
 * Created on Aug 14, 2008
 * (C) Copyright TANDBERG Television Ltd.
 */
package com.tandbergtv.watchpoint.pmm.web.validators;

import static com.tandbergtv.watchpoint.pmm.schedule.ScheduleValidationCode.LICENSE_EXPIRES;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
import java.util.Iterator;
import java.util.List;

import org.apache.log4j.Logger;
import org.apache.struts.action.ActionMessage;

import com.tandbergtv.watchpoint.pmm.entities.DistributionSchedule;
import com.tandbergtv.watchpoint.pmm.entities.IContainer;
import com.tandbergtv.watchpoint.pmm.entities.Schedule;
import com.tandbergtv.watchpoint.pmm.entities.Title;
import com.tandbergtv.watchpoint.pmm.schedule.ISchedulePersistenceService;
import com.tandbergtv.watchpoint.pmm.schedule.search.IScheduleSearchService;
import com.tandbergtv.watchpoint.pmm.title.conf.IRightsManager;
import com.tandbergtv.watchpoint.pmm.title.conf.specs.RightsManagerFactory;
import com.tandbergtv.watchpoint.pmm.util.validation.ValidationException;
import com.tandbergtv.watchpoint.pmm.util.validation.ValidationMessage;
import com.tandbergtv.watchpoint.pmm.web.schedule.ScheduleForm;
import com.tandbergtv.watchpoint.pmm.web.schedule.ScheduleFormPopulator;
import com.tandbergtv.watchpoint.pmm.web.util.CommonUtils;
import com.tandbergtv.workflow.core.service.ServiceRegistry;
import com.tandbergtv.workflow.core.service.cache.ICacheService;

/**
 * This class includes all validation methods for various fields in a schedule form.
 * 
 * @author spuranik
 * 
 */
public class ScheduleValidator {

	// date field in schedule form
	private static final String DATE_FIELD = "date";

	private static final Logger logger = Logger.getLogger(ScheduleValidator.class);
	private static String CONTAINER_CACHE_SERVICE_NAME = "Container Cache";

	/**
	 * This method verifies: - The pitch date is in the correct format - If there is already a pitch
	 * schedule for this date and given destination.
	 * 
	 * @param obj
	 * @param action
	 * @param field
	 * @param msgs
	 * @param validator
	 * @param request
	 * @return
	 */
	@SuppressWarnings("unchecked")
	public static boolean validatePitchDate(java.lang.Object obj,
			org.apache.commons.validator.ValidatorAction action,
			org.apache.commons.validator.Field field, org.apache.struts.action.ActionMessages msgs,
			org.apache.commons.validator.Validator validator,
			javax.servlet.http.HttpServletRequest request) {

		// do the validation only if the schedule is being created/modified
		if (!(request.getRequestURI().endsWith("saveschedule.do") || request.getRequestURI()
				.endsWith("modifyschedule.do"))) {
			return true;
		}

		ScheduleForm form = (ScheduleForm) obj;
		// currently there is no validation for a planner
		if (form.getIsPlanner()) {
			return true;
		}

		long contextId = Long.parseLong(form.getContextId());
		ICacheService<IContainer> containerCache = (ICacheService<IContainer>) ServiceRegistry
				.getDefault().lookup(CONTAINER_CACHE_SERVICE_NAME);
		IContainer container = containerCache.get(contextId);

		// run these validations only for the date field
		if (!field.getKey().equalsIgnoreCase(DATE_FIELD)) {
			return true;
		}

		// date is a required field, so ensure it is provided.
		if (form.getDate() == null || form.getDate().trim().length() == 0) {
			String errorMsg = "Pitch date is required for a Distribution Schedule.";
			msgs.add(field.getKey(), new ActionMessage(errorMsg, false));
			logger.debug(errorMsg);
			ScheduleFormPopulator.repopulateFormFields(form);
			
			return false;
		}
		
		// check the format if the date was provided
		if (!isValidDate(form.getDate())) {
			String errorMsg = "Schedule pitch date should be a valid date in the correct format("
					+ CommonUtils.getDateFormat() + ").";
			msgs.add(field.getKey(), new ActionMessage(errorMsg, false));
			logger.debug(errorMsg);
			ScheduleFormPopulator.repopulateFormFields(form);
			
			return false;
		}
		
		// ensure the date given is current or in the future
		if (isDateInPast(form.getDate(), CommonUtils.getDateFormat())) {
			String errorMsg = "Schedule pitch date should be current or in the future.";
			msgs.add(field.getKey(), new ActionMessage(errorMsg, false));
			logger.debug(errorMsg);
			ScheduleFormPopulator.repopulateFormFields(form);
			
			return false;
		}

		// if the date is provided and is in the valid format, check if any other
		// schedule exists on that date for this destination.
		Date pitchDate = null;
		
		try {
			pitchDate = CommonUtils.getDate(form.getDate());
			long scheduleId = (form.getScheduleId() == null || form
					.getScheduleId().trim().length() == 0) ? 0 : Long.parseLong(form
					.getScheduleId());

			if (schedulePresent(scheduleId, contextId, pitchDate)) {
				String errorMsg = container.getContainerName()
						+ " already has pitch schedules for "
						+ form.getDate()
						+ ". Only one pitch schedule can be associated with a destination on a particular day.";
				msgs.add(field.getKey(), new ActionMessage(errorMsg, false));
				logger.debug(errorMsg);
				ScheduleFormPopulator.repopulateFormFields(form);
				
				return false;
			}
		} catch (RuntimeException e) {
			// this should not really happen because the format has been validated,
			// before doing this check!
		}

		try {
			validateTitleLicense(form, pitchDate);
		} catch (ValidationException ve) {
			List<ValidationMessage> validationMessages = ve.getValidationMessages();

			for (ValidationMessage message : validationMessages) {
				String key = message.getCode();
				Object[] values = message.getProperties().toArray();
				ActionMessage actionMessage = new ActionMessage(key, values);
				msgs.add("Schedule Validation", actionMessage);
			}

			ScheduleFormPopulator.repopulateFormFields(form);
			
			return false;
		}
		
		ScheduleFormPopulator.repopulateFormFields(form);
		
		return true;
	}
	
	/**
	 * Ensures that titles in the schedule are licensed
	 * 
	 * @param form
	 * @param pitchDate
	 */
	private static void validateTitleLicense(ScheduleForm form, Date pitchDate) {
		if (form.getScheduleId() == null || form.getScheduleId().trim().length() == 0)
			return;

		ServiceRegistry registry = ServiceRegistry.getDefault();
		ISchedulePersistenceService service = registry.lookup(ISchedulePersistenceService.class);

		Schedule schedule = (DistributionSchedule) service.get(Long.parseLong(form.getScheduleId()));
		
		IRightsManager rm = RightsManagerFactory.getRightsManager();
		for (Title title : schedule.getTitles()) {
			if (rm != null && !rm.isLicensed(title, pitchDate)) {
				List<ValidationMessage> messages = new ArrayList<ValidationMessage>();
				List<String> ids = new ArrayList<String>();
				
				ids.add(title.getId().toString());
				messages.add(new ValidationMessage(LICENSE_EXPIRES.toString(), ids));
				throw new ValidationException("Title is not licensed", messages);
			}
		}
	}

	/**
	 * This method determines if a distribution schedule already exists for that contextId and date.
	 * 
	 * @param contextId
	 * @param pitchDate
	 * @return
	 */
	private static boolean schedulePresent(long scheduleId, long contextId, Date pitchDate) {
		IScheduleSearchService searchService = ServiceRegistry.getDefault().lookup(
				IScheduleSearchService.class);
		Collection<DistributionSchedule> pitchSchedules = searchService
				.getPitchSchedulesByPitchDate(contextId, pitchDate);

		// if the user is creating a schedule and schedules already exist, then its
		// invalid.
		if (scheduleId == 0 && pitchSchedules.size() > 0) {
			return true;
		} else if (scheduleId != 0 && pitchSchedules.size() > 0) {
			// The user is trying to set a date on this already existing schedule with
			// the date used by another schedule then its not allowed.
			// The user could be updating this schedule's info (other than pitch date),
			// then its fine.
			Iterator<DistributionSchedule> iter = pitchSchedules.iterator();
			while (iter.hasNext()) {
				if (iter.next().getId() != scheduleId) {
					return true;
				}
				// Strictly speaking, there should be only one entry in this collection.
				break;
			}
		}
		return false;
	}

	/**
	 * Checks if the given date is in the format specified in application properties
	 * 
	 * @param date
	 * 
	 * @return
	 */
	private static boolean isValidDate(String date) {
		try {
			String format = CommonUtils.getDateFormat();
			SimpleDateFormat df = new SimpleDateFormat(format);
			df.setLenient(false);
			df.parse(date);
			if (date.trim().length() != format.length()) {
				return false;
			}
			return true;
		} catch (ParseException e) {
			return false;
		} catch (IllegalArgumentException e) {
			return false;
		} catch (RuntimeException e) {
			return false;
		}
	}

	/**
	 * Checks if the given date (and format) is either current or in the future.
	 * 
	 * @param date
	 *            The date that needs to be verified
	 * @return true if the given date is either current or in the future.
	 */
	private static boolean isDateInPast(String date, String format) {
		try {
			SimpleDateFormat timefomat = new SimpleDateFormat(format);
			timefomat.setLenient(false);
			Date d = timefomat.parse(date);

			Calendar dateCalendar = Calendar.getInstance();
			dateCalendar.setTime(d);
			dateCalendar = resetTime(dateCalendar);

			Calendar currCalendar = Calendar.getInstance();
			currCalendar.setTime(new Date());
			currCalendar = resetTime(currCalendar);

			return dateCalendar.before(currCalendar);
		} catch (ParseException e) {
			// this should not happen as the format should be already verified
			// before this method is called.
			logger.error(
					"Parsing error while determining if the date in current or in the future: ", e);
			return true;
		}
	}

	/**
	 * This method sets the time for this calendar to midnight. Basically removing the time
	 * information from the calendar.
	 * 
	 * @param jobCalendar
	 * @return
	 */
	private static Calendar resetTime(Calendar c) {
		c.set(Calendar.HOUR, 0);
		c.set(Calendar.MINUTE, 0);
		c.set(Calendar.SECOND, 0);
		c.set(Calendar.MILLISECOND, 0);
		c.set(Calendar.AM_PM, Calendar.AM);
		return c;
	}

}
