/*
 * Copyright 2002-2008 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */


using System;
using System.Collections;

using Common.Logging;

using Quartz;
using Quartz.Xml;

using Spring.Collections;
using Spring.Context;
using Spring.Core.IO;
using Spring.Transaction;
using Spring.Transaction.Support;

namespace Spring.Scheduling.Quartz
{
    /// <summary>
    /// Common base class for accessing a Quartz Scheduler, i.e. for registering jobs,
    /// triggers and listeners on a <see cref="IScheduler" /> instance.
    /// </summary>
    /// <remarks>
    /// For concrete usage, check out the <see cref="SchedulerFactoryObject" /> and
    /// <see cref="SchedulerAccessorObject" /> classes.
    ///</remarks>
    /// <author>Juergen Hoeller</author>
    /// <author>Marko Lahma (.NET)</author>
    public abstract class SchedulerAccessor : IResourceLoaderAware
    {
        /// <summary>
        /// Logger instance.
        /// </summary>
        protected readonly ILog logger;
        private bool overwriteExistingJobs;

        private string[] jobSchedulingDataLocations;

        private IList jobDetails;
        private IDictionary calendars;
        private IList triggers;

        private ISchedulerListener[] schedulerListeners;
        private IJobListener[] globalJobListeners;
        private IJobListener[] jobListeners;
        private ITriggerListener[] globalTriggerListeners;
        private ITriggerListener[] triggerListeners;

        private IPlatformTransactionManager transactionManager;
        /// <summary>
        /// Resource loader instance for sub-classes
        /// </summary>
        protected IResourceLoader resourceLoader;

        /// <summary>
        /// Initializes a new instance of the <see cref="SchedulerAccessor"/> class.
        /// </summary>
        protected SchedulerAccessor()
        {
            logger = LogManager.GetLogger(GetType());
        }

        /// <summary>
        /// Set whether any jobs defined on this SchedulerFactoryObject should overwrite
        /// existing job definitions. Default is "false", to not overwrite already
        /// registered jobs that have been read in from a persistent job store.
        /// </summary>
        public virtual bool OverwriteExistingJobs
        {
            set { overwriteExistingJobs = value; }
        }


        /// <summary>
        /// Set the locations of Quartz job definition XML files that follow the
        /// "job_scheduling_data_1_5" XSD. Can be specified to automatically
        /// register jobs that are defined in such files, possibly in addition
        /// to jobs defined directly on this SchedulerFactoryObject.
        /// </summary>
        /// <seealso cref="JobSchedulingDataProcessor" />
        public virtual string[] JobSchedulingDataLocations
        {
            set { jobSchedulingDataLocations = value; }
        }

        /// <summary> 
        /// Set the location of a Quartz job definition XML file that follows the
        /// "job_scheduling_data" XSD. Can be specified to automatically
        /// register jobs that are defined in such a file, possibly in addition
        /// to jobs defined directly on this SchedulerFactoryObject.
        /// </summary>
        /// <seealso cref="ResourceJobSchedulingDataProcessor" />
        /// <seealso cref="JobSchedulingDataProcessor" />
        public virtual string JobSchedulingDataLocation
        {
            set { jobSchedulingDataLocations = new string[] {value}; }
        }

        /// <summary>
        /// Register a list of JobDetail objects with the Scheduler that
        /// this FactoryObject creates, to be referenced by Triggers.
        /// This is not necessary when a Trigger determines the JobDetail
        /// itself: In this case, the JobDetail will be implicitly registered
        /// in combination with the Trigger.
        /// </summary>
        /// <seealso cref="Triggers" />
        /// <seealso cref="JobDetail" />
        /// <seealso cref="JobDetailObject" />
        /// <seealso cref="IJobDetailAwareTrigger" />
        /// <seealso cref="Trigger.JobName" />
        public virtual JobDetail[] JobDetails
        {
            set
            {
                // Use modifiable ArrayList here, to allow for further adding of
                // JobDetail objects during autodetection of JobDetailAwareTriggers.
                jobDetails = new ArrayList(value);
            }
        }

        /// <summary>
        /// Register a list of Quartz ICalendar objects with the Scheduler
        /// that this FactoryObject creates, to be referenced by Triggers.
        /// </summary>
        /// <value>Map with calendar names as keys as Calendar objects as values</value> 
        /// <seealso cref="ICalendar" />
        /// <seealso cref="Trigger.CalendarName" />
        public virtual IDictionary Calendars
        {
            set { calendars = value; }
        }

