/*
 * 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.Serializable;
import java.util.Date;
import java.util.concurrent.locks.ReentrantLock;

import javax.ejb.EJBException;
import javax.ejb.ScheduleExpression;
import javax.ejb.Timer;
import javax.ejb.TimerHandle;

import org.jboss.as.ejb3.logging.EjbLogger;
import org.jboss.as.ejb3.component.allowedmethods.AllowedMethodsInformation;
import org.jboss.as.ejb3.component.allowedmethods.MethodType;
import org.jboss.as.ejb3.timerservice.spi.TimedObjectInvoker;

/**
 * Implementation of EJB3.1 {@link Timer}
 *
 * @author <a href="mailto:cdewolf@redhat.com">Carlo de Wolf</a>
 * @version $Revision: $
 */
public class TimerImpl implements Timer {
    /**
     * The output format with the cached final values, after the first toString() invocation
     */
    private String toStringTemplate = null;
    /**
     * 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;

    private final String timedObjectId;

    /**
     * In use lock. This is held by timer invocations and cancellation within the scope of a transaction, all changes
     * to timer state after creation should be done within this lock, to prevent state being overwritten by multiple
     * threads.
     * <p/>
     * If the timer cancelled, but then rolled back we do not want the timer task to see this cancellation.
     *
     * todo: we can probably just use a sync block here
     */
    private final ReentrantLock inUseLock = new ReentrantLock();

