/**
 * JOnAS: Java(TM) Open Application Server
 * Copyright (C) 1999 Bull S.A.
 * Contact: jonas-team@ow2.org
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
 * USA
 *
 * --------------------------------------------------------------------------
 * $Id$
 * --------------------------------------------------------------------------
 */

package org.ow2.jonas.lib.ejb21;

import java.io.File;
import java.io.Serializable;
import java.util.Date;

import javax.ejb.EJBException;
import javax.ejb.NoSuchObjectLocalException;
import javax.ejb.ScheduleExpression;
import javax.ejb.Timer;
import javax.ejb.TimerHandle;
import javax.transaction.RollbackException;
import javax.transaction.Status;
import javax.transaction.Synchronization;
import javax.transaction.SystemException;
import javax.transaction.Transaction;
import javax.transaction.TransactionManager;

import org.ow2.jonas.lib.timer.TimerEvent;
import org.ow2.jonas.lib.timer.TimerEventListener;
import org.ow2.jonas.lib.timer.TimerManager;
import org.ow2.jonas.lib.timer.TraceTimer;

import org.objectweb.util.monolog.api.BasicLevel;

/**
 * JOnAS Implementation of the Timer interface (from EJB 2.1) This is a basic
 * implementation based on jonas_timer. A later (and better ?) implementation
 * could be based on Quartz.
 * @author Philippe Durieux
 */
public class JTimer implements Timer, TimerEventListener, Synchronization {

    /**
     * true if "one-shot" timer
     */
    private boolean oneshot;

    /**
     * period in case of repeatable timer (millisec)
     */
    private long period;

    /**
     * initial duration (millisec)
     */
    private long initialduration;

    /**
     * The TimerService that manages this Timer.
     */
    private JTimerService timerservice;

    /**
     * Info provided at Timer Creation time.
     */
    private Serializable info;

    /**
     * Jonas Timer Manager
     */
    private TimerManager tim = TimerManager.getInstance();

    /**
     * jonas timer initial
     */
    private TimerEvent te1 = null;

    /**
     * repeatable jonas timer
     */
    private TimerEvent te2 = null;

    /**
     * Start Time of this Timer (millisec) used for Timer identification.
     */
    private long starttime;

    /**
     * Next expiration time Used to compute the remaining time.
     */
    private long endtime;

    /**
     * true when timer has been created inside current tx
     */
    private boolean createdInTx = false;

    /**
     * true when timer has been cancelled inside current tx
     */
    private boolean cancelledInTx = false;

    /**
     * true when timer has been cancelled
     */
    private boolean cancelled = false;

    /**
     * TimerHandle for this Timer
     */
    private TimerHandle myHandle = null;
    
    /**
     * File representing the Timer on disk
     */
    private File myfile = null;

    /**
     * constructor
     */
    public JTimer(JTimerService timerservice, long initial, long period, Serializable info) {
        if (TraceTimer.isDebug()) {
            TraceTimer.logger.log(BasicLevel.DEBUG, "New JTimer initial = " + initial + ", period = " + period);
        }
        this.timerservice = timerservice;
        this.info = info;
        this.period = period;
        initialduration = initial;
        oneshot = (period == 0);

    }

    /**
     * Give a String representation of the Timer
     */
    public String toString() {
        String ret = "Timer:";
        ret += " starttime=" + starttime;
        ret += " endtime=" + endtime;
        ret += " period=" + period;
        return ret;
    }
    
    /**
     * Set the file associated with this timer.
     */
    public void setFile(File file) {
        myfile = file;
    }
    
    /**
     * @return the start time in millisec.
     */
    public long getStartTime() {
        return starttime;
    }

    /**
     * @return the initial duration in millisec.
     */
    public long getInitialDuration() {
        return initialduration;
    }

    /**
     * @return the period in millisec. (periodic timers only)
     */
    public long getPeriod() {
        return period;
    }

    /**
     * @return the Jonas Timer Service that manages this Timer
     */
    public JTimerService getTimerService() {
        return timerservice;
    }