        /// <summary>
        /// Register a list of Trigger objects with the Scheduler that
        /// this FactoryObject creates.
        /// </summary>
        /// <remarks>
        /// If the Trigger determines the corresponding JobDetail itself,
        /// the job will be automatically registered with the Scheduler.
        /// Else, the respective JobDetail needs to be registered via the
        /// "jobDetails" property of this FactoryObject.
        /// </remarks>
        /// <seealso cref="JobDetails" />
        /// <seealso cref="JobDetail" />
        /// <seealso cref="IJobDetailAwareTrigger" />
        /// <seealso cref="CronTriggerObject" />
        /// <seealso cref="SimpleTriggerObject" />
        public virtual Trigger[] Triggers
        {
            set { triggers = new ArrayList(value); }
        }

        /// <summary>
        /// Specify Quartz SchedulerListeners to be registered with the Scheduler.
        /// </summary>
        public virtual ISchedulerListener[] SchedulerListeners
        {
            set { schedulerListeners = value; }
        }

        /// <summary>
        /// Specify global Quartz JobListeners to be registered with the Scheduler.
        /// Such JobListeners will apply to all Jobs in the Scheduler.
        /// </summary>
        public virtual IJobListener[] GlobalJobListeners
        {
            set { globalJobListeners = value; }
        }

        /// <summary>
        /// Specify named Quartz JobListeners to be registered with the Scheduler.
        /// Such JobListeners will only apply to Jobs that explicitly activate
        /// them via their name.
        /// </summary>
        /// <seealso cref="IJobListener.Name" />
        /// <seealso cref="JobDetail.AddJobListener" />
        /// <seealso cref="JobDetail.JobListenerNames" />
        public virtual IJobListener[] JobListeners
        {
            set { jobListeners = value; }
        }


        /// <summary>
        /// Specify global Quartz TriggerListeners to be registered with the Scheduler.
        /// Such TriggerListeners will apply to all Triggers in the Scheduler.
        /// </summary>
        public virtual ITriggerListener[] GlobalTriggerListeners
        {
            set { globalTriggerListeners = value; }
        }


        /// <summary>
        /// Specify named Quartz TriggerListeners to be registered with the Scheduler.
        /// Such TriggerListeners will only apply to Triggers that explicitly activate
        /// them via their name.
        /// </summary>
        /// <seealso cref="ITriggerListener.Name" />
        /// <seealso cref="Trigger.AddTriggerListener" />
        /// <seealso cref="Trigger.TriggerListenerNames" />
        public virtual ITriggerListener[] TriggerListeners
        {
            set { triggerListeners = value; }
        }


        /// <summary>
        /// Set the transaction manager to be used for registering jobs and triggers
        /// that are defined by this SchedulerFactoryObject. Default is none; setting
        /// this only makes sense when specifying a DataSource for the Scheduler.
        /// </summary>
        public virtual IPlatformTransactionManager TransactionManager
        {
            set { transactionManager = value; }
        }

        /// <summary>
        /// Sets the <see cref="Spring.Core.IO.IResourceLoader"/>
        /// that this object runs in.
        /// </summary>
        /// <value></value>
        /// <remarks>
        /// Invoked <b>after</b> population of normal objects properties but
        /// before an init callback such as
        /// <see cref="Spring.Objects.Factory.IInitializingObject"/>'s
        /// <see cref="Spring.Objects.Factory.IInitializingObject.AfterPropertiesSet()"/>
        /// or a custom init-method. Invoked <b>before</b> setting
        /// <see cref="Spring.Context.IApplicationContextAware"/>'s
        /// <see cref="Spring.Context.IApplicationContextAware.ApplicationContext"/>
        /// property.
        /// </remarks>
        public virtual IResourceLoader ResourceLoader
        {
            set { resourceLoader = value; }
        }

        /// <summary>
        /// Register jobs and triggers (within a transaction, if possible).
        /// </summary>
        protected virtual void RegisterJobsAndTriggers()
        {
            ITransactionStatus transactionStatus = null;
            if (transactionManager != null)
            {
                transactionStatus = transactionManager.GetTransaction(new DefaultTransactionDefinition());
            }
            try
            {
                if (jobSchedulingDataLocations != null)
                {
                    JobSchedulingDataProcessor dataProcessor = new JobSchedulingDataProcessor(true, true);
                    for (int i = 0; i < this.jobSchedulingDataLocations.Length; i++)
                    {
                        dataProcessor.ProcessFileAndScheduleJobs(
                            jobSchedulingDataLocations[i], GetScheduler(), overwriteExistingJobs);
                    }
                }

                // Register JobDetails.
                if (jobDetails != null)
                {
                    foreach (JobDetail jobDetail in jobDetails)
                    {
                        AddJobToScheduler(jobDetail);
                    }
                }
                else
                {
                    // Create empty list for easier checks when registering triggers.
                    jobDetails = new LinkedList();
                }

                // Register Calendars.
                if (calendars != null)
                {
                    foreach (DictionaryEntry entry in calendars)
                    {
                        string calendarName = (string) entry.Key;
                        ICalendar calendar = (ICalendar) entry.Value;
                        GetScheduler().AddCalendar(calendarName, calendar, true, true);
                    }
                }

                // Register Triggers.
                if (triggers != null)
                {
                    foreach (Trigger trigger in triggers)
                    {
                        AddTriggerToScheduler(trigger);
                    }
                }
            }

            catch (Exception ex)
            {
                if (transactionStatus != null)
                {
                    try
                    {
                        transactionManager.Rollback(transactionStatus);
                    }
                    catch (TransactionException)
                    {
                        logger.Error("Job registration exception overridden by rollback exception", ex);
                        throw;
                    }
                }
                if (ex is SchedulerException)
                {
                    throw;
                }
                throw new SchedulerException("Registration of jobs and triggers failed: " + ex.Message);
            }

            if (transactionStatus != null)
            {
                transactionManager.Commit(transactionStatus);
            }
        }

