/* Glazed Lists                                                 (c) 2003-2006 */
/* http://publicobject.com/glazedlists/                      publicobject.com,*/
/*                                                     O'Dell Engineering Ltd.*/
package ca.odell.glazedlists.impl.io;

import ca.odell.glazedlists.EventList;
import ca.odell.glazedlists.event.ListEvent;
import ca.odell.glazedlists.io.ByteCoder;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

/**
 * An utility interface for converting Objects to bytes for storage or network
 * transport.
 *
 * @author <a href="mailto:jesse@swank.ca">Jesse Wilson</a>
 */
public class ListEventToBytes {
    
    /** the virtual event type */
    private static final int CLEAR = -1;
    
    /**
     * Convert the specified ListEvent to bytes.
     */
    public static Bufferlo toBytes(ListEvent listEvent, ByteCoder byteCoder) throws IOException {
        // populate the list of parts
        List parts = new ArrayList();
        while(listEvent.next()) {
            int index = listEvent.getIndex();
            int type = listEvent.getType();
            Object value = null;
            if(type == ListEvent.INSERT || type == ListEvent.UPDATE) value = listEvent.getSourceList().get(index);
            parts.add(new ListEventPart(index, type, value));
        }
        
        return partsToBytes(parts, byteCoder);
    }
    
    /**
     * Convert the List to a ListEvent. This is for snapshots or compressions.
     */
    public static Bufferlo toBytes(EventList list, ByteCoder byteCoder) throws IOException {
        List parts = new ArrayList();
        
        // start with a clear
        parts.add(new ListEventPart(-1, ListEventToBytes.CLEAR, null));
        
        // add all values as adds
        for(int i = 0; i < list.size(); i++) {
            parts.add(new ListEventPart(i, ListEvent.INSERT, list.get(i)));
        }
        
        // get the whole list
        return partsToBytes(parts, byteCoder);
    }
    
    /**
     * Apply the specified list event to the specified target list. The write lock
     * for this list must already be acquired if the list is shared between threads.
     */
    public static void toListEvent(Bufferlo listEvent, EventList target, ByteCoder byteCoder) throws IOException {
        List parts = bytesToParts(listEvent, byteCoder);
        for(Iterator i = parts.iterator(); i.hasNext(); ) {
            ListEventPart part = (ListEventPart)i.next();
            if(part.isDelete()) {
                target.remove(part.getIndex());
            } else if(part.isUpdate()) {
                target.set(part.getIndex(), part.getValue());
            } else if(part.isInsert()) {
                target.add(part.getIndex(), part.getValue());
            } else if(part.isClear()) {
                target.clear();
            }
        }
    }
        
    /**
     * Encode the parts into bytes.
     */
    private static Bufferlo partsToBytes(List parts, ByteCoder delegate) throws IOException {
        // prepare the result
        Bufferlo partsAsBytes = new Bufferlo();
        DataOutputStream dataOut = new DataOutputStream(partsAsBytes.getOutputStream());
        
        // convert each part in sequence
        for(int i = 0; i < parts.size(); i++) {
            ListEventPart part = (ListEventPart)parts.get(i);
            
            // write the index of this part
            dataOut.writeInt(i);
            
            // write the type
            dataOut.writeInt(part.getType());

            // write the index of the change
            if(part.hasIndex()) dataOut.writeInt(part.getIndex());
            
            // write the value
            if(part.hasValue()) {
                Bufferlo valueBuffer = new Bufferlo();
                delegate.encode(part.getValue(), valueBuffer.getOutputStream());
                dataOut.writeInt(valueBuffer.length());
                dataOut.flush();
                partsAsBytes.append(valueBuffer);
            }
        }
        
        // that was easy
        return partsAsBytes;
    }
    
    /**
     * Decode the bytes into parts.
     */
    private static List bytesToParts(Bufferlo partsAsBytes, ByteCoder delegate) throws IOException {
        // prepare the result
        List parts = new ArrayList();

        // read till the end of the file
        DataInputStream dataIn = new DataInputStream(partsAsBytes.getInputStream());
        
        // convert each part in sequence
        while(partsAsBytes.length() > 0) {
            ListEventPart currentPart = new ListEventPart();
            
            // read the index of this part
            int expectedPartIndex = parts.size();
            int partIndex = dataIn.readInt();
            if(partIndex != expectedPartIndex) throw new IOException("Expected " + expectedPartIndex + " but found " + partIndex);
            
            // read in the type of this part
            currentPart.setType(dataIn.readInt());
            
            // read in the index of this change
            if(currentPart.hasIndex()) {
                currentPart.setIndex(dataIn.readInt());
            }
            
            // read the value
            if(currentPart.hasValue()) {
                int valueLength = dataIn.readInt();
                Bufferlo valueBuffer = partsAsBytes.consume(valueLength);
                Object value = delegate.decode(valueBuffer.getInputStream());
                currentPart.setValue(value);
            }
            
            // we've completed one part
            parts.add(currentPart);
        }
        
        // that was easy
        return parts;
    }

    /**
     * A part of a ListEvent that is byte codable.
     */
    static class ListEventPart {
        
        /** the changed index */
        private int index = -1;
        
        /** the type of change, INSERT, UPDATE, DELETE or CLEAR */
        private int type = ListEventToBytes.CLEAR;
        
        /** the inserted or updated value */
        private Object value = null;
        
        /**
         * Create a new ByteCodableListEventBlock.
         */
        public ListEventPart(int index, int type, Object value) {
            this.index = index;
            this.type = type;
            this.value = value;
        }
        
        /**
         * Create a new empty ByteCodableListEventBlock.
         */
        public ListEventPart() {
        }
        
        public int getIndex() {
            return index;
        }
        public void setIndex(int index) {
            this.index = index;
        }

        public Object getValue() {
            return value;
        }
        public void setValue(Object value) {
            this.value = value;
        }

        public int getType() {
            return type;
        }
        public void setType(int type) {
            this.type = type;
        }

        public boolean isDelete() {
            return (type == ListEvent.DELETE);
        }
        public boolean isUpdate() {
            return (type == ListEvent.UPDATE);
        }
        public boolean isInsert() {
            return (type == ListEvent.INSERT);
        }
        public boolean isClear() {
            return (type == ListEventToBytes.CLEAR);
        }
        public boolean hasIndex() {
            return (isUpdate() || isInsert() || isDelete());
        }
        public boolean hasValue() {
            return (isUpdate() || isInsert());
        }
    }
}
