/** 
 * 
 * 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.service.impl;

import org.activemq.service.QueueList;
import org.activemq.service.QueueListEntry;
import org.activemq.util.FastInputStream;
import org.activemq.util.FastOutputStream;
import org.activemq.util.JMSExceptionHelper;

import javax.jms.JMSException;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.Serializable;

/**
 * A base class which is useful for implementation inheritence when implementing
 * a persistent QueueList
 *
 * @version $Revision: 1.1.1.1 $
 */
public abstract class QueueListSupport implements QueueList {
    protected static final Long HEAD_KEY = new Long(0);

    public static class Header implements Serializable {
        private static final long serialVersionUID = 64734383295040L;

        public Long headKey;
        public Long tailKey;
        public long lastKeyValue;
        public int size;

        public byte[] asBytes() throws IOException {
            ByteArrayOutputStream buffer = new ByteArrayOutputStream();
            DataOutputStream out = new DataOutputStream(buffer);
            out.writeLong(unwrapLong(headKey));
            out.writeLong(unwrapLong(tailKey));
            out.writeLong(lastKeyValue);
            out.writeInt(size);
            return buffer.toByteArray();
        }

        public void fromBytes(byte[] data) throws IOException {
            DataInputStream in = new DataInputStream(new ByteArrayInputStream(data));
            this.headKey = wrapLong(in.readLong());
            this.tailKey = wrapLong(in.readLong());
            this.lastKeyValue = in.readLong();
            this.size = in.readInt();
        }
    }

    public static class Node implements Serializable, QueueListEntry {
        private static final long serialVersionUID = 4609474001468609536L;

        public Long previousKey;
        public Long nextKey;
        public Object value;

        // not stored, but cached when read from the table
        public transient Long key;

        public Object getElement() {
            return value;
        }

        public byte[] asBytes() throws IOException {
            FastOutputStream buffer = new FastOutputStream();
            DataOutputStream out = new DataOutputStream(buffer);
            out.writeLong(unwrapLong(previousKey));
            out.writeLong(unwrapLong(nextKey));
            out.writeUTF((String) value);
            return buffer.toByteArray();
        }

        public void fromBytes(byte[] data) throws IOException {
            DataInputStream in = new DataInputStream(new FastInputStream(data));
            this.previousKey = wrapLong(in.readLong());
            this.nextKey = wrapLong(in.readLong());
            this.value = in.readUTF();
        }
    }

    public Object getFirst() throws JMSException {
        try {
            Long key = getHeader().headKey;
            if (key != null) {
                Node node = getNode(key);
                if (node != null) {
                    return node.getElement();
                }
            }
            return null;
        }
        catch (IOException e) {
            throw JMSExceptionHelper.newJMSException("Failed to read from table: " + e, e);
        }
    }

    public Object getLast() throws JMSException {
        try {
            Long key = getHeader().tailKey;
            if (key != null) {
                Node node = getNode(key);
                if (node != null) {
                    return node.getElement();
                }
            }
            return null;
        }
        catch (IOException e) {
            throw JMSExceptionHelper.newJMSException("Failed to read from table: " + e, e);
        }
    }

    public Object removeFirst() throws JMSException {
        try {
            Header header = getHeader();
            Long key = header.headKey;
            if (key != null) {
                Node node = getNode(key);
                if (node != null) {
                    doRemoveNode(node);
                    header.headKey = node.nextKey;
                    --header.size;
                    updateHeader(header);
                    return node.getElement();
                }
            }
            return null;
        }
        catch (IOException e) {
            throw JMSExceptionHelper.newJMSException("Failed to write to table: " + e, e);
        }
    }

    public Object removeLast() throws JMSException {
        try {
            Header header = getHeader();
            Long key = header.tailKey;
            if (key != null) {
                Node node = getNode(key);
                if (node != null) {
                    doRemoveNode(node);
                    header.tailKey = node.previousKey;
                    --header.size;
                    updateHeader(header);
                    return node.getElement();
                }
            }
            return null;
        }
        catch (IOException e) {
            throw JMSExceptionHelper.newJMSException("Failed to write to table: " + e, e);
        }
    }

    public QueueListEntry addFirst(Object value) throws JMSException {
        try {
            Header header = getHeader();
            Node node = createNode();
            node.value = value;
            Long nextKey = header.headKey;
            node.nextKey = nextKey;
            Long key = createKey(header);
            node.key = key;
            updateNode(node);
            updateNextNode(nextKey, key);
            header.headKey = key;
            if (header.tailKey == null) {
                header.tailKey = key;
            }
            header.size++;
            updateHeader(header);
            return node;
        }
        catch (IOException e) {
            throw JMSExceptionHelper.newJMSException("Failed to write to table: " + e, e);
        }
    }

    public QueueListEntry addLast(Object value) throws JMSException {
        try {
            Header header = getHeader();
            return doAddLast(value, header);
        }
        catch (IOException e) {
            throw JMSExceptionHelper.newJMSException("Failed to write to table: " + e, e);
        }
    }

