/* ======================================================================
The Bodington System Software License, Version 1.0
  
Copyright (c) 2001 The University of Leeds.  All rights reserved.
  
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:

1.  Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.

2.  Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.

3.  The end-user documentation included with the redistribution, if any,
must include the following acknowledgement:  "This product includes
software developed by the University of Leeds
(http://www.bodington.org/)."  Alternately, this acknowledgement may
appear in the software itself, if and wherever such third-party
acknowledgements normally appear.

4.  The names "Bodington", "Nathan Bodington", "Bodington System",
"Bodington Open Source Project", and "The University of Leeds" must not be
used to endorse or promote products derived from this software without
prior written permission. For written permission, please contact
d.gardner@leeds.ac.uk.

5.  The name "Bodington" may not appear in the name of products derived
from this software without prior written permission of the University of
Leeds.

THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
WARRANTIES, INCLUDING, BUT NOT LIMITED TO,  TITLE,  THE IMPLIED WARRANTIES 
OF QUALITY  AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO 
EVENT SHALL THE UNIVERSITY OF LEEDS OR ITS CONTRIBUTORS BE LIABLE FOR 
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE 
GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 
STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 
ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
POSSIBILITY OF SUCH DAMAGE.
=========================================================

This software was originally created by the University of Leeds and may contain voluntary 
contributions from others.  For more information on the Bodington Open Source Project, please 
see http://bodington.org/

====================================================================== */

package org.bodington.server;

import org.bodington.sqldatabase.*;
import org.bodington.database.*;

import java.util.Enumeration;
import java.util.Calendar;
import java.text.*;


/**
 * Class used to store a job which should be run some time in the 
 * furture.
 * @see org.bodington.server.JobSession
 * @see org.bodington.server.JobScheduler
 * @see org.bodington.server.JobResult
 * @author Jon Maber
 *
 */