        /// <summary>
        /// Add the given job to the Scheduler, if it doesn't already exist.
        /// Overwrites the job in any case if "overwriteExistingJobs" is set.
        /// </summary>
        /// <param name="jobDetail">the job to add</param>
        /// <returns><code>true</code> if the job was actually added, <code>false</code> if it already existed before</returns>
        private bool AddJobToScheduler(JobDetail jobDetail)
        {
            if (overwriteExistingJobs ||
                GetScheduler().GetJobDetail(jobDetail.Name, jobDetail.Group) == null)
            {
                GetScheduler().AddJob(jobDetail, true);
                return true;
            }
            else
            {
                return false;
            }
        }

        /// <summary>
        /// Add the given trigger to the Scheduler, if it doesn't already exist.
        /// Overwrites the trigger in any case if "overwriteExistingJobs" is set.
        /// </summary>
        /// <param name="trigger">the trigger to add</param>
        /// <returns><code>true</code> if the trigger was actually added, <code>false</code> if it already existed before</returns>
        private bool AddTriggerToScheduler(Trigger trigger)
        {
            bool triggerExists = (GetScheduler().GetTrigger(trigger.Name, trigger.Group) != null);
            if (!triggerExists || this.overwriteExistingJobs)
            {
                // Check if the Trigger is aware of an associated JobDetail.
                if (trigger is IJobDetailAwareTrigger)
                {
                    JobDetail jobDetail = ((IJobDetailAwareTrigger) trigger).JobDetail;
                    // Automatically register the JobDetail too.
                    if (!jobDetails.Contains(jobDetail) && AddJobToScheduler(jobDetail))
                    {
                        jobDetails.Add(jobDetail);
                    }
                }
                if (!triggerExists)
                {
                    try
                    {
                        GetScheduler().ScheduleJob(trigger);
                    }
                    catch (ObjectAlreadyExistsException ex)
                    {
                        if (logger.IsDebugEnabled)
                        {
                            logger.Debug(
                                "Unexpectedly found existing trigger, assumably due to cluster race condition: " +
                                ex.Message + " - can safely be ignored");
                        }
                        if (overwriteExistingJobs)
                        {
                            GetScheduler().RescheduleJob(trigger.Name, trigger.Group, trigger);
                        }
                    }
                }
                else
                {
                    GetScheduler().RescheduleJob(trigger.Name, trigger.Group, trigger);
                }
                return true;
            }
            else
            {
                return false;
            }
        }


        /// <summary>
        /// Register all specified listeners with the Scheduler.
        /// </summary>
        protected virtual void RegisterListeners()
        {
            if (schedulerListeners != null)
            {
                for (int i = 0; i < schedulerListeners.Length; i++)
                {
                    GetScheduler().AddSchedulerListener(schedulerListeners[i]);
                }
            }
            if (globalJobListeners != null)
            {
                for (int i = 0; i < globalJobListeners.Length; i++)
                {
                    GetScheduler().AddGlobalJobListener(globalJobListeners[i]);
                }
            }
            if (jobListeners != null)
            {
                for (int i = 0; i < jobListeners.Length; i++)
                {
                    GetScheduler().AddJobListener(jobListeners[i]);
                }
            }
            if (globalTriggerListeners != null)
            {
                for (int i = 0; i < globalTriggerListeners.Length; i++)
                {
                    GetScheduler().AddGlobalTriggerListener(globalTriggerListeners[i]);
                }
            }
            if (triggerListeners != null)
            {
                for (int i = 0; i < triggerListeners.Length; i++)
                {
                    GetScheduler().AddTriggerListener(triggerListeners[i]);
                }
            }
        }

        /// <summary>
        /// Template method that determines the Scheduler to operate on.
        /// To be implemented by subclasses.
        /// </summary>
        /// <returns></returns>
        protected abstract IScheduler GetScheduler();
    }
}