    /**
     * start the Timer
     */
    public void startTimer() {

        TraceTimer.logger.log(BasicLevel.DEBUG, "");
        
        // If this is created inside a transaction, register this object as a synchronization.
        TransactionManager tm = timerservice.getTransactionManager();
        try {
			Transaction tx = tm.getTransaction();
			if (tx != null) {
				tx.registerSynchronization(this);
				createdInTx = true;
			}
		} catch (SystemException e) {
            TraceTimer.logger.log(BasicLevel.ERROR, "Cannot get Transaction", e);
		} catch (IllegalStateException e) {
            TraceTimer.logger.log(BasicLevel.ERROR, "Cannot register synchronization:", e);
		} catch (RollbackException e) {
            TraceTimer.logger.log(BasicLevel.ERROR, "transaction already rolled back", e);
		}

		// init time
        starttime = System.currentTimeMillis();
        endtime = starttime + initialduration;

        // Create the jonas timer
        te1 = tim.addTimerMs(this, initialduration, null, false);
    }

    /**
     * Stop a timer. Used internally.
     */
    public void stopTimer() {
        if (TraceTimer.isDebug()) {
            TraceTimer.logger.log(BasicLevel.DEBUG, "Stop JTimer");
        }
        cancelled = true;
        if (te1 != null) {
            te1.unset();
            te1 = null;
        }
        if (te2 != null) {
            te2.unset();
            te2 = null;
        }
        // Remove the file
        if (myfile != null) {
            myfile.delete();
        }
    }

    /**
     * @return true if timer has been cancelled
     */
    public boolean isCancelled() {
        return (cancelled || cancelledInTx);
    }

    // ------------------------------------------------------------------------------------
    // The container must implement suitable equals() and hashCode() methods
    // ------------------------------------------------------------------------------------

    /**
     * Indicates whether some other object is "equal to" this one.
     * @param obj - the reference object with which to compare.
     * @return true if this object is the same as the obj argument; false
     *         otherwise.
     */
    public boolean equals(Object obj) {
        if (obj instanceof JTimer) {
            JTimer timer2 = (JTimer) obj;
            if (timer2.getInitialDuration() != initialduration) {
                if (TraceTimer.isDebug()) {
                    TraceTimer.logger.log(BasicLevel.DEBUG, "different duration");
                }
                return false;
            }
            if (timer2.getPeriod() != period) {
                if (TraceTimer.isDebug()) {
                    TraceTimer.logger.log(BasicLevel.DEBUG, "different period");
                }
                return false;
            }
            if (timer2.getTimerService() != timerservice) {
                if (TraceTimer.isDebug()) {
                    TraceTimer.logger.log(BasicLevel.DEBUG, "different timerservice");
                }
                return false;
            }
            if (timer2.getStartTime() != starttime) {
                if (TraceTimer.isDebug()) {
                    TraceTimer.logger.log(BasicLevel.DEBUG, "different startTime");
                }
                return false;
            }
            if (TraceTimer.isDebug()) {
                TraceTimer.logger.log(BasicLevel.DEBUG, "timers are equal");
            }
            return true;
        } else {
            if (TraceTimer.isDebug()) {
                TraceTimer.logger.log(BasicLevel.DEBUG, "not a Timer");
            }
            return false;
        }
    }

    /**
     * Returns a hash code value for the object.
     * @return a hash code value for this object.
     */
    public int hashCode() {
        return (int) (getPeriod() / 1000);
    }

    /**
     * This is used to retrieve a Timer from its Handle only.
     */
    public boolean sameas(Object obj) {
        if (obj instanceof JTimer) {
            JTimer timer2 = (JTimer) obj;
            if (timer2.getInitialDuration() != initialduration) {
                if (TraceTimer.isDebug()) {
                    TraceTimer.logger.log(BasicLevel.DEBUG, "different duration");
                }
                return false;
            }
            if (timer2.getPeriod() != period) {
                if (TraceTimer.isDebug()) {
                    TraceTimer.logger.log(BasicLevel.DEBUG, "different period");
                }
                return false;
            }
            if (timer2.getTimerService() != timerservice) {
                if (TraceTimer.isDebug()) {
                    TraceTimer.logger.log(BasicLevel.DEBUG, "different timerservice");
                }
                return false;
            }
            if (TraceTimer.isDebug()) {
                TraceTimer.logger.log(BasicLevel.DEBUG, "timers are equal");
            }
            return true;
        } else {
            if (TraceTimer.isDebug()) {
                TraceTimer.logger.log(BasicLevel.DEBUG, "not a Timer");
            }
            return false;
        }
    }