public class Job extends org.bodington.sqldatabase.SqlPersistentObject
    {
    public static final int STATE_SUSPENDED     = 0;
    public static final int STATE_WAITING       = 1;
    public static final int STATE_OVERDUE       = 2;
    public static final int STATE_EXECUTING     = 3;
    public static final int STATE_COMPLETED     = 4;
    public static final int STATE_CANCELLED     = 5;
    public static final int STATE_FAILED        = 6;
        
    public static final int REPEAT_TYPE_NEVER		=0;
    public static final int REPEAT_TYPE_ONCE		=1;
    public static final int REPEAT_TYPE_DAILY		=2;
    public static final int REPEAT_TYPE_WEEKLY		=3;
    public static final int REPEAT_TYPE_MONTHLY_WEEK	=4;
    public static final int REPEAT_TYPE_MONTHLY_DAY	=5;
    
    public static final int REPEAT_SPEC_M_MASK_DAYOFWEEK    = 0x00000007;  //monday = 0, sunday = 6, not used = 7
    public static final int REPEAT_SPEC_M_MASK_WEEKOFMONTH  = 0x00000038;  //last week=0, first week = 1 not used = 7
    
    public static final int REPEAT_SPEC_M_MASK_DAYOFMONTH   = 0x00001f00;  //1st of month=1
    
    public static final int REPEAT_SPEC_M_MASK_MONTHOFYEAR  = 0x0fff0000;  //jan= bit 0, dec = bit 11
    
    public static final int REPEAT_SPEC_M_SHIFT_DAYOFWEEK     = 0;
    public static final int REPEAT_SPEC_M_SHIFT_WEEKOFMONTH   = 3;
    public static final int REPEAT_SPEC_M_SHIFT_DAYOFMONTH    = 8;
    public static final int REPEAT_SPEC_M_SHIFT_MONTHOFYEAR   = 16;
    
    PrimaryKey job_id;
    PrimaryKey user_id;
    PrimaryKey resource_id;
    String session_name;
    String method_name;
    String parameter;
    int state;
    int options;
    
    java.sql.Timestamp submission_time;
    java.sql.Timestamp from_time;
    java.sql.Timestamp to_time;
    int repeat_type;
    int repeat_spec;
    int repeat_period;					// in seconds, if less than or equal to 0 there is no repeat
    java.sql.Timestamp execution_time;

        
    public static Job findJob( PrimaryKey key )
	throws BuildingServerException
	{
	return (Job)findPersistentObject( key, "org.bodington.server.Job" );
	}

    public static Job findJob( String where )
	throws BuildingServerException
	{
	return (Job)findPersistentObject( where, "org.bodington.server.Job" );
	}

    public static Enumeration findJobs( String where )
	throws BuildingServerException
	{
	return findPersistentObjects( where, "org.bodington.server.Job" );
	}

    public static Enumeration findJobs( String where, String order )
	throws BuildingServerException
	{
	return findPersistentObjects( where, order, "org.bodington.server.Job" );
	}

    public PrimaryKey getPrimaryKey()
        {
        return getJobId();
        }

    public void setPrimaryKey(PrimaryKey key)
        {
        setJobId( key );
        }

    public PrimaryKey getJobId()
        {
        return job_id;
        }
         
    public void setJobId( PrimaryKey key )
        {
        job_id = key;
        }
        
    
    public PrimaryKey getUserId()
        {
        return user_id;
        }
         
    public void setUserId( PrimaryKey key )
        {
        user_id = key;
        setUnsaved();
        }
        
    
    public PrimaryKey getResourceId()
        {
        return resource_id;
        }
         
    public void setResourceId( PrimaryKey key )
        {
        resource_id = key;
        setUnsaved();
        }
        
    
    public String getSessionName()
        {
        return session_name;
        }
         
    public void setSessionName( String n )
        {
        session_name = n;
        setUnsaved();
        }
        

    public String getMethodName()
        {
        return method_name;
        }
         
    public void setMethodName( String n )
        {
        method_name = n;
        setUnsaved();
        }
        
    public String getParameter()
        {
        return parameter;
        }
         
    public void setParameter( String n )
        {
        parameter = n;
        setUnsaved();
        }
        
    
    
    public int getState()
        {
        return state;
        }
         
    public void setState( int n )
        {
        state = n;
        setUnsaved();
        }
        
    public int getOptions()
        {
        return options;
        }
         
    public void setOptions( int n )
        {
        options = n;
        setUnsaved();
        }
        
        
        
    public java.sql.Timestamp getSubmissionTime()
	{
	return submission_time;
	}
    public void setSubmissionTime( java.sql.Timestamp t )
	{
	submission_time = t;
	setUnsaved();
	}


    public java.sql.Timestamp getFromTime()
	{
	return from_time;
	}
    public void setFromTime( java.sql.Timestamp t )
	{
	from_time = t;
	setUnsaved();
	}


    public java.sql.Timestamp getToTime()
	{
	return to_time;
	}
    public void setToTime( java.sql.Timestamp t )
	{
	to_time = t;
	setUnsaved();
	}


    public int getRepeatType()
        {
        return repeat_type;
        }
         
    public void setRepeatType( int n )
        {
        repeat_type = n;
        setUnsaved();
        }
        
        
    public int getRepeatPeriod()
        {
        return repeat_period;
        }
         
    public void setRepeatPeriod( int n )
        {
        repeat_period = n;
        setUnsaved();
        }
        
        
    public int getRepeatSpec()
        {
        return repeat_spec;
        }
         
    public void setRepeatSpec( int n )
        {
        repeat_spec = n;
        setUnsaved();
        }
    
    public boolean isRepeatSpecDayOfWeek( int d )
	{
	if ( repeat_type != Job.REPEAT_TYPE_WEEKLY )
	    throw new IndexOutOfBoundsException( "Can only specify days of week in weekly schedule." );
	if ( d < Calendar.SUNDAY || d > Calendar.SATURDAY )
	    throw new IndexOutOfBoundsException( "Invalid day of week index." );
	return (repeat_spec & (1 << (d-Calendar.SUNDAY))) != 0;
	}
	
    public void setRepeatSpecDayOfWeek( int d, boolean b )
	{
	if ( d < Calendar.SUNDAY || d > Calendar.SATURDAY )
	    throw new IndexOutOfBoundsException( "Invalid day of week index." );

	repeat_type = Job.REPEAT_TYPE_WEEKLY;
	
	
	int mask;
	if ( b )
	    {
	    mask = 1 << (d-Calendar.SUNDAY);
	    repeat_spec = repeat_spec | mask;
	    }
	else
	    {
	    mask = 0x7f - (1 << (d-Calendar.SUNDAY));
	    repeat_spec = repeat_spec & mask;
	    }
	}
	
    public boolean isRepeatSpecWeekOfMonth( int w, int d )
	{
	if ( repeat_type != Job.REPEAT_TYPE_MONTHLY_WEEK )
	    throw new IndexOutOfBoundsException( "Can only specify week in monthly (+week number) schedule." );
	if ( w<0 || w>4 )
	    throw new IndexOutOfBoundsException( "Invalid week index, must be between 0 and 4 inclusive." );
	if ( d<0 || d>6 )
	    throw new IndexOutOfBoundsException( "Invalid day of week index, must be between 0 and 6 inclusive." );

	int n = repeat_spec & (REPEAT_SPEC_M_MASK_WEEKOFMONTH | REPEAT_SPEC_M_MASK_DAYOFWEEK);
	int m = (w << Job.REPEAT_SPEC_M_SHIFT_WEEKOFMONTH) | (d << Job.REPEAT_SPEC_M_SHIFT_DAYOFWEEK);

	return n == m;
	}
	
    public void setRepeatSpecWeekOfMonth( int w, int d, boolean b )
	{
	if ( w<0 || w>4 )
	    throw new IndexOutOfBoundsException( "Invalid week index, must be between 0 and 4 inclusive." );
	if ( d<0 || d>6 )
	    throw new IndexOutOfBoundsException( "Invalid day of week index, must be between 0 and 6 inclusive." );

	repeat_type = Job.REPEAT_TYPE_MONTHLY_WEEK;
	repeat_spec = (w << Job.REPEAT_SPEC_M_SHIFT_WEEKOFMONTH) | (d << Job.REPEAT_SPEC_M_SHIFT_DAYOFWEEK);
	}
	
    public int getRepeatSpecDayOfMonth()
	{
	if ( repeat_type != Job.REPEAT_TYPE_MONTHLY_DAY )
	    throw new IndexOutOfBoundsException( "Can only get day of month for monthly (+date) schedule." );

	return (repeat_spec & REPEAT_SPEC_M_MASK_DAYOFMONTH) >> REPEAT_SPEC_M_SHIFT_DAYOFMONTH;
	}
	
    public void setRepeatSpecDayOfMonth( int d )
	{
	if ( d<1 || d>31 )
	    throw new IndexOutOfBoundsException( "Invalid day of month index, must be between 1 and 31 inclusive." );

	repeat_type = Job.REPEAT_TYPE_MONTHLY_DAY;
	repeat_spec = (d << Job.REPEAT_SPEC_M_SHIFT_DAYOFWEEK);
	}
	

    public boolean isRepeatSpecMonthOfYear( int m )
	{
	if ( repeat_type != Job.REPEAT_TYPE_MONTHLY_WEEK && repeat_type != Job.REPEAT_TYPE_MONTHLY_DAY )
	    throw new IndexOutOfBoundsException( "Can only specify month in monthly schedule." );
	if ( m < Calendar.JANUARY || m > Calendar.DECEMBER )
	    throw new IndexOutOfBoundsException( "Invalid month index." );


	return (repeat_spec & ((1 << m) << REPEAT_SPEC_M_SHIFT_MONTHOFYEAR)) != 0;
	}

    public void setRepeatSpecMonthOfYear( int m, boolean b )
	{
	if ( repeat_type != Job.REPEAT_TYPE_MONTHLY_WEEK && repeat_type != Job.REPEAT_TYPE_MONTHLY_DAY )
	    throw new IndexOutOfBoundsException( "Can only specify month in monthly schedule." );
	if ( m < Calendar.JANUARY || m > Calendar.DECEMBER )
	    throw new IndexOutOfBoundsException( "Invalid month index, must be between 1 and 12 inclusive." );

	int mask;
	if ( b )
	    {
	    mask = (1 << m) << REPEAT_SPEC_M_SHIFT_MONTHOFYEAR;
	    repeat_spec = repeat_spec | mask;
	    }
	else
	    {
	    mask = (0xfff - (1 << m)) << REPEAT_SPEC_M_SHIFT_MONTHOFYEAR;
	    repeat_spec = repeat_spec & mask;
	    }
	}
	
    public java.sql.Timestamp getExecutionTime()
	{
	return execution_time;
	}
    public void setExecutionTime( java.sql.Timestamp t )
	{
	execution_time = t;
	setUnsaved();
	}


    public void setFirstExecutionTime()
	{
	Calendar now = Calendar.getInstance();
	now.setTime( from_time );
	setFirstExecutionTimeAfter( now );
	}
	
    public void setNextExecutionTime()
	{
	setFirstExecutionTimeAfter( Calendar.getInstance() );
	}
	
    private void setFirstExecutionTimeAfter( Calendar now )
	{
	boolean in_future = false;
	Calendar execute = Calendar.getInstance();
	Calendar to=null;
	execute.setTime( from_time );
	if ( to_time != null )
	    {
	    to = Calendar.getInstance();
	    to.setTime( to_time );
	    }
	int item=0;  //0th day in weekly cycle
	boolean first_pass=true;

	
	do 
	    {
	    // offset execute to next repeat time
	    switch ( repeat_type )
		{
		case REPEAT_TYPE_DAILY:
		    if ( first_pass  )
			break;
		    execute.add( Calendar.DATE, 1 );
		    break;

		case REPEAT_TYPE_WEEKLY:
		    // if just started and already on a valid day no need to advance
		    if ( first_pass && isRepeatSpecDayOfWeek(execute.get( Calendar.DAY_OF_WEEK ) ) )
			break;
		    
		    // if not the first pass we must advance a day at a time to first valid
		    // day of week or to start of next week of execution
		    do
			{
			execute.add( Calendar.DATE, 7*getRepeatPeriod() );
			item++;
			// if we just got to start of week following execution week
			// 
			// 
			if ( item==7 )
			    {
			    // if the repeat period is less frequent than every week
			    // then the date needs to be advanced a multiple of weeks
			    if ( getRepeatPeriod()>1 )
				execute.add( Calendar.DATE, 7*(getRepeatPeriod()-1) );
			    item=0;
			    }
			}
		    while ( !isRepeatSpecDayOfWeek(execute.get( Calendar.DAY_OF_WEEK ) ) );
		    
		    break;

		case REPEAT_TYPE_MONTHLY_WEEK:
		    throw new IllegalArgumentException( "Not supported yet." );

		case REPEAT_TYPE_MONTHLY_DAY:
		    while ( first_pass && !isRepeatSpecMonthOfYear( execute.get( Calendar.MONTH ) ) )
			{
			if ( !first_pass )
			    execute.add( Calendar.MONTH, 1 );
			first_pass=false;
			// advance to correct date in current month or next month if
			// date doesn't exist in current month
			while ( execute.get( Calendar.DATE ) != getRepeatSpecDayOfMonth() )
			    execute.add( Calendar.DATE, 1 );
			}
		    break;

		case REPEAT_TYPE_NEVER:
		    setExecutionTime( null );
		    return;
		    
		default:
		    if ( !execute.before( now ) )
			{
			if ( to!=null && !execute.before( to ) )
			    return;
			setExecutionTime( new java.sql.Timestamp( execute.getTimeInMillis() ) );
			setState( STATE_OVERDUE );
			}
		    return;
		}

	    if (  to!=null && !execute.before( to ) )
		return;
		
	    first_pass=false;
	    in_future = !execute.before( now );
	    }
	while ( !in_future );
	
	setExecutionTime( new java.sql.Timestamp( execute.getTimeInMillis() ) );
	setState( STATE_OVERDUE );
	}
	
    private static final String[] wom = {"last", "first","second","third","fourth"};
    private static final DateFormatSymbols symbols= new DateFormatSymbols();
    
    public String getScheduleDescription()
	{
	int i, j;
	StringBuffer buffer = new StringBuffer();
	SimpleDateFormat date = new SimpleDateFormat( "d MMMMM yyyy" );
	SimpleDateFormat time = new SimpleDateFormat( "h:mm a zzzz" );
	
	switch ( repeat_type )
	    {
	    case REPEAT_TYPE_NEVER:
		buffer.append( "never" );
		break;
		
	    case REPEAT_TYPE_DAILY:
		buffer.append( "daily at " );
		buffer.append( time.format( from_time ) );
		buffer.append( " starting on " );
		buffer.append( date.format( from_time ) );
		if ( to_time != null )
		    {
		    buffer.append( " until " );
		    buffer.append( date.format( to_time ) );
		    }
		break;
		
	    case REPEAT_TYPE_WEEKLY:
		if ( repeat_period > 1 )
		    {
		    buffer.append( "every " );
		    buffer.append( repeat_period );
		    buffer.append( " weeks " );
		    }
		else
		    buffer.append( "weekly at " );
		buffer.append( time.format( from_time ) );
		buffer.append( " on" );
		for ( i=Calendar.SUNDAY; i<=Calendar.SATURDAY; i++ )
		    if ( isRepeatSpecDayOfWeek( i ) )
			{
			buffer.append( " " );
			buffer.append( symbols.getWeekdays()[i] );
			break;
			}
		buffer.append( " starting from " );
		buffer.append( date.format( from_time ) );
		if ( to_time != null )
		    {
		    buffer.append( " until " );
		    buffer.append( date.format( to_time ) );
		    }
		break;
		
	    case REPEAT_TYPE_MONTHLY_WEEK:
		buffer.append( "monthly at " );
		buffer.append( time.format( from_time ) );
		buffer.append( " on" );
		for ( j=0; j<5; j++ )
		    for ( i=Calendar.SUNDAY; i<=Calendar.SATURDAY; i++ )
			if ( isRepeatSpecWeekOfMonth( j, i ) )
			    {
			    buffer.append( " the " );
			    buffer.append( wom[j] );
			    buffer.append( " " );
			    buffer.append( symbols.getWeekdays()[i] );
			    buffer.append( " of the month" );
			    }
		buffer.append( " during" );
		for ( i=Calendar.JANUARY; i<=Calendar.DECEMBER; i++ )
		    if ( isRepeatSpecMonthOfYear( i ) )
			{
			buffer.append( " " );
			buffer.append( symbols.getMonths()[i] );
			break;
			}
		buffer.append( " starting on " );
		buffer.append( date.format( to_time ) );
		if ( to_time != null )
		    {
		    buffer.append( " until " );
		    buffer.append( date.format( to_time ) );
		    }
		break;
		
	    case REPEAT_TYPE_MONTHLY_DAY:
		buffer.append( "monthly at " );
		buffer.append( time.format( from_time ) );
		buffer.append( " on day " );
		buffer.append( getRepeatSpecDayOfMonth() );
		buffer.append( " of the months" );
		for ( i=Calendar.JANUARY; i<=Calendar.DECEMBER; i++ )
		    if ( isRepeatSpecMonthOfYear( i ) )
			{
			buffer.append( " " );
			buffer.append( symbols.getMonths()[i] );
			break;
			}
		buffer.append( " starting on " );
		buffer.append( date.format( to_time ) );
		if ( to_time != null )
		    {
		    buffer.append( " until " );
		    buffer.append( date.format( to_time ) );
		    }
		break;
		
	    default:
		buffer.append( "once only on " );
		buffer.append( date.format( from_time ) );
		buffer.append( " at " );
		buffer.append( time.format( from_time ) );
		break;
	    }
	    
	return buffer.toString();
	}
	
	
    public void main( String[] args )
	{
	Job j = new Job();
	j.setFromTime( new java.sql.Timestamp( System.currentTimeMillis() ) );
	j.setRepeatType( Job.REPEAT_TYPE_DAILY );
	j.setNextExecutionTime();
	}

    /**
     * Look for jobs that match the supplied parameters. None of the parameters
     * should be null although they may be empty strings. Dont ask me why (database).
     * This might have been a nice method if it wasn't for the SQL hell.
     * @param session The session to look for. Normally this is the full (including package) class name.
     * @param method The method to invoke on this class.
     * @param future If true then only jobs that will run are included (future).
     * @return A list of jobs that match the criterea.
     * @throws BuildingServerException
     */
    public static Enumeration findJobs( String session, String method, boolean future)
    	throws BuildingServerException
    {
        StringBuffer where = new StringBuffer();
        where.append(
            " session_name = '"+ session+
            "' AND method_name = '" + method+
            "'");
        if(future)
        {
            where.append("  AND state = "+ Job.STATE_OVERDUE);
        }
        
        return Job.findJobs(where.toString());
        
    }
    }
    
