/** 
 * 
 * Copyright 2004 Protique Ltd
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); 
 * you may not use this file except in compliance with the License. 
 * You may obtain a copy of the License at 
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, 
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
 * See the License for the specific language governing permissions and 
 * limitations under the License. 
 * 
 **/

package org.activemq.io.util;
import java.io.File;
import java.io.IOException;
import java.util.List;
import javax.jms.JMSException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.activemq.io.WireFormat;
import org.activemq.io.impl.DefaultWireFormat;
import org.activemq.message.ActiveMQMessage;
import EDU.oswego.cs.dl.util.concurrent.SynchronizedInt;

/**
 * Implements a controlled thread safe queue, with ActiveMQMessages being spooled to disk for reading asynchronously.
 */
public class SpooledBoundedActiveMQMessageQueue {
    private String name;
    private DataContainer container;
    private WireFormat wireFormat;
    private long maxDataLength;
    private boolean closed;
    private boolean stopped;
    private SynchronizedInt size = new SynchronizedInt(0);
    private Object inLock = new Object();
    private Object outLock = new Object();
    private static int WAIT_TIMEOUT = 250;
    private static final Log log = LogFactory.getLog(SpooledBoundedActiveMQMessageQueue.class);

    /**
     * Constructor for SpooledBoundedActiveMQMessageQueue
     * 
     * @param dir
     * @param name
     * @param maxDataLength
     * @param maxBlockSize
     * @throws IOException
     */
    public SpooledBoundedActiveMQMessageQueue(File dir, String name, long maxDataLength, int maxBlockSize) throws IOException {
        //ensure name can be used as a file name
        char[] chars = name.toCharArray();
        for (int i = 0;i < chars.length;i++) {
            if (!Character.isLetterOrDigit(chars[i])) {
                chars[i] = '_';
            }
        }
        this.name = new String(chars);
        this.maxDataLength = maxDataLength;
        this.wireFormat = new DefaultWireFormat();
        this.container = new DataContainer(dir, this.name, maxBlockSize);
        //as the DataContainer is temporary, clean-up any old files
        this.container.deleteAll();
    }

    /**
     * Constructor for SpooledBoundedActiveMQMessageQueue
     * 
     * @param dir
     * @param name
     * @throws IOException
     */
    public SpooledBoundedActiveMQMessageQueue(File dir, String name) throws IOException {
        this(dir, name, 1024 * 1024 * 64, 8192);
    }

    /**
     * Place a ActiveMQMessage at the head of the Queue
     * 
     * @param packet
     * @throws JMSException
     */
    public void enqueue(ActiveMQMessage packet) throws JMSException {
        if (!isFull()) {
            enqueueNoBlock(packet);
        }
        else {
            synchronized (inLock) {
                try {
                    while (isFull()) {
                        inLock.wait(WAIT_TIMEOUT);
                    }
                }
                catch (InterruptedException ie) {
                }
            }
            enqueueNoBlock(packet);
        }
    }

    /**
     * Enqueue a ActiveMQMessage without checking usage limits
     * 
     * @param packet
     * @throws JMSException
     */
    public void enqueueNoBlock(ActiveMQMessage packet) throws JMSException {
        byte[] data;
        try {
            data = wireFormat.toBytes(packet);
            size.increment();
            container.write(data);
        }
        catch (IOException e) {
            JMSException jmsEx = new JMSException("enqueNoBlock failed: " + e.getMessage());
            jmsEx.setLinkedException(e);
            throw jmsEx;
        }
        synchronized (outLock) {
            outLock.notify();
        }
    }

    /**
     * @return the first dequeued ActiveMQMessage or blocks until one is available
     * @throws JMSException
     * @throws InterruptedException
     */
    public ActiveMQMessage dequeue() throws JMSException, InterruptedException {
        ActiveMQMessage result = null;
        synchronized (outLock) {
            while ((result = dequeueNoWait()) == null) {
                outLock.wait(WAIT_TIMEOUT);
            }
        }
        return result;
    }

    /**
     * @return the ActiveMQMessage from the head of the Queue or null if the Queue is empty
     * @param timeInMillis maximum time to wait to dequeue a ActiveMQMessage
     * @throws JMSException
     * @throws InterruptedException
     */
    public ActiveMQMessage dequeue(long timeInMillis) throws JMSException, InterruptedException {
        ActiveMQMessage result = dequeueNoWait();
        if (result == null) {
            synchronized (outLock) {
                outLock.wait(timeInMillis);
                result = dequeueNoWait();
            }
        }
        return result;
    }

    /**
     * @return the ActiveMQMessage from the head of the Queue or null if the Queue is empty
     * @throws JMSException
     * @throws InterruptedException
     */
    public ActiveMQMessage dequeueNoWait() throws JMSException, InterruptedException {
        ActiveMQMessage result = null;
        if (stopped) {
            synchronized (outLock) {
                while (stopped && !closed) {
                    outLock.wait(WAIT_TIMEOUT);
                }
            }
        }
        byte[] data;
        try {
            data = container.read();
            if (data != null) {
                result = (ActiveMQMessage)wireFormat.fromBytes(data);
                size.decrement();
            }
        }
        catch (IOException e) {
            JMSException jmsEx = new JMSException("fromBytes failed");
            jmsEx.setLinkedException(e);
            throw jmsEx;
        }
        if (result != null && !isFull()) {
            synchronized (inLock) {
                inLock.notify();
            }
        }
        return result;
    }

    /**
     * @return true if this queue has reached it's data length limit
     */
    public boolean isFull() {
        return container.length() >= maxDataLength;
    }

    /**
     * close this queue
     */
    public void close() {
        try {
            closed = true;
            container.close();
        }
        catch (IOException ioe) {
            log.warn("Couldn't close queue", ioe);
        }
    }

    /**
     * @return the name of this BoundedActiveMQMessageQueue
     */
    public String getName() {
        return name;
    }

    /**
     * @return number of ActiveMQMessages held by this queue
     */
    public int size() {
        return size.get();
    }

    /**
     * @return true if the queue is enabled for dequeing (default = true)
     */
    public boolean isStarted() {
        return stopped == false;
    }

    /**
     * disable dequeueing
     */
    public void stop() {
        synchronized (outLock) {
            stopped = true;
        }
    }

    /**
     * enable dequeueing
     */
    public void start() {
        stopped = false;
        synchronized (outLock) {
            outLock.notifyAll();
        }
        synchronized (inLock) {
            inLock.notifyAll();
        }
    }

    /**
     * @return true if this queue is empty
     */
    public boolean isEmpty() {
        return size.get() == 0;
    }

    /**
     * clear the queue
     */
    public void clear() {
    }

    /**
     * @return a copy of the contents
     */
    public List getContents() {
        return null;
    }
}