/*
 * Copyright (c) 1998-2012 Caucho Technology -- all rights reserved
 *
 * This file is part of Resin(R) Open Source
 *
 * Each copy or derived work must preserve the copyright notice and this
 * notice unmodified.
 *
 * Resin Open Source is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * Resin Open Source 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, or any warranty
 * of NON-INFRINGEMENT.  See the GNU General Public License for more
 * details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Resin Open Source; if not, write to the
 *
 *   Free Software Foundation, Inc.
 *   59 Temple Place, Suite 330
 *   Boston, MA 02111-1307  USA
 *
 * @author Scott Ferguson
 */

package com.caucho.jms.connection;

import java.io.Serializable;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.MessageConsumer;
import javax.jms.MessageListener;
import javax.jms.Session;

import com.caucho.jms.message.MessageImpl;
import com.caucho.jms.message.ObjectMessageImpl;
import com.caucho.jms.message.TextMessageImpl;
import com.caucho.jms.queue.AbstractDestination;
import com.caucho.jms.queue.AbstractQueue;
import com.caucho.jms.queue.MessageCallback;
import com.caucho.jms.queue.MessageException;
import com.caucho.jms.queue.QueueEntry;
import com.caucho.jms.selector.Selector;
import com.caucho.jms.selector.SelectorParser;
import com.caucho.util.Alarm;
import com.caucho.util.CurrentTime;
import com.caucho.util.L10N;

/**
 * A basic message consumer.
 */
public class MessageConsumerImpl<E> implements MessageConsumer
{
  private static final Logger log
    = Logger.getLogger(MessageConsumerImpl.class.getName());
  private static final L10N L = new L10N(MessageConsumerImpl.class);

  private final Object _consumerLock = new Object();

  protected final JmsSession _session;

  private AbstractQueue<E> _queue;

  private MessageListener _messageListener;
  private ClassLoader _listenerClassLoader;

  private MessageConsumerCallback _messageCallback;

  private String _messageSelector;
  protected Selector _selector;
  private boolean _noLocal;
  private boolean _isAutoAcknowledge;

  private AtomicBoolean _isActive = new AtomicBoolean();
  private AtomicBoolean _isClosed = new AtomicBoolean();

  MessageConsumerImpl(JmsSession session,
                      AbstractQueue<E> queue,
                      String messageSelector,
                      boolean noLocal)
    throws JMSException
  {
    _session = session;
    _queue = queue;
    _messageSelector = messageSelector;

    if (_messageSelector != null) {
      SelectorParser parser = new SelectorParser();
      _selector = parser.parse(messageSelector);
    }
    _noLocal = noLocal;

    // _queue.addMessageAvailableListener(this);

    switch (_session.getAcknowledgeMode()) {
    case Session.AUTO_ACKNOWLEDGE:
    case Session.DUPS_OK_ACKNOWLEDGE:
      _isAutoAcknowledge = true;
      break;

    default:
      _isAutoAcknowledge = false;
      break;
    }
  }

  /**
   * Returns the destination
   */
  protected AbstractDestination<E> getDestination()
    throws JMSException
  {
    if (_isClosed.get() || _session.isClosed())
      throw new javax.jms.IllegalStateException(L.l("getDestination(): MessageConsumer is closed."));

    return _queue;
  }

  /**
   * Returns true if local messages are not sent.
   */
  public boolean getNoLocal()
    throws JMSException
  {
    if (_isClosed.get() || _session.isClosed())
      throw new javax.jms.IllegalStateException(L.l("getNoLocal(): MessageConsumer is closed."));

    return _noLocal;
  }

  /**
   * Returns the message listener
   */
  public MessageListener getMessageListener()
    throws JMSException
  {
    if (_isClosed.get() || _session.isClosed())
      throw new javax.jms.IllegalStateException(L.l("getNoLocal(): MessageConsumer is closed."));

    return _messageListener;
  }

  /**
   * Sets the message listener
   */
  @Override
  public void setMessageListener(MessageListener listener)
    throws JMSException
  {
    setMessageListener(listener, -1);
  }

