/*
 * Copyright (C) 2005 - 2011 Jaspersoft Corporation. All rights reserved.
 * http://www.jaspersoft.com.
 *
 * Unless you have purchased  a commercial license agreement from Jaspersoft,
 * the following license terms  apply:
 *
 * This program is free software: you can redistribute it and/or  modify
 * it under the terms of the GNU Affero General Public License  as
 * published by the Free Software Foundation, either version 3 of  the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Affero  General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public  License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */
package com.jaspersoft.jasperserver.api.engine.scheduling;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.InitializingBean;

import com.jaspersoft.jasperserver.api.JSValidationException;
import com.jaspersoft.jasperserver.api.metadata.jasperreports.domain.ReportUnit;
import com.jaspersoft.jasperserver.api.logging.audit.context.AuditContext;
import com.jaspersoft.jasperserver.api.logging.audit.domain.AuditEvent;
import com.jaspersoft.jasperserver.api.common.domain.ExecutionContext;
import com.jaspersoft.jasperserver.api.common.domain.ValidationError;
import com.jaspersoft.jasperserver.api.common.domain.ValidationErrors;
import com.jaspersoft.jasperserver.api.engine.scheduling.domain.ReportJob;
import com.jaspersoft.jasperserver.api.engine.scheduling.domain.ReportJobIdHolder;
import com.jaspersoft.jasperserver.api.engine.scheduling.domain.ReportJobRuntimeInformation;
import com.jaspersoft.jasperserver.api.engine.scheduling.domain.ReportJobSummary;
import com.jaspersoft.jasperserver.api.engine.scheduling.domain.ReportJobTrigger;
import com.jaspersoft.jasperserver.api.engine.scheduling.domain.ReportJobSimpleTrigger;
import com.jaspersoft.jasperserver.api.engine.scheduling.domain.ReportJobCalendarTrigger;
import com.jaspersoft.jasperserver.api.engine.scheduling.service.ReportJobsPersistenceService;
import com.jaspersoft.jasperserver.api.engine.scheduling.service.ReportJobsScheduler;
import com.jaspersoft.jasperserver.api.engine.scheduling.service.ReportSchedulerListener;
import com.jaspersoft.jasperserver.api.engine.scheduling.service.ReportSchedulingService;

/**
 * @author Lucian Chirita (lucianc@users.sourceforge.net)
 * @version $Id: ReportSchedulingFacade.java 19922 2010-12-11 14:59:51Z tmatyashovsky $
 */
