/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2011, Red Hat, Inc., and individual contributors
 * as indicated by the @author tags. See the copyright.txt file in the
 * distribution for a full listing of individual contributors.
 *
 * This 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 (at your option) any later version.
 *
 * This software 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 software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.jboss.as.ejb3.timerservice;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectStreamClass;
import java.io.Serializable;
import java.util.Date;

import javax.ejb.EJBException;
import javax.ejb.ScheduleExpression;
import javax.ejb.Timer;
import javax.ejb.TimerHandle;
import javax.transaction.Status;
import javax.transaction.Synchronization;
import javax.transaction.Transaction;

import org.jboss.as.ejb3.component.allowedmethods.AllowedMethodsInformation;
import org.jboss.as.ejb3.component.allowedmethods.MethodType;
import org.jboss.as.ejb3.timerservice.persistence.TimerEntity;
import org.jboss.as.ejb3.timerservice.spi.TimedObjectInvoker;
import org.jboss.as.ejb3.timerservice.task.TimerTask;

import static org.jboss.as.ejb3.EjbLogger.ROOT_LOGGER;
import static org.jboss.as.ejb3.EjbMessages.MESSAGES;

/**
 * Implementation of EJB3.1 {@link Timer}
 *
 * @author <a href="mailto:cdewolf@redhat.com">Carlo de Wolf</a>
 * @version $Revision: $
 */
public class TimerImpl implements Timer {

    /**
     * Unique id for this timer instance
     */
    protected final String id;

    /**
     * The timer state
     */
    protected volatile TimerState timerState;

    /**
     * The {@link javax.ejb.TimerService} through which this timer was created
     */
    protected final TimerServiceImpl timerService;

    /**
     * The {@link TimedObjectInvoker} to which this timer corresponds
     */
    protected final TimedObjectInvoker timedObjectInvoker;

    /**
     * The info which was passed while creating the timer.
     */
    protected final Serializable info;

    /**
     * Indicates whether the timer is persistent
     */
    protected final boolean persistent;

    /**
     * A {@link javax.ejb.TimerHandle} for this timer
     */
    protected final TimerHandleImpl handle;

    /**
     * The initial (first) expiry date of this timer
     */
    protected final Date initialExpiration;

    /**
     * The duration in milli sec. between timeouts
     */
    protected final long intervalDuration;

    /**
     * If this is an entity bean then this is the primary key
     */
    protected final Object primaryKey;

    /**
     * Next expiry date of this timer
     */
    protected volatile Date nextExpiration;

    /**
     * The date of the previous run of this timer
     */
    protected volatile Date previousRun;

    /**
     * Creates a {@link TimerImpl}
     *
     * @param id               The id of this timer
     * @param service          The timer service through which this timer was created
     * @param initialExpiry    The first expiry of this timer
     * @param intervalDuration The duration (in milli sec) between timeouts
     * @param info             The info that will be passed on through the {@link Timer} and will be available through the {@link Timer#getInfo()} method
     * @param persistent       True if this timer is persistent. False otherwise
     */
    public TimerImpl(String id, TimerServiceImpl service, Date initialExpiry, long intervalDuration, Serializable info,
                     boolean persistent, Object primaryKey, final TimerState timerState) {
        this(id, service, initialExpiry, intervalDuration, initialExpiry, info, persistent, primaryKey, timerState);
    }

    /**
     * Creates a {@link TimerImpl}
     *
     * @param id               The id of this timer
     * @param service          The timer service through which this timer was created
     * @param initialExpiry    The first expiry of this timer. Can be null
     * @param intervalDuration The duration (in milli sec) between timeouts
     * @param nextEpiry        The next expiry of this timer
     * @param info             The info that will be passed on through the {@link Timer} and will be available through the {@link Timer#getInfo()} method
     * @param persistent       True if this timer is persistent. False otherwise
     */
    public TimerImpl(String id, TimerServiceImpl service, Date initialExpiry, long intervalDuration, Date nextEpiry,
                     Serializable info, boolean persistent, Object primaryKey, final TimerState timerState) {
        assert service != null : "service is null";
        assert id != null : "id is null";

        this.id = id;
        this.timerService = service;

        this.timedObjectInvoker = service.getInvoker();
        this.info = info;
        this.persistent = persistent;
        this.initialExpiration = initialExpiry;
        this.intervalDuration = intervalDuration;
        this.nextExpiration = nextEpiry;
        this.previousRun = null;
        this.primaryKey = primaryKey;

        // create a timer handle for this timer
        this.handle = new TimerHandleImpl(this.id, this.timedObjectInvoker.getTimedObjectId(), service);
        this.timerState = timerState;
    }