    /**
     * Creates a {@link TimerImpl}
     *
     * @param builder          The builder with the timer information
     * @param service          The timer service through which this timer was created
     */
    protected TimerImpl(Builder builder, TimerServiceImpl service) {
        assert builder.id != null : "id is null";

        this.id = builder.id;
        this.timedObjectId = builder.timedObjectId;
        this.info = builder.info;
        this.persistent = builder.persistent;
        this.initialExpiration = builder.initialDate;
        this.intervalDuration = builder.repeatInterval;
        if (builder.newTimer && builder.nextDate == null) {
            this.nextExpiration = initialExpiration;
        } else {
            this.nextExpiration = builder.nextDate;
        }
        this.previousRun = builder.previousRun;
        this.primaryKey = builder.primaryKey;

        this.timerState = builder.timerState;
        this.timerService = service;
        this.timedObjectInvoker = service.getInvoker();
        this.handle = new TimerHandleImpl(this.id, this.timedObjectInvoker.getTimedObjectId(), service);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void cancel() throws IllegalStateException, EJBException {
        timerService.cancelTimer(this);
    }

    /**
     * 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}
     *
     * @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 EjbLogger.ROOT_LOGGER.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;
    }

    public boolean isTimerPersistent() {
        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 EjbLogger.ROOT_LOGGER.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) {
        if(next == null) {
            setTimerState(TimerState.EXPIRED);
        }
        this.nextExpiration = next;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public ScheduleExpression getSchedule() throws IllegalStateException, EJBException {
        this.assertTimerState();
        throw EjbLogger.ROOT_LOGGER.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 EjbLogger.ROOT_LOGGER.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 timedObjectId;
    }

    /**
     * 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>
     * <li>has not been suspended</li>
     * </ul>
     * <p/>
     * And if the corresponding timer service is still up
     * <p/>
     * </p>
     *
     * @return
     */
    public boolean isActive() {
        return timerService.isStarted() && !isCanceled() && !isExpired() && (timerService.isScheduled(getId()) || timerState == TimerState.CREATED);
    }

    /**
     * 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 EjbLogger.ROOT_LOGGER.timerHasExpired();
        if (timerState == TimerState.CANCELED)
            throw EjbLogger.ROOT_LOGGER.timerWasCanceled();
        AllowedMethodsInformation.checkAllowed(MethodType.TIMER_SERVICE_METHOD);
    }

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

    /**
     * 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();
    }

    /**
     * Triggers timer, outside of normal expiration. Only used when running an explicit management trigger operation.
     *
     * The tigger operation simply runs the callback, it does not modify the timer state in any way, and there is no
     * protection against overlapping events when running it. This is the expected behaviour, as otherwise the semantics
     * of dealing with concurrent execution is complex and kinda weird.
     */
    public void invokeOneOff() throws Exception {
        this.getTimerTask().invokeBeanMethod(this);
    }
    /**
     * 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);
    }

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

    public void lock() {
        inUseLock.lock();
    }

    public void unlock() {
        inUseLock.unlock();
    }

    /**
     * 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() {
        if (this.toStringTemplate == null) {
            // initialize with the first invocation
            StringBuilder sb = new StringBuilder();
            sb.append("[id=");
            sb.append(this.id);
            sb.append(" timedObjectId=");
            sb.append(timedObjectId);
            sb.append(" auto-timer?:");
            sb.append(this.isAutoTimer());
            sb.append(" persistent?:");
            sb.append(this.persistent);
            sb.append(" timerService=");
            sb.append(this.timerService);
            sb.append(" initialExpiration=");
            sb.append(this.initialExpiration);
            sb.append(" intervalDuration(in milli sec)=");
            sb.append(this.intervalDuration);
            this.toStringTemplate = sb.toString();
        }
        // complete with the dynamic values
        StringBuilder sb = new StringBuilder(this.toStringTemplate);
        sb.append(" nextExpiration=");
        sb.append(this.nextExpiration);
        sb.append(" timerState=");
        sb.append(this.timerState);
        sb.append(" info=");
        sb.append(this.info);
        sb.append("]");
        return sb.toString();
   }

    public Object getPrimaryKey() {
        return primaryKey;
    }

    public static Builder builder() {
        return new Builder();
    }

    public static class Builder {
        protected String id;
        protected String timedObjectId;
        protected Date initialDate;
        protected long repeatInterval;
        protected Date nextDate;
        protected Date previousRun;
        protected Serializable info;
        protected Object primaryKey;
        protected TimerState timerState;
        protected boolean persistent;
        protected boolean newTimer;

        public Builder setId(final String id) {
            this.id = id;
            return this;
        }

        public Builder setTimedObjectId(final String timedObjectId) {
            this.timedObjectId = timedObjectId;
            return this;
        }

        public Builder setInitialDate(final Date initialDate) {
            this.initialDate = initialDate;
            return this;
        }

        public Builder setRepeatInterval(final long repeatInterval) {
            this.repeatInterval = repeatInterval;
            return this;
        }

        public Builder setNextDate(final Date nextDate) {
            this.nextDate = nextDate;
            return this;
        }

        public Builder setPreviousRun(final Date previousRun) {
            this.previousRun = previousRun;
            return this;
        }

        public Builder setInfo(final Serializable info) {
            this.info = info;
            return this;
        }

        public Builder setPrimaryKey(final Object primaryKey) {
            this.primaryKey = primaryKey;
            return this;
        }

        public Builder setTimerState(final TimerState timerState) {
            this.timerState = timerState;
            return this;
        }

        public Builder setPersistent(final boolean persistent) {
            this.persistent = persistent;
            return this;
        }

        public Builder setNewTimer(final boolean newTimer) {
            this.newTimer = newTimer;
            return this;
        }

        public String getId() {
            return id;
        }

        public String getTimedObjectId() {
            return timedObjectId;
        }

        public Date getInitialDate() {
            return initialDate;
        }

        public long getRepeatInterval() {
            return repeatInterval;
        }

        public Date getNextDate() {
            return nextDate;
        }

        public Date getPreviousRun() {
            return previousRun;
        }

        public Serializable getInfo() {
            return info;
        }

        public Object getPrimaryKey() {
            return primaryKey;
        }

        public TimerState getTimerState() {
            return timerState;
        }

        public boolean isPersistent() {
            return persistent;
        }

        public boolean isNewTimer() {
            return newTimer;
        }

        public TimerImpl build(final TimerServiceImpl timerService) {
            return new TimerImpl(this, timerService);
        }
    }
}