    // ------------------------------------------------------------------------------------
    // TimerEventListener implemetation
    // ------------------------------------------------------------------------------------

    /**
     * The timer has just expired.
     */
    public void timeoutExpired(Object arg) {
        if (TraceTimer.isDebug()) {
            TraceTimer.logger.log(BasicLevel.DEBUG, "JTimer expires");
        }
        // propagate the timeout
        timerservice.notify(this);
        if (cancelled) {
            if (TraceTimer.isDebug()) {
                TraceTimer.logger.log(BasicLevel.DEBUG, "JTimer cancelled during timeout");
            }
            return;
        }
        if (te2 == null) {
            te1 = null;
            if (oneshot) {
                // This Timer can be cancelled now.
                doCancel();
            } else {
                // start a new jonas timer (periodic)
                if (TraceTimer.isDebug()) {
                    TraceTimer.logger.log(BasicLevel.DEBUG, "Start periodic jonas timer");
                }
                endtime = System.currentTimeMillis() + period;
                te2 = tim.addTimerMs(this, period, null, true);
            }
        } else {
            // recompute time of next expiration
            endtime = System.currentTimeMillis() + period;
        }
    }

    // ------------------------------------------------------------------------------------
    // Timer implemetation
    // ------------------------------------------------------------------------------------

    /**
     * Cause the timer and all its associated expiration notifications to be
     * cancelled.
     * @throws IllegalStateException the instance is in a state that does not
     *         allow access to this method.
     * @throws NoSuchObjectLocalException If invoked on a timer that has expired
     *         or has been cancelled.
     * @throws EJBException If this method could not complete due to a
     *         system-level failure.
     */
    public void cancel() throws IllegalStateException, NoSuchObjectLocalException, EJBException {
        if (cancelled || cancelledInTx) {
            TraceTimer.logger.log(BasicLevel.DEBUG, "Timer cancelled");
            throw new NoSuchObjectLocalException("Timer already cancelled");
        }
        if (TraceTimer.isDebug()) {
            TraceTimer.logger.log(BasicLevel.DEBUG, "");
        }

        // If this is called inside a transaction, register this object as a synchronization.
        TransactionManager tm = timerservice.getTransactionManager();
        try {
			Transaction tx = tm.getTransaction();
			if (tx != null) {
			    // will be cancelled at commit.
				tx.registerSynchronization(this);
				cancelledInTx = true;
			} else {
			    doCancel();
			}
		} catch (SystemException e) {
            TraceTimer.logger.log(BasicLevel.ERROR, "Cannot get Transaction", e);
		} catch (IllegalStateException e) {
            TraceTimer.logger.log(BasicLevel.ERROR, "Cannot register synchronization:", e);
		} catch (RollbackException e) {
            TraceTimer.logger.log(BasicLevel.ERROR, "transaction already rolled back", e);
		}
    }

    private void doCancel() {
        stopTimer();
        timerservice.remove(this);
    }

    /**
     * Get the number of milliseconds that will elapse before the next scheduled
     * timer expiration.
     * @return the number of milliseconds that will elapse before the next
     *         scheduled timer expiration.
     * @throws IllegalStateException the instance is in a state that does not
     *         allow access to this method.
     * @throws NoSuchObjectLocalException If invoked on a timer that has expired
     *         or has been cancelled.
     * @throws EJBException If this method could not complete due to a
     *         system-level failure.
     */
    public long getTimeRemaining() throws IllegalStateException, NoSuchObjectLocalException, EJBException {
        if (cancelled || cancelledInTx) {
            TraceTimer.logger.log(BasicLevel.DEBUG, "Timer cancelled");
            throw new NoSuchObjectLocalException("Timer cancelled or expired");
        }
        if (TraceTimer.isDebug()) {
            TraceTimer.logger.log(BasicLevel.DEBUG, "");
        }
        return endtime - System.currentTimeMillis();
    }