    /**
     * Creates a {@link TimerImpl} out of a persisted timer
     *
     * @param persistedTimer The persisted state of the timer
     * @param service        The timer service to which this timer belongs
     */
    public TimerImpl(TimerEntity persistedTimer, TimerServiceImpl service) {
        this(persistedTimer.getId(), service, persistedTimer.getInitialDate(), persistedTimer.getInterval(),
                persistedTimer.getNextDate(), persistedTimer.getInfo(), true, persistedTimer.getPrimaryKey(), persistedTimer.getTimerState());
        this.previousRun = persistedTimer.getPreviousRun();
    }

    /**
     * Returns the id of this timer
     *
     * @return
     */
    public String getId() {
        return this.id;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isCalendarTimer() throws IllegalStateException, EJBException {
        // first check whether this timer has expired or cancelled
        this.assertTimerState();

        return false;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void cancel() throws IllegalStateException, EJBException {
        // first check whether the timer has expired or has been cancelled
        this.assertTimerState();
        boolean startedInTx = timerState == TimerState.CREATED;
        if (timerState != TimerState.EXPIRED) {
            setTimerState(TimerState.CANCELED);
        }
        if (timerService.transactionActive() && !startedInTx) {
            final Transaction currentTx = this.timerService.getTransaction();
            this.registerTimerCancellationWithTx(currentTx);
        } else {
            // cancel any scheduled Future for this timer
            this.cancelTimeout();
        }
        // persist changes
        timerService.persistTimer(this, false);
    }

    /**
     * {@inheritDoc}
     *
     * @see #getTimerHandle()
     */
    @Override
    public TimerHandle getHandle() throws IllegalStateException, EJBException {
        // make sure it's in correct state
        this.assertTimerState();

        // for non-persistent timers throws an exception (mandated by EJB3 spec)
        if (this.persistent == false) {
            throw MESSAGES.invalidTimerHandlersForPersistentTimers("EJB3.1 Spec 18.2.6");
        }
        return this.handle;
    }

    /**
     * This method returns the {@link javax.ejb.TimerHandle} corresponding to this {@link TimerImpl}.
     * Unlike the {@link #getHandle()} method, this method does <i>not</i> throw an {@link IllegalStateException}
     * or {@link javax.ejb.NoSuchObjectLocalException} or {@link javax.ejb.EJBException}, for non-persistent timers.
     * Instead this method returns the {@link javax.ejb.TimerHandle} corresponding to that non-persistent
     * timer (remember that {@link TimerImpl} creates {@link javax.ejb.TimerHandle} for both persistent and non-persistent timers)
     *
     * @return
     */
    public TimerHandle getTimerHandle() {
        return this.handle;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isPersistent() throws IllegalStateException, EJBException {
        // make sure the call is allowed in the current timer state
        this.assertTimerState();

        return this.persistent;
    }

    /**
     * {@inheritDoc}
     *
     * @see #getTimerInfo()
     */
    @Override
    public Serializable getInfo() throws IllegalStateException, EJBException {
        // make sure this call is allowed
        this.assertTimerState();

        return this.info;
    }

    /**
     * This method is similar to {@link #getInfo()}, except that this method does <i>not</i> check the timer state
     * and hence does <i>not</i> throw either {@link IllegalStateException} or {@link javax.ejb.NoSuchObjectLocalException}
     * or {@link javax.ejb.EJBException}.
     *
     * @return
     */
    public Serializable getTimerInfo() {
        return this.info;
    }

    /**
     * {@inheritDoc}
     *
     * @see #getNextExpiration()
     */
    @Override
    public Date getNextTimeout() throws IllegalStateException, EJBException {
        // first check the validity of the timer state
        this.assertTimerState();
        if (this.nextExpiration == null) {
            throw MESSAGES.noMoreTimeoutForTimer(this);
        }
        return this.nextExpiration;
    }

    /**
     * This method is similar to {@link #getNextTimeout()}, except that this method does <i>not</i> check the timer state
     * and hence does <i>not</i> throw either {@link IllegalStateException} or {@link javax.ejb.NoSuchObjectLocalException}
     * or {@link javax.ejb.EJBException}.
     *
     * @return
     */
    public Date getNextExpiration() {
        return this.nextExpiration;
    }

    /**
     * Sets the next timeout of this timer
     *
     * @param next The next scheduled timeout of this timer
     */
    public void setNextTimeout(Date next) {
        this.nextExpiration = next;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public ScheduleExpression getSchedule() throws IllegalStateException, EJBException {
        this.assertTimerState();
        throw MESSAGES.invalidTimerNotCalendarBaseTimer(this);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public long getTimeRemaining() throws IllegalStateException, EJBException {
        // TODO: Rethink this implementation

        // first check the validity of the timer state
        this.assertTimerState();
        if (this.nextExpiration == null) {
            throw MESSAGES.noMoreTimeoutForTimer(this);
        }
        long currentTimeInMillis = System.currentTimeMillis();
        long nextTimeoutInMillis = this.nextExpiration.getTime();

        // if the next expiration is *not* in future and the repeat interval isn't
        // a positive number (i.e. no repeats) then there won't be any more timeouts.
        // So throw a NoMoreTimeoutsException.
        // NOTE: We check for intervalDuration and not just nextExpiration because,
        // it's a valid case where the nextExpiration is in past (maybe the server was
        // down when the timeout was expected)
        //      if (nextTimeoutInMillis < currentTimeInMillis && this.intervalDuration <= 0)
        //      {
        //         throw new NoMoreTimeoutsException("No more timeouts for timer " + this);
        //      }
        return nextTimeoutInMillis - currentTimeInMillis;
    }

    public boolean isAutoTimer() {
        return false;
    }

    /**
     * Cancels any scheduled timer task for this timer
     */
    protected void cancelTimeout() {
        // delegate to the timerservice, so that it can cancel any scheduled Future
        // for this timer
        this.timerService.cancelTimeout(this);
    }

    /**
     * Returns the initial (first) timeout date of this timer
     *
     * @return
     */
    public Date getInitialExpiration() {
        return this.initialExpiration;
    }

    /**
     * Returns the interval (in milliseconds), between timeouts, of this timer.
     *
     * @return
     */
    public long getInterval() {
        return this.intervalDuration;
    }

    /**
     * Returns the timed object id to which this timer belongs
     *
     * @return
     */
    public String getTimedObjectId() {
        return this.timerService.getInvoker().getTimedObjectId();
    }

    /**
     * Returns the timer service through which this timer was created
     *
     * @return
     */
    public TimerServiceImpl getTimerService() {
        return this.timerService;
    }

    /**
     * Returns true if this timer is active. Else returns false.
     * <p>
     * A timer is considered to be "active", if its {@link TimerState}
     * is neither of the following:
     * <ul>
     * <li>{@link TimerState#CANCELED}</li>
     * <li>{@link TimerState#EXPIRED}</li>
     * </ul>
     * <p/>
     * And if the corresponding timer service is still up
     * <p/>
     * </p>
     *
     * @return
     */
    public boolean isActive() {
        return timerService.isStarted() && !isCanceled() && !isExpired();
    }

    /**
     * Returns true if this timer is in {@link TimerState#CANCELED} state. Else returns false.
     *
     * @return
     */
    public boolean isCanceled() {
        return timerState == TimerState.CANCELED;
    }

    /**
     * Returns true if this timer is in {@link TimerState#EXPIRED} state. Else returns false
     *
     * @return
     */
    public boolean isExpired() {
        return timerState == TimerState.EXPIRED;
    }

    /**
     * Returns true if this timer is in {@link TimerState#RETRY_TIMEOUT}. Else returns false.
     *
     * @return
     */
    public boolean isInRetry() {
        return timerState == TimerState.RETRY_TIMEOUT;
    }

    /**
     * Returns the {@link java.util.Date} of the previous timeout of this timer
     *
     * @return
     */
    public Date getPreviousRun() {
        return this.previousRun;
    }

    /**
     * Sets the {@link java.util.Date} of the previous timeout of this timer
     *
     * @param previousRun
     */
    public void setPreviousRun(Date previousRun) {
        this.previousRun = previousRun;
    }

    /**
     * Returns the current state of this timer
     *
     * @return
     */
    public TimerState getState() {
        return this.timerState;
    }

    /**
     * Asserts that the timer is <i>not</i> in any of the following states:
     * <ul>
     * <li>{@link TimerState#CANCELED}</li>
     * <li>{@link TimerState#EXPIRED}</li>
     * </ul>
     *
     * @throws javax.ejb.NoSuchObjectLocalException
     *          if the txtimer was canceled or has expired
     */
    protected void assertTimerState() {
        if (timerState == TimerState.EXPIRED)
            throw MESSAGES.timerHasExpired();
        if (timerState == TimerState.CANCELED)
            throw MESSAGES.timerWasCanceled();
        AllowedMethodsInformation.checkAllowed(MethodType.TIMER_SERVICE_METHOD);
    }

    /**
     * Expire, and remove it from the timer service.
     */
    public void expireTimer() {
        ROOT_LOGGER.debug("expireTimer: " + this);
        setTimerState(TimerState.EXPIRED);
        // remove from timerservice
        timerService.persistTimer(this, false);
        // Cancel any scheduled timer task for this timer
        this.cancelTimeout();
    }

    /**
     * Sets the state of this timer
     *
     * @param state The state of this timer
     */
    public void setTimerState(TimerState state) {
        this.timerState = state;
    }

    /**
     * Returns the current persistent state of this timer
     */
    public TimerEntity getPersistentState() {
        if (!this.persistent) {
            throw MESSAGES.failToPersistTimer(this);
        }

        //we always create a new copy of the persistent state
        return this.createPersistentState();
    }

    /**
     * Suspends any currently scheduled task for this timer
     * <p>
     * Note that, suspend does <b>not</b> cancel the {@link Timer}. Instead,
     * it just cancels the <b>next scheduled timeout</b>. So once the {@link Timer}
     * is restored (whenever that happens), the {@link Timer} will continue to
     * timeout at appropriate times.
     * </p>
     */
    // TODO: Revisit this method, we probably don't need this any more.
    // In terms of implementation, this is just equivalent to cancelTimeout() method
    public void suspend() {
        // cancel any scheduled timer task (Future) for this timer
        this.cancelTimeout();
    }

    /**
     * Creates and schedules a {@link TimerTask} for the next timeout of this timer
     *
     * @param newTimer <code>true</code> if this is a new timer being scheduled, and not a re-schedule due to a timeout
     */
    public void scheduleTimeout(boolean newTimer) {
        // just delegate to timerservice, for it to do the actual scheduling
        this.timerService.scheduleTimeout(this, newTimer);
    }

    /**
     * Creates and returns a new persistent state of this timer
     *
     * @return
     */
    protected TimerEntity createPersistentState() {
        return new TimerEntity(this);
    }

    /**
     * Returns the task which handles the timeouts of this {@link TimerImpl}
     *
     * @return
     * @see TimerTask
     */
    protected TimerTask<?> getTimerTask() {
        return new TimerTask<TimerImpl>(this);
    }

    /**
     * A {@link javax.ejb.Timer} is equal to another {@link javax.ejb.Timer} if their
     * {@link javax.ejb.TimerHandle}s are equal
     */
    @Override
    public boolean equals(Object obj) {
        if (obj == null) {
            return false;
        }
        if (this.handle == null) {
            return false;
        }
        if (obj instanceof TimerImpl == false) {
            return false;
        }
        TimerImpl otherTimer = (TimerImpl) obj;
        return this.handle.equals(otherTimer.getTimerHandle());
    }

    @Override
    public int hashCode() {
        return this.handle.hashCode();
    }


    /**
     * A nice formatted string output for this timer
     * {@inheritDoc}
     */
    @Override
    public String toString() {
        //TODO: Cache this
        StringBuilder sb = new StringBuilder();
        sb.append("[id=");
        sb.append(this.id);
        sb.append(" ");
        sb.append("timedObjectId=");
        if (this.timedObjectInvoker == null) {
            sb.append("null");
        } else {
            sb.append(this.timedObjectInvoker.getTimedObjectId());
        }
        sb.append(" ");
        sb.append("auto-timer?:");
        sb.append(this.isAutoTimer());
        sb.append(" ");
        sb.append("persistent?:");
        sb.append(this.persistent);
        sb.append(" ");
        sb.append("timerService=");
        sb.append(this.timerService);
        sb.append(" ");
        sb.append("initialExpiration=");
        sb.append(this.initialExpiration);
        sb.append(" ");
        sb.append("intervalDuration(in milli sec)=");
        sb.append(this.intervalDuration);
        sb.append(" ");
        sb.append("nextExpiration=");
        sb.append(this.nextExpiration);
        sb.append(" ");
        sb.append("timerState=");
        sb.append(this.timerState);

        return sb.toString();
    }

    private void registerTimerCancellationWithTx(Transaction tx) {
        try {
            tx.registerSynchronization(new TimerCancellationTransactionSynchronization(this));
        } catch (Exception e) {
            throw MESSAGES.failToRegisterWithTxTimerCancellation(e);
        }
    }

    private Serializable deserialize(byte[] bytes) {
        if (bytes == null) {
            return null;
        }
        try {
            ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
            ObjectInputStream ois = new ObjectInputStreamWithTCCL(bais);
            return (Serializable) ois.readObject();
        } catch (IOException ioe) {
            throw MESSAGES.failToDeserializeInfoInTimer(ioe);
        } catch (ClassNotFoundException cnfe) {
            throw MESSAGES.failToDeserializeInfoInTimer(cnfe);
        }
    }

    /**
     * {@link ObjectInputStreamWithTCCL} during {@link #resolveClass(java.io.ObjectStreamClass)}
     * first tries to resolve the class in the thread context classloader
     * {@link Thread#getContextClassLoader()}. If it cannot resolve in the current context
     * loader, it passes on the control to {@link java.io.ObjectInputStream} to resolve the class
     */
    private final class ObjectInputStreamWithTCCL extends ObjectInputStream {

        public ObjectInputStreamWithTCCL(InputStream in) throws IOException {
            super(in);
        }

        protected Class<?> resolveClass(ObjectStreamClass v) throws IOException, ClassNotFoundException {
            String className = v.getName();
            Class<?> resolvedClass = null;

            ROOT_LOGGER.trace("Attempting to locate class [" + className + "]");

            try {
                resolvedClass = timedObjectInvoker.getClassLoader().loadClass(className);
            } catch (ClassNotFoundException e) {
                resolvedClass = getClass().getClassLoader().loadClass(className);
            }
            return resolvedClass;
        }
    }

    private class TimerCancellationTransactionSynchronization implements Synchronization {

        /**
         * The timer being managed in the transaction
         */
        private TimerImpl timer;

        public TimerCancellationTransactionSynchronization(TimerImpl timer) {
            if (timer == null) {
                throw MESSAGES.timerIsNull();
            }
            this.timer = timer;
        }

        @Override
        public void afterCompletion(int status) {
            if (status == Status.STATUS_COMMITTED) {
                ROOT_LOGGER.debug("commit timer cancellation: " + this.timer);
                this.timer.cancelTimeout();
            } else if (status == Status.STATUS_ROLLEDBACK) {
                ROOT_LOGGER.debug("rollback timer cancellation: " + this.timer);
                this.timer.setTimerState(TimerState.ACTIVE);
            }
        }

        @Override
        public void beforeCompletion() {

        }

    }

    public Object getPrimaryKey() {
        return primaryKey;
    }
}