  /**
   * Sets the message listener with a poll interval
   */
  public void setMessageListener(MessageListener listener, long pollInterval)
    throws JMSException
  {
    if (_isClosed.get() || _session.isClosed())
      throw new javax.jms.IllegalStateException(L.l("setMessageListener(): MessageConsumer is closed."));

    _messageListener = listener;
    _messageCallback = new MessageConsumerCallback(listener);

    _listenerClassLoader = Thread.currentThread().getContextClassLoader();

    // if Consumer has already been started then register the message Call back.
    if (isActive()) {
      addMessageCallback();
    }

  }

  /**
   * Returns the message consumer's selector.
   */
  public String getMessageSelector()
    throws JMSException
  {
    if (_isClosed.get() || _session.isClosed())
      throw new javax.jms.IllegalStateException(L.l("getMessageSelector(): MessageConsumer is closed."));

    return _messageSelector;
  }

  /**
   * Returns the parsed selector.
   */
  public Selector getSelector()
  {
    return _selector;
  }

  /**
   * Returns true if active
   */
  public boolean isActive()
    throws JMSException
  {
    if (_isClosed.get() || _session.isClosed())
      throw new javax.jms.IllegalStateException(L.l("isActive(): MessageConsumer is closed."));

    return _session.isActive() && ! _isClosed.get();
  }

  /**
   * Returns true if closed
   */
  public boolean isClosed()
  {
    return _isClosed.get() || _session.isClosed();
  }

  /**
   * Receives the next message, blocking until a message is available.
   */
  @Override
  public Message receive()
    throws JMSException
  {
    long timeout = Long.MAX_VALUE / 2;
    
    if (CurrentTime.isTest()) {
      timeout = 600000L;
    }
    
    return receiveImpl(timeout);
  }

  /**
   * Receives a message from the queue.
   */
  @Override
  public Message receiveNoWait()
    throws JMSException
  {
    return receiveImpl(0);
  }

  /**
   * Receives a message from the queue.
   */
  @Override
  public Message receive(long timeout)
    throws JMSException
  {
    Message msg = receiveImpl(timeout);

    if (msg != null && log.isLoggable(Level.FINE))
      log.fine(_queue + " receive message " + msg);

    return msg;
  }

  /**
   * Receives a message from the queue.
   */
  protected Message receiveImpl(long timeout)
    throws JMSException
  {
    if (isClosed())
      throw new javax.jms.IllegalStateException(L.l("receiveNoWait(): MessageConsumer is closed."));

    if (Long.MAX_VALUE / 2 < timeout || timeout < 0)
      timeout = Long.MAX_VALUE / 2;

    long now = CurrentTime.getCurrentTimeActual();
    long expireTime = timeout > 0 ? now + timeout : 0;

    while (_session.isActive()) {
      QueueEntry<E> entry
        = _queue.receiveEntry(expireTime, _isAutoAcknowledge, _selector);

      if (entry == null) {
        return null;
      }

      E payload = entry.getPayload();

      if (payload == null)
        return null;
      
      MessageImpl msg = null;

      if (payload instanceof MessageImpl) {
        msg = (MessageImpl) payload;
      }
      else if (payload instanceof String) {
        msg = new TextMessageImpl((String) payload);
        msg.setJMSMessageID(entry.getMsgId());
      }
      else {
        msg = new ObjectMessageImpl((Serializable) payload);
        msg.setJMSMessageID(entry.getMsgId());
      }

      msg.setReceive();
      
      /*if (_selector != null && ! _selector.isMatch(msg)) {
        _queue.acknowledge(msg.getJMSMessageID());
        continue;
      }*/

      //else {
      if (log.isLoggable(Level.FINE))
        log.fine(_queue + " receiving message " + msg);

      if (! _isAutoAcknowledge) {
        _session.addTransactedReceive(_queue, msg);
      }

      return msg;
      //}
    }

    return null;
  }

  /**
   * Notifies that a message is available.
   */
  public boolean notifyMessageAvailable()
  {
    synchronized (_consumerLock) {
      _consumerLock.notifyAll();
    }

    return _session.notifyMessageAvailable();
  }