    /**
     * Get the point in time at which the next timer expiration is scheduled to
     * occur.
     * @return the point in time at which the next timer expiration is scheduled
     *         to occur.
     * @throws IllegalStateException the instance is in a state that does not
     *         allow access to this method.
     * @throws NoSuchObjectLocalException If invoked on a timer that has expired
     *         or has been cancelled.
     * @throws EJBException If this method could not complete due to a
     *         system-level failure.
     */
    public Date getNextTimeout() throws IllegalStateException, NoSuchObjectLocalException, EJBException {
        if (cancelled || cancelledInTx) {
            TraceTimer.logger.log(BasicLevel.DEBUG, "Timer cancelled");
            throw new NoSuchObjectLocalException("Timer cancelled or expired");
        }
        if (TraceTimer.isDebug()) {
            TraceTimer.logger.log(BasicLevel.DEBUG, "");
        }
        return new Date(endtime);
    }

    /**
     * Get the information associated with the timer at the time of creation.
     * @return The Serializable object that was passed in at timer creation, or
     *         null if the info argument passed in at timer creation was null.
     * @throws IllegalStateException the instance is in a state that does not
     *         allow access to this method.
     * @throws NoSuchObjectLocalException If invoked on a timer that has expired
     *         or has been cancelled.
     * @throws EJBException If this method could not complete due to a
     *         system-level failure.
     */
    public Serializable getInfo() throws IllegalStateException, NoSuchObjectLocalException, EJBException {
        if (cancelled || cancelledInTx) {
            TraceTimer.logger.log(BasicLevel.DEBUG, "Timer cancelled");
            throw new NoSuchObjectLocalException("Timer cancelled or expired");
        }
        if (TraceTimer.isDebug()) {
            TraceTimer.logger.log(BasicLevel.DEBUG, "");
        }
        return info;
    }

    /**
     * Get a serializable handle to the timer. This handle can be used at a
     * later time to re-obtain the timer reference.
     * @return a serializable handle to the timer.
     * @throws IllegalStateException the instance is in a state that does not
     *         allow access to this method.
     * @throws NoSuchObjectLocalException If invoked on a timer that has expired
     *         or has been cancelled.
     * @throws EJBException If this method could not complete due to a
     *         system-level failure.
     */
    public TimerHandle getHandle() throws IllegalStateException, NoSuchObjectLocalException, EJBException {
        if (cancelled || cancelledInTx) {
            TraceTimer.logger.log(BasicLevel.DEBUG, "Timer cancelled");
            throw new NoSuchObjectLocalException("Timer cancelled or expired");
        }
        if (TraceTimer.isDebug()) {
            TraceTimer.logger.log(BasicLevel.DEBUG, "");
        }
        if (myHandle == null) {
            myHandle = new JTimerHandle(starttime, initialduration, period, info, timerservice.getEjbName(),
                                        timerservice.getContainer(), timerservice.getPK());
        }
        return myHandle;
    }

    @Override
    public ScheduleExpression getSchedule() throws IllegalStateException, NoSuchObjectLocalException, EJBException {
        throw new UnsupportedOperationException("EJBs 2.1 do not support this operation.");
    }

    @Override
    public boolean isCalendarTimer() throws IllegalStateException, NoSuchObjectLocalException, EJBException {
        throw new UnsupportedOperationException("EJBs 2.1 do not support this operation.");
    }

    @Override
    public boolean isPersistent() throws IllegalStateException, NoSuchObjectLocalException, EJBException {
        throw new UnsupportedOperationException("EJBs 2.1 do not support this operation.");
    }

    // ------------------------------------------------------------------------------------
    // Synchronization implemetation
    // ------------------------------------------------------------------------------------

    /**
     * The afterCompletion method is called by the transaction manager after the
     * transaction is committed or rolled back. This method executes without a
     * transaction context.
     * @param status The status of the transaction completion.
     */
    public void afterCompletion(int status) {
        if (TraceEjb.isDebugTx()) {
            TraceEjb.tx.log(BasicLevel.DEBUG, "");
        }

        // Timers created in a transaction that is rolled back must be removed.
        if (createdInTx) {
            if (status != Status.STATUS_COMMITTED && !cancelled) {
                doCancel();
            }
            createdInTx = false;
        }

        // Cancel timers only if transaction is committed
        if (cancelledInTx) {
            if (status == Status.STATUS_COMMITTED && !cancelled) {
                doCancel();
            }
            cancelledInTx = false;
        }
    }

	/*
	 * @see javax.transaction.Synchronization#beforeCompletion()
	 */
	public void beforeCompletion() {
		// nothing to do
	}

}