public class ReportSchedulingFacade 
	implements ReportSchedulingService, ReportSchedulingInternalService, ReportSchedulerListener, InitializingBean {
	
	private static final Log log = LogFactory.getLog(ReportSchedulingFacade.class);
	
	private ReportJobsPersistenceService persistenceService;
	private ReportJobsInternalService jobsInternalService;
	private ReportJobsScheduler scheduler;
	private ReportJobValidator validator;

    private AuditContext auditContext;
    
    private Map outputKeyMapping;

	/**
	 * @return Returns the outputKeyMapping.
	 */
	public Map getOutputKeyMapping() {
		return outputKeyMapping;
	}

	/**
	 * @param outputKeyMapping The outputKeyMapping to set.
	 */
	public void setOutputKeyMapping(Map outputKeyMapping) {
		this.outputKeyMapping = outputKeyMapping;
	}

	public ReportJobsPersistenceService getPersistenceService() {
		return persistenceService;
	}

	public void setPersistenceService(
			ReportJobsPersistenceService persistenceService) {
		this.persistenceService = persistenceService;
	}

	public ReportJobsScheduler getScheduler() {
		return scheduler;
	}

	public void setScheduler(ReportJobsScheduler scheduler) {
		this.scheduler = scheduler;
	}

	public ReportJobValidator getValidator() {
		return validator;
	}

	public void setValidator(ReportJobValidator validator) {
		this.validator = validator;
	}

	public ReportJobsInternalService getJobsInternalService() {
		return jobsInternalService;
	}

	public void setJobsInternalService(ReportJobsInternalService jobsInternalService) {
		this.jobsInternalService = jobsInternalService;
	}

    public void setAuditContext(AuditContext auditContext) {
        this.auditContext = auditContext;
    }

    public void afterPropertiesSet() throws Exception {
		getScheduler().addReportSchedulerListener(this);
	}

    private String getReportJobFormatsAsString(ReportJob job) {
    	
    	// fix for pluggable output formats
//        Map<Byte, String> formatNamesMap = new HashMap<Byte, String>();
//        formatNamesMap.put(ReportJob.OUTPUT_FORMAT_PDF, "PDF");
//        formatNamesMap.put(ReportJob.OUTPUT_FORMAT_HTML, "HTML");
//        formatNamesMap.put(ReportJob.OUTPUT_FORMAT_XLS, "XLS");
//        formatNamesMap.put(ReportJob.OUTPUT_FORMAT_RTF, "RTF");
//        formatNamesMap.put(ReportJob.OUTPUT_FORMAT_CSV, "CSV");
//        formatNamesMap.put(ReportJob.OUTPUT_FORMAT_ODT, "ODT");
//        formatNamesMap.put(ReportJob.OUTPUT_FORMAT_TXT, "TXT");
//        formatNamesMap.put(ReportJob.OUTPUT_FORMAT_DOCX, "DOCX");
//        formatNamesMap.put(ReportJob.OUTPUT_FORMAT_ODS, "ODS");       

        StringBuilder sb = new StringBuilder();
        for (Object outputFormat: job.getOutputFormats()) {
            if (sb.length() > 0) {
                sb.append(",");
            }
            
         // fix for pluggable output formats
//            sb.append(formatNamesMap.get(Byte.valueOf(outputFormat.toString())));
            sb.append(((String)outputKeyMapping.get(outputFormat.toString())).toUpperCase());
        }

        return sb.toString();
    }

    private String getReportJobNotificationEmailsAsString(ReportJob job) {
        if (job.getMailNotification() == null) {
            return null;
        }

        List emailsList = new ArrayList();
        if (job.getMailNotification().getToAddresses() != null) {
            emailsList.addAll(job.getMailNotification().getToAddresses());
        }
        if (job.getMailNotification().getCcAddresses() != null) {
            emailsList.addAll(job.getMailNotification().getCcAddresses());
        }
        if (job.getMailNotification().getBccAddresses() != null) {
            emailsList.addAll(job.getMailNotification().getBccAddresses());
        }

        StringBuilder sb = new StringBuilder();
        for (Object email: emailsList) {
            if (sb.length() > 0) {
                sb.append(",");
            }
            sb.append(email.toString());
        }

        return sb.toString();
    }

    private String getSetOfBytesAsString(Set set, Map<Byte, String> namesMap) {
        if (set == null || set.isEmpty()) {
            return null;
        }

        StringBuilder sb = new StringBuilder();
        for (Object element: set) {
            if (sb.length() > 0) {
                sb.append(",");
            }
            sb.append(namesMap.get(Byte.valueOf(element.toString())));
        }

        return sb.toString();
    }

    public void createDeleteReportSchedulingEvent() {
        auditContext.doInAuditContext(new AuditContext.AuditContextCallback() {
            public void execute() {
                auditContext.createAuditEvent("deleteReportScheduling");
            }
        });
    }

    public void closeDeleteReportSchedulingEvent() {
        auditContext.doInAuditContext("deleteReportScheduling", new AuditContext.AuditContextCallbackWithEvent() {
            public void execute(AuditEvent auditEvent) {
                auditContext.closeAuditEvent(auditEvent);
            }
        });
    }

    public void addParamsToAuditEvent(final ReportJob job, final String jobType) {
        auditContext.doInAuditContext(jobType, new AuditContext.AuditContextCallbackWithEvent() {
            public void execute(AuditEvent auditEvent) {
                auditEvent.setResourceUri(job.getSource().getReportUnitURI());
                auditContext.setResourceTypeToAuditEvent(ReportUnit.class.getName(), auditEvent);

                auditContext.addPropertyToAuditEvent("jobLabel", job.getLabel(), auditEvent);
                auditContext.addPropertyToAuditEvent("jobDescription", job.getDescription(), auditEvent);
                auditContext.addPropertyToAuditEvent("jobBaseOutputFilename", job.getBaseOutputFilename(), auditEvent);
                auditContext.addPropertyToAuditEvent("jobOutputFormats", getReportJobFormatsAsString(job), auditEvent);
                auditContext.addPropertyToAuditEvent("jobOutputLocale", job.getOutputLocale(), auditEvent);
                auditContext.addPropertyToAuditEvent("jobDestinationFolder", job.getContentRepositoryDestination().getFolderURI(), auditEvent);
                auditContext.addPropertyToAuditEvent("jobNotificationEmails", getReportJobNotificationEmailsAsString(job), auditEvent);

                auditContext.addPropertyToAuditEvent("jobTriggerStartType",
                        job.getTrigger().getStartType() == ReportJobTrigger.START_TYPE_NOW ? "NOW" : "SCHEDULE",
                        auditEvent);

                auditContext.addPropertyToAuditEvent("jobTimezone", job.getTrigger().getTimezone(), auditEvent);
                auditContext.addPropertyToAuditEvent("jobStartDate", job.getTrigger().getStartDate(), auditEvent);
                auditContext.addPropertyToAuditEvent("jobEndDate", job.getTrigger().getEndDate(), auditEvent);

                if (job.getTrigger() instanceof ReportJobSimpleTrigger) {
                    auditContext.addPropertyToAuditEvent("jobTriggerType", "SIMPLE_TRIGGER", auditEvent);
                    ReportJobSimpleTrigger trigger = (ReportJobSimpleTrigger)job.getTrigger();
                    auditContext.addPropertyToAuditEvent("jobSimpleTriggerOccurenceCount",
                            trigger.getOccurrenceCount(), auditEvent);
                    auditContext.addPropertyToAuditEvent("jobSimpleTriggerRecurrenceInterval",
                            trigger.getRecurrenceInterval(), auditEvent);

                    Map<Byte, String> intervalUnitNamesMap = new HashMap<Byte, String>();
                    intervalUnitNamesMap.put(ReportJobSimpleTrigger.INTERVAL_MINUTE, "MINUTE");
                    intervalUnitNamesMap.put(ReportJobSimpleTrigger.INTERVAL_HOUR, "HOUR");
                    intervalUnitNamesMap.put(ReportJobSimpleTrigger.INTERVAL_DAY, "DAY");
                    intervalUnitNamesMap.put(ReportJobSimpleTrigger.INTERVAL_WEEK, "WEEK");
                    String intervalUnitName = intervalUnitNamesMap.get(trigger.getRecurrenceIntervalUnit());
                    auditContext.addPropertyToAuditEvent("jobSimpleTriggerRecurrenceIntervalUnit",
                            intervalUnitName, auditEvent);
                } else {
                    ReportJobCalendarTrigger trigger = (ReportJobCalendarTrigger)job.getTrigger();
                    auditContext.addPropertyToAuditEvent("jobTriggerType", "CALENDAR_TRIGGER", auditEvent);
                    auditContext.addPropertyToAuditEvent("jobCalendarTriggerMinutes", trigger.getMinutes(), auditEvent);
                    auditContext.addPropertyToAuditEvent("jobCalendarTriggerHours", trigger.getHours(), auditEvent);

                    Map<Byte, String> daysTypeNamesMap = new HashMap<Byte, String>();
                    daysTypeNamesMap.put(ReportJobCalendarTrigger.DAYS_TYPE_ALL, "ALL");
                    daysTypeNamesMap.put(ReportJobCalendarTrigger.DAYS_TYPE_WEEK, "WEEK");
                    daysTypeNamesMap.put(ReportJobCalendarTrigger.DAYS_TYPE_MONTH, "MONTH");

                    auditContext.addPropertyToAuditEvent("jobCalendarTriggerDaysType",
                            daysTypeNamesMap.get(trigger.getDaysType()), auditEvent);

                    Map<Byte, String> weekDaysMap = new HashMap<Byte, String>();
                    weekDaysMap.put((byte)2, "mon");
                    weekDaysMap.put((byte)3, "tue");
                    weekDaysMap.put((byte)4, "wen");
                    weekDaysMap.put((byte)5, "thu");
                    weekDaysMap.put((byte)6, "fri");
                    weekDaysMap.put((byte)7, "sat");
                    weekDaysMap.put((byte)1, "sun");
                    auditContext.addPropertyToAuditEvent("jobCalendarTriggerWeekDays",
                            getSetOfBytesAsString(trigger.getWeekDays(), weekDaysMap), auditEvent);
                    auditContext.addPropertyToAuditEvent("jobCalendarTriggerMonthDays", trigger.getMonthDays(), auditEvent);

                    Map<Byte, String> monthsMap = new HashMap<Byte, String>();
                    monthsMap.put((byte)1, "jan");
                    monthsMap.put((byte)2, "feb");
                    monthsMap.put((byte)3, "mar");
                    monthsMap.put((byte)4, "apr");
                    monthsMap.put((byte)5, "may");
                    monthsMap.put((byte)6, "jun");
                    monthsMap.put((byte)7, "jul");
                    monthsMap.put((byte)8, "aug");
                    monthsMap.put((byte)9, "sep");
                    monthsMap.put((byte)10, "oct");
                    monthsMap.put((byte)11, "nov");
                    monthsMap.put((byte)12, "dec");
                    auditContext.addPropertyToAuditEvent("jobCalendarTriggerMonths",
                            getSetOfBytesAsString(trigger.getMonths(), monthsMap), auditEvent);
                }

                if (job.getSource().getParametersMap() != null) {
                    for (Object key: job.getSource().getParametersMap().keySet()) {
                        String stringKey = key.toString();
                        Object value = job.getSource().getParametersMap().get(key);
                        String stringValue = value != null ?  value.toString() : "";
                        String param = stringKey + "=" + stringValue;
                        auditContext.addPropertyToAuditEvent("jobParam", param, auditEvent);
                    }
                }
            }
        });
    }

	public ReportJob scheduleJob(ExecutionContext context, ReportJob job) {
		validate(context, job);
        addParamsToAuditEvent(job, "scheduleReport");
		ReportJob savedJob = persistenceService.saveJob(context, job);
		scheduler.scheduleJob(context, savedJob);
		return savedJob;
	}

	protected void validate(ExecutionContext context, ReportJob job) {
		ValidationErrors errors = validator.validateJob(context, job);
		if (errors.isError()) {
			throw new JSValidationException(errors);
		}
	}

	public List getScheduledJobs(ExecutionContext context, String reportUnitURI) {
		List jobs = persistenceService.listJobs(context, reportUnitURI);
		setSummaryRuntimeInformation(context, jobs);
		return jobs;
	}

	public List getScheduledJobs(ExecutionContext context) {
		List jobs = persistenceService.listJobs(context);
		setSummaryRuntimeInformation(context, jobs);
		return jobs;
	}

	protected void setSummaryRuntimeInformation(ExecutionContext context, List jobs) {
		if (jobs != null && !jobs.isEmpty()) {
			long[] jobIds = new long[jobs.size()];
			int idx = 0;
			for (Iterator it = jobs.iterator(); it.hasNext(); ++idx) {
				ReportJobSummary job = (ReportJobSummary) it.next();
				jobIds[idx] = job.getId();
			}
			
			ReportJobRuntimeInformation[] runtimeInfos = scheduler.getJobsRuntimeInformation(context, jobIds);
			
			idx = 0;
			for (Iterator it = jobs.iterator(); it.hasNext(); ++idx) {
				ReportJobSummary job = (ReportJobSummary) it.next();
				job.setRuntimeInformation(runtimeInfos[idx]);
			}			
		}
	}

	public void removeScheduledJob(ExecutionContext context, long jobId) {
        createDeleteReportSchedulingEvent();
		deleteJob(context, jobId);
        closeDeleteReportSchedulingEvent();
	}

	public void removeScheduledJobs(ExecutionContext context, long[] jobIds) {
		for (int i = 0; i < jobIds.length; i++) {
            createDeleteReportSchedulingEvent();
			long jobId = jobIds[i];
			deleteJob(context, jobId);
            closeDeleteReportSchedulingEvent();
		}
	}

	public void removeReportUnitJobs(String reportUnitURI) {
		long[] deletedJobIds = getJobsInternalService().deleteReportUnitJobs(reportUnitURI);
		unscheduleJobs(deletedJobIds);		
	}

	protected void unscheduleJobs(long[] deletedJobIds) {
		if (deletedJobIds != null && deletedJobIds.length > 0) {
			for (int i = 0; i < deletedJobIds.length; i++) {
				long jobId = deletedJobIds[i];
				scheduler.removeScheduledJob(null, jobId);				
			}
		}
	}

	protected void deleteJob(ExecutionContext context, long jobId) {
        addParamsToAuditEvent(getScheduledJob(context, jobId), "deleteReportScheduling");
		scheduler.removeScheduledJob(context, jobId);
		persistenceService.deleteJob(context, new ReportJobIdHolder(jobId));
	}

	public ReportJob getScheduledJob(ExecutionContext context, long jobId) {
		return persistenceService.loadJob(context, new ReportJobIdHolder(jobId));
	}

	public void reportJobFinalized(long jobId) {
		if (log.isDebugEnabled()) {
			log.debug("Job " + jobId + " finalized, deleting data");
		}

		getJobsInternalService().deleteJob(jobId);
	}

	public void updateScheduledJob(ExecutionContext context, ReportJob job) {
		validate(context, job);

		ReportJobTrigger origTrigger = job.getTrigger();
		long origTriggerId = origTrigger.getId();
		int origTriggerVersion = origTrigger.getVersion();

        addParamsToAuditEvent(job, "updateReportScheduling");

		ReportJob savedJob = persistenceService.updateJob(context, job);
		ReportJobTrigger updatedTrigger = savedJob.getTrigger();

		if (updatedTrigger.getId() != origTriggerId || updatedTrigger.getVersion() != origTriggerVersion) {
			scheduler.rescheduleJob(context, savedJob);
		} else {
			if (log.isDebugEnabled()) {
				log.debug("Trigger attributes not changed for job " + job.getId() + ", the job will not be rescheduled");
			}
		}
	}

	public ValidationErrors validateJob(ExecutionContext context, ReportJob job) {
		ValidationErrors errors = validator.validateJob(context, job);
		if (!hasTriggerErrors(errors)) {
			scheduler.validate(job, errors);
		}
		return errors;
	}

	protected boolean hasTriggerErrors(ValidationErrors errors) {
		boolean triggerError = false;
		for(Iterator it = errors.getErrors().iterator(); !triggerError && it.hasNext(); ) {
			ValidationError error = (ValidationError) it.next();
			String field = error.getField();
			if (field != null && (field.equals("trigger") || field.startsWith("trigger."))) {
				triggerError = true;
			}
		}
		return triggerError;
	}

	public ReportJob saveJob(ExecutionContext context, ReportJob job) {
		validateSaveJob(context, job);
		ReportJob savedJob = jobsInternalService.saveJob(context, job, false);
		scheduler.scheduleJob(context, savedJob);
		return savedJob;
	}

	protected void validateSaveJob(ExecutionContext context, ReportJob job) {
		ValidationErrors errors = validator.validateJob(context, job);
		
		// allow jobs with past start dates to be saved
		errors.removeError("error.before.current.date", "trigger.startDate");
		
		if (errors.isError()) {
			throw new JSValidationException(errors);
		}
	}

	public void updateReportUnitURI(String oldURI, String newURI) {
		getJobsInternalService().updateReportUnitURI(oldURI, newURI);
	}
}