    public boolean contains(Object value) throws JMSException {
        return indexOf(value) != -1;
    }

    public int size() throws JMSException {
        try {
            return getHeader().size;
        }
        catch (IOException e) {
            throw JMSExceptionHelper.newJMSException("Failed to read from table: " + e, e);
        }
    }

    public boolean isEmpty() throws JMSException {
        int size = size();
        return size == 0;
    }

    public QueueListEntry add(Object value) throws JMSException {
        return addLast(value);
    }

    public Object get(int index) throws JMSException {
        try {
            Node node = getNode(index);
            if (node != null) {
                return node.value;
            }
            return null;
        }
        catch (IOException e) {
            throw JMSExceptionHelper.newJMSException("Failed to read from table: " + e, e);
        }
    }

    public Object set(int index, Object element) throws JMSException {
        try {
            Node node = getNode(index);
            if (node != null) {
                Object previous = node.value;
                node.value = element;
                updateNode(node);
                return previous;
            }
            return null;
        }
        catch (IOException e) {
            throw JMSExceptionHelper.newJMSException("Failed to write to table: " + e, e);
        }
    }

    public void add(int index, Object element) throws JMSException {
        if (index == 0) {
            addFirst(element);
        }
        else {
            try {
                Header header = getHeader();
                Node nextNode = getNode(header, index);
                doAddBefore(header, nextNode, element);
            }
            catch (IOException e) {
                throw JMSExceptionHelper.newJMSException("Failed to write to table: " + e, e);
            }
        }
    }

    public Object remove(int index) throws JMSException {
        try {
            Node node = getNode(index);
            if (node != null) {
                removeNode(node);
            }
            return null;
        }
        catch (IOException e) {
            throw JMSExceptionHelper.newJMSException("Failed to write to table: " + e, e);
        }
    }

    public int indexOf(Object value) throws JMSException {
        try {
            Header header = getHeader();
            Long key = header.headKey;
            for (int i = 0; key != null; i++) {
                Node node = getNode(key);
                if (node == null) {
                    break;
                }
                if (value == node.value || (value != null && value.equals(node.value))) {
                    return i;
                }
                key = node.nextKey;
            }
            return -1;
        }
        catch (IOException e) {
            throw JMSExceptionHelper.newJMSException("Failed to read from table: " + e, e);
        }
    }

    public int lastIndexOf(Object value) throws JMSException {
        try {
            Header header = getHeader();
            Long key = header.tailKey;
            for (int i = header.size - 1; key != null; i--) {
                Node node = getNode(key);
                if (node == null) {
                    break;
                }
                if (value == node.value || (value != null && value.equals(node.value))) {
                    return i;
                }
                key = node.previousKey;
            }
            return -1;
        }
        catch (IOException e) {
            throw JMSExceptionHelper.newJMSException("Failed to read from table: " + e, e);
        }
    }

    public QueueListEntry getFirstEntry() throws JMSException {
        try {
            return getNode(getHeader().headKey);
        }
        catch (IOException e) {
            throw JMSExceptionHelper.newJMSException("Failed to read from table: " + e, e);
        }
    }

    public QueueListEntry getLastEntry() throws JMSException {
        try {
            return getNode(getHeader().tailKey);
        }
        catch (IOException e) {
            throw JMSExceptionHelper.newJMSException("Failed to read from table: " + e, e);
        }
    }

    public QueueListEntry getNextEntry(QueueListEntry entry) throws JMSException {
        Node node = (Node) entry;
        try {
            return getNode(node.nextKey);
        }
        catch (IOException e) {
            throw JMSExceptionHelper.newJMSException("Failed to read from table: " + e, e);
        }
    }

    public QueueListEntry getPrevEntry(QueueListEntry entry) throws JMSException {
        Node node = (Node) entry;
        try {
            return getNode(node.previousKey);
        }
        catch (IOException e) {
            throw JMSExceptionHelper.newJMSException("Failed to read from table: " + e, e);
        }
    }

    public QueueListEntry addBefore(Object value, QueueListEntry entry) throws JMSException {
        try {
            return doAddBefore(getHeader(), (Node) entry, value);
        }
        catch (IOException e) {
            throw JMSExceptionHelper.newJMSException("Failed to write to table: " + e, e);
        }
    }

    public void remove(QueueListEntry entry) throws JMSException {
        try {
            removeNode((Node) entry);
        }
        catch (IOException e) {
            throw JMSExceptionHelper.newJMSException("Failed to write to table: " + e, e);
        }
    }

    public Object[] toArray() throws JMSException {
        try {
            Header header = getHeader();
            int size = header.size;
            if (size == 0) {
                return EMPTY_ARRAY;
            }
            Long key = header.headKey;
            Object[] answer = new Object[size];
            for (int i = 0; i < size && key != null; i++) {
                Node node = getNode(key);
                if (node != null) {
                    answer[i] = node.value;
                    key = node.nextKey;
                }
            }
            return answer;
        }
        catch (IOException e) {
            throw JMSExceptionHelper.newJMSException("Failed to write to table: " + e, e);
        }
    }