  /**
   * Called with the session's thread to handle any messages
   */
  boolean handleMessage(MessageListener listener)
  {
    if (_messageListener != null)
      listener = _messageListener;

    if (listener == null)
      return false;

    MessageImpl msg = null;
    try {
      // MessageCallback<E> callback = _messageCallback;
      
      // XXX: not correct with new model

      // _queue.listen(callback);

      /*
      if (msg == null)
        System.out.println(_queue + " NOMESSAGE:");
      */

      if (msg != null) {
        if (log.isLoggable(Level.FINE)) {
          log.fine(_queue + " deliver " + msg + " to listener " + listener);
        }

        msg.setSession(_session);

        // XXX: ejb30/bb/mdb/activationconfig/queue/selectorauto/annotated/negativeTest1
        if (_selector == null || _selector.isMatch(msg)) {
          _session.addTransactedReceive(_queue, msg);

          Thread thread = Thread.currentThread();
          ClassLoader oldLoader = thread.getContextClassLoader();
          try {
            thread.setContextClassLoader(_listenerClassLoader);

            listener.onMessage(msg);
          } finally {
            thread.setContextClassLoader(oldLoader);
          }
        }

        if (_session.getTransacted())
          _session.commit();
        else
          msg.acknowledge();

        return true;
      }
    } catch (Exception e) {
      log.log(Level.WARNING, L.l("{0}: message listener '{1}' failed for message '{2}' with exception\n{3}",
                                 this, listener, msg, e.toString()),
              e);

      _queue.addListenerException(e);
    }

    return false;
  }
  
  public void addMessageCallback() 
  {
    MessageConsumerCallback callback = _messageCallback;

    if (callback != null) {
      boolean isAutoAcknowledge = _isAutoAcknowledge;
      
      _queue.addMessageCallback(callback, isAutoAcknowledge);
    }    
  }

  /**
   * Starts the consumer
   */
  public void start()
  {
    _isActive.set(true);
    addMessageCallback();
  }  
  

  /**
   * Stops the consumer.
   */
  public void stop()
    throws JMSException
  {
    _isActive.set(false);
    _queue.removeMessageCallback(_messageCallback);

    /*
    synchronized (_consumerLock) {
      _consumerLock.notifyAll();
    }
    */
  }

  /**
   * Closes the consumer.
   */
  @Override
  public void close()
    throws JMSException
  {
    if (_isClosed.getAndSet(true))
      return;

    if (_queue instanceof TemporaryQueueImpl) {    
      ((TemporaryQueueImpl)_queue).removeMessageConsumer();
    }
    
    // _queue.removeMessageAvailableListener(this);
    _session.removeConsumer(this);
  }

  @Override
  public String toString()
  {
    return getClass().getSimpleName() + "[" + _queue + "]";
  }

  class MessageConsumerCallback implements MessageCallback<E> {
    private final MessageListener _listener;
    private final ClassLoader _classLoader;
    
    MessageConsumerCallback(MessageListener listener)
    {
      _listener = listener;
      _classLoader = Thread.currentThread().getContextClassLoader();
    }
    
    public boolean isClosed()
    {
      return MessageConsumerImpl.this.isClosed() || ! _isActive.get();
    }

    @Override
    public void messageReceived(String msgId, E payload)
    {
      MessageImpl message = null;

      try {
        if (payload instanceof MessageImpl)
          message = (MessageImpl) payload;
        else if (payload instanceof String) {
          message = new TextMessageImpl((String) payload);
          message.setJMSMessageID(msgId);
        }
        else {
          message = new ObjectMessageImpl((Serializable) payload);
          message.setJMSMessageID(msgId);
        }

        if (_selector == null || _selector.isMatch(message)) {
          // XXX: only if XA
          //if (! _isAutoAcknowledge) {
          _session.addTransactedReceive(_queue, message);
          //}

          Thread thread = Thread.currentThread();
          ClassLoader oldLoader = thread.getContextClassLoader();
          try {
            thread.setContextClassLoader(_classLoader);

            _session.acquireListenSemaphore();
            
            _listener.onMessage(message);
          } finally {
            thread.setContextClassLoader(oldLoader);
            
            _session.releaseListenSemaphore();

            // XXX: commit/rollback?
            if (_session.getTransacted())
              _session.commit();
            else
              _session.acknowledge();
          }
        }
      } catch (JMSException e) {
        throw new MessageException(e);
      }
    }
  }
}
