package anastore.util;

import java.io.ObjectStreamClass;
import java.io.ObjectStreamField;
import java.util.LinkedList;
import java.util.List;

/**
 * A class for tracking class descriptor expectations in object
 * streams.  This structure is constructed by both the output stream
 * and the input stream as they process the stream.  Assuming that
 * both sides are following the same expectation rules and have the
 * same class definitions, both sides will build the structure in
 * exactly the same way.  This allows for the bulk of class
 * descriptors to be encoded by a single byte indicating that the
 * next descriptor is what both sides would expect.
 *<p>
 * This class's implementation is closely tied to
 * FastObjectOutputStream and FastObjectInputStream, thus it is
 * package protected.
 */
class StreamExpectations
{
    private static final boolean LOG = false;
    private static final Log log = new Log(LOG);

    static final byte CODE_EXPECTED       = -1;
    static final byte CODE_SUBCLASS       = -2;
    static final byte CODE_SUBCLASS_REF   = -3;
    static final byte CODE_SKIP           = -4;
    static final byte CODE_UNEXPECTED     = -5;
    static final byte CODE_UNEXPECTED_REF = -6;

    /**
     * The current list of expectations.  The head of the list is the
     * next expectation.
     */
    private List<ObjectStreamClass> _expectations =
        new LinkedList<ObjectStreamClass>();

    /**
     * Retrieve the next expectation.  Return null if there are no
     * expectations.
     */
    public ObjectStreamClass takeExpectation()
    {
        if (_expectations.isEmpty())
            return null;
        return _expectations.remove(0);
    }

    /**
     * Return true if the given descriptor can be expected at some
     * point in the future.
     */
    public boolean canExpect(ObjectStreamClass desc)
    {
        return _expectations.contains(desc);
    }

    /**
     * Fast forward to just past the expectation of the given
     * descriptor, returning the number of expectations that had to be
     * skipped over.
     */
    public int skipOver(ObjectStreamClass desc)
        throws IllegalArgumentException
    {
        int skip = _expectations.indexOf(desc) + 1;
        if (skip == -1)
            throw new IllegalArgumentException("Descriptor not expected");
        for (int i = 0; i < skip; ++i)
            _expectations.remove(0);
        return skip;
    }

    /**
     * Skip the given number of expectations and take the next
     * expectation.
     */
    public ObjectStreamClass skipBy(int skip)
    {
        if (skip >= _expectations.size())
            throw new IllegalArgumentException
                ("Cannot skip " + skip + " expectations");
        for (int i = 0; i < skip; ++i)
            _expectations.remove(0);
        return takeExpectation();
    }

    /**
     * Clear the list of expectations.
     */
    public void clear()
    {
        _expectations.clear();
    }

    /**
     * Update the expectations list to include the expectations for
     * the given descriptor.  This should mimic the sequence of
     * descriptors that will be written by the Java serialization
     * protocol following the given descriptor as closely as possible.
     * Note, however, that this should not be relied on for
     * correctness.
     */
    public void addExpectations(ObjectStreamClass desc)
    {
        int pos = 0;
        ObjectStreamClass sc = desc;
        while (true) {
            sc = ObjectStreamClass.lookup(sc.forClass().getSuperclass());
            if (sc == null)
                break;
            if (LOG)
                log.println("Adding expectation " + sc.getName());
            _expectations.add(pos, sc);
            ++pos;
        }
        for (ObjectStreamField f : desc.getFields()) {
            // Primitive and string data is encoded specially
            if (f.isPrimitive() || f.getType() == String.class)
                continue;
            if (LOG)
                log.println("Adding expectation " + f.getType().getName());
            _expectations.add(pos, ObjectStreamClass.lookup(f.getType()));
            ++pos;
        }
    }

    /**
     * Check if the expectations of two StreamExpectations objects are
     * the same.  Only for testing.
     */
    @Override
    public boolean equals(Object o)
    {
        if (!(o instanceof StreamExpectations))
            return false;
        StreamExpectations s2 = (StreamExpectations)o;
        return _expectations.equals(s2._expectations);
    }

    @Override
    public String toString()
    {
        StringBuilder out = new StringBuilder("[");
        boolean first = true;
        for (ObjectStreamClass exp : _expectations) {
            if (!first)
                out.append(", ");
            first = false;
            out.append(exp.getName());
        }
        out.append(']');
        return out.toString();
    }
}