    public void rotate() throws JMSException {
        // TODO could tune this by just swizzling pointers
        Object obj = removeFirst();
        if (obj != null) {
            addLast(obj);
        }
    }

    protected Long createKey(Header header) throws IOException, JMSException {
        long value = header.lastKeyValue;
        while (true) {
            if (value == Long.MAX_VALUE) {
                value = 1;
            }
            else {
                value++;
            }
            Long key = new Long(value);
            if (getNode(key) == null) {
                header.lastKeyValue = value;
                return key;
            }
        }
    }

    protected boolean removeNode(Node node) throws IOException, JMSException {
        Header header = null;
        boolean updatedPrevious = false;
        if (node.previousKey != null) {
            // lets point the previous node to our next node
            Node previousNode = getNode(node.previousKey);
            if (previousNode != null) {
                previousNode.nextKey = node.nextKey;
                updateNode(previousNode);
                updatedPrevious = true;
            }
        }
        if (!updatedPrevious) {
            // lets update the head record
            header = getHeader();
            header.headKey = node.nextKey;
        }

        boolean updatedNext = false;
        if (node.nextKey != null) {
            // lets point the next node to our previous node
            Node nextNode = getNode(node.nextKey);
            if (nextNode != null) {
                nextNode.previousKey = node.previousKey;
                updateNode(nextNode);
                updatedNext = true;
            }
        }
        if (!updatedNext) {
            // lets update the tail record
            header = getHeader();
            header.tailKey = node.previousKey;
        }
        return true;
    }

    /**
     * Looks up the header object, lazily creating one if the current table is empty
     *
     * @return
     * @throws java.io.IOException
     */
    protected abstract Header getHeader() throws IOException, JMSException;

    /**
     * Writes the header back to disk after its been changed
     *
     * @param header
     * @throws java.io.IOException
     */
    protected abstract void updateHeader(Header header) throws IOException, JMSException;

    /**
     * Updates the node
     *
     * @param node
     * @throws java.io.IOException
     */
    protected abstract void updateNode(Node node) throws IOException, JMSException;

    protected abstract Node getNode(Long key) throws IOException, JMSException;

    protected Node getNode(int index) throws IOException, JMSException {
        Header header = getHeader();
        return getNode(header, index);
    }

    protected Node getNode(Header header, int index) throws IOException, JMSException {
        Node node = null;
        int middle = header.size / 2;
        if (index > middle) {
            // lets navigate backwards
            Long key = header.tailKey;
            for (int i = header.size; i > index && key != null; i--) {
                node = getNode(key);
                if (node != null) {
                    key = node.previousKey;
                }
            }
        }
        else {
            Long key = header.headKey;
            for (int i = 0; i <= index && key != null; i++) {
                node = getNode(key);
                if (node != null) {
                    key = node.nextKey;
                }
            }
        }
        return node;
    }

    protected Node doAddLast(Object value, Header header) throws IOException, JMSException {
        Node node = createNode();
        Long key = createKey(header);
        node.key = key;
        node.value = value;
        Long previousKey = header.tailKey;
        node.previousKey = previousKey;
        updateNode(node);
        updatePreviousNode(previousKey, key);
        header.tailKey = key;
        if (header.headKey == null) {
            header.headKey = key;
        }
        header.size++;
        updateHeader(header);
        return node;
    }

    protected void updateNextNode(Long nextKey, Long key) throws IOException, JMSException {
        if (nextKey != null) {
            Node nextNode = getNode(nextKey);
            if (nextNode == null) {
                throw new IOException("Missing node for key: " + nextKey);
            }
            nextNode.previousKey = key;
            updateNode(nextNode);
        }
    }

    protected void updatePreviousNode(Long previousKey, Long key) throws IOException, JMSException {
        if (previousKey != null) {
            Node previousNode = getNode(previousKey);
            if (previousNode == null) {
                throw new IOException("Missing previous node for key: " + previousKey);
            }
            previousNode.nextKey = key;
            updateNode(previousNode);
        }
    }

    protected Node doAddBefore(Header header, Node nextNode, Object element) throws JMSException, IOException {
        if (nextNode == null) {
            return doAddLast(element, header);
        }
        else {
            // lets add before this nextNode
            Long key = createKey(header);
            Node node = createNode();
            node.value = element;
            node.key = key;
            Long previousKey = nextNode.previousKey;
            node.previousKey = previousKey;
            node.nextKey = nextNode.key;
            nextNode.previousKey = key;
            header.size++;

            updateNode(node);
            updateNode(nextNode);
            updatePreviousNode(previousKey, key);
            updateHeader(header);
            return node;
        }
    }

    protected abstract void doRemoveNode(Node node) throws IOException, JMSException;

    protected static Long wrapLong(long value) {
        // lets use 0 for null
        if (value == 0) {
            return null;
        }
        // TODO use a cache?
        return new Long(value);
    }

    protected static long unwrapLong(Long key) {
        if (key != null) {
            return key.longValue();
        }
        // lets use 0 for null
        return 0;
    }

    protected Node createNode() {
        return new Node();
    }
}
