/* -*- Mode: java; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
 *
 * ***** BEGIN LICENSE BLOCK *****
 * Version: MPL 1.1/GPL 2.0
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (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.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is Rhino code, released
 * May 6, 1999.
 *
 * The Initial Developer of the Original Code is
 * Netscape Communications Corporation.
 * Portions created by the Initial Developer are Copyright (C) 1997-2000
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *   Igor Bukanov
 *
 * Alternatively, the contents of this file may be used under the terms of
 * the GNU General Public License Version 2 or later (the "GPL"), in which
 * case the provisions of the GPL are applicable instead of those above. If
 * you wish to allow use of your version of this file only under the terms of
 * the GPL and not to allow others to use your version of this file under the
 * MPL, indicate your decision by deleting the provisions above and replacing
 * them with the notice and other provisions required by the GPL. If you do
 * not delete the provisions above, a recipient may use your version of this
 * file under either the MPL or the GPL.
 *
 * ***** END LICENSE BLOCK ***** */

package org.m0zilla.javascript;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

/**
Implementation of resizable array with focus on minimizing memory usage by storing few initial array elements in object fields. Can also be used as a stack.
*/

public class ObjArray implements Serializable
{
    static final long serialVersionUID = 4174889037736658296L;

    public ObjArray() { }

    public final boolean isSealed()
    {
        return sealed;
    }

    public final void seal()
    {
        sealed = true;
    }

    public final boolean isEmpty()
    {
        return size == 0;
    }

    public final int size()
    {
        return size;
    }

    public final void setSize(int newSize)
    {
        if (newSize < 0) throw new IllegalArgumentException();
        if (sealed) throw onSeledMutation();
        int N = size;
        if (newSize < N) {
            for (int i = newSize; i != N; ++i) {
                setImpl(i, null);
            }
        } else if (newSize > N) {
            if (newSize > FIELDS_STORE_SIZE) {
                ensureCapacity(newSize);
            }
        }
        size = newSize;
    }

    public final Object get(int index)
    {
        if (!(0 <= index && index < size)) throw onInvalidIndex(index, size);
        return getImpl(index);
    }

    public final void set(int index, Object value)
    {
        if (!(0 <= index && index < size)) throw onInvalidIndex(index, size);
        if (sealed) throw onSeledMutation();
        setImpl(index, value);
    }

    private Object getImpl(int index)
    {
        switch (index) {
            case 0: return f0;
            case 1: return f1;
            case 2: return f2;
            case 3: return f3;
            case 4: return f4;
        }
        return data[index - FIELDS_STORE_SIZE];
    }

    private void setImpl(int index, Object value)
    {
        switch (index) {
            case 0: f0 = value; break;
            case 1: f1 = value; break;
            case 2: f2 = value; break;
            case 3: f3 = value; break;
            case 4: f4 = value; break;
            default: data[index - FIELDS_STORE_SIZE] = value;
        }

    }

    public int indexOf(Object obj)
    {
        int N = size;
        for (int i = 0; i != N; ++i) {
            Object current = getImpl(i);
            if (current == obj || (current != null && current.equals(obj))) {
                return i;
            }
        }
        return -1;
    }

    public int lastIndexOf(Object obj)
    {
        for (int i = size; i != 0;) {
            --i;
            Object current = getImpl(i);
            if (current == obj || (current != null && current.equals(obj))) {
                return i;
            }
        }
        return -1;
    }

    public final Object peek()
    {
        int N = size;
        if (N == 0) throw onEmptyStackTopRead();
        return getImpl(N - 1);
    }

    public final Object pop()
    {
        if (sealed) throw onSeledMutation();
        int N = size;
        --N;
        Object top;
        switch (N) {
            case -1: throw onEmptyStackTopRead();
            case 0: top = f0; f0 = null; break;
            case 1: top = f1; f1 = null; break;
            case 2: top = f2; f2 = null; break;
            case 3: top = f3; f3 = null; break;
            case 4: top = f4; f4 = null; break;
            default:
                top = data[N - FIELDS_STORE_SIZE];
                data[N - FIELDS_STORE_SIZE] = null;
        }
        size = N;
        return top;
    }

    public final void push(Object value)
    {
        add(value);
    }

    public final void add(Object value)
    {
        if (sealed) throw onSeledMutation();
        int N = size;
        if (N >= FIELDS_STORE_SIZE) {
            ensureCapacity(N + 1);
        }
        size = N + 1;
        setImpl(N, value);
    }

    public final void add(int index, Object value)
    {
        int N = size;
        if (!(0 <= index && index <= N)) throw onInvalidIndex(index, N + 1);
        if (sealed) throw onSeledMutation();
        Object tmp;
        switch (index) {
            case 0:
                if (N == 0) { f0 = value; break; }
                tmp = f0; f0 = value; value = tmp;
            case 1:
                if (N == 1) { f1 = value; break; }
                tmp = f1; f1 = value; value = tmp;
            case 2:
                if (N == 2) { f2 = value; break; }
                tmp = f2; f2 = value; value = tmp;
            case 3:
                if (N == 3) { f3 = value; break; }
                tmp = f3; f3 = value; value = tmp;
            case 4:
                if (N == 4) { f4 = value; break; }
                tmp = f4; f4 = value; value = tmp;

                index = FIELDS_STORE_SIZE;
            default:
                ensureCapacity(N + 1);
                if (index != N) {
                    System.arraycopy(data, index - FIELDS_STORE_SIZE,
                                     data, index - FIELDS_STORE_SIZE + 1,
                                     N - index);
                }
                data[index - FIELDS_STORE_SIZE] = value;
        }
        size = N + 1;
    }

    public final void remove(int index)
    {
        int N = size;
        if (!(0 <= index && index < N)) throw onInvalidIndex(index, N);
        if (sealed) throw onSeledMutation();
        --N;
        switch (index) {
            case 0:
                if (N == 0) { f0 = null; break; }
                f0 = f1;
            case 1:
                if (N == 1) { f1 = null; break; }
                f1 = f2;
            case 2:
                if (N == 2) { f2 = null; break; }
                f2 = f3;
            case 3:
                if (N == 3) { f3 = null; break; }
                f3 = f4;
            case 4:
                if (N == 4) { f4 = null; break; }
                f4 = data[0];

                index = FIELDS_STORE_SIZE;
            default:
                if (index != N) {
                    System.arraycopy(data, index - FIELDS_STORE_SIZE + 1,
                                     data, index - FIELDS_STORE_SIZE,
                                     N - index);
                }
                data[N - FIELDS_STORE_SIZE] = null;
        }
        size = N;
    }

    public final void clear()
    {
        if (sealed) throw onSeledMutation();
        int N = size;
        for (int i = 0; i != N; ++i) {
            setImpl(i, null);
        }
        size = 0;
    }

    public final Object[] toArray()
    {
        Object[] array = new Object[size];
        toArray(array, 0);
        return array;
    }

    public final void toArray(Object[] array)
    {
        toArray(array, 0);
    }

    public final void toArray(Object[] array, int offset)
    {
        int N = size;
        switch (N) {
            default:
                System.arraycopy(data, 0, array, offset + FIELDS_STORE_SIZE,
                                 N - FIELDS_STORE_SIZE);
            case 5: array[offset + 4] = f4;
            case 4: array[offset + 3] = f3;
            case 3: array[offset + 2] = f2;
            case 2: array[offset + 1] = f1;
            case 1: array[offset + 0] = f0;
            case 0: break;
        }
    }

    private void ensureCapacity(int minimalCapacity)
    {
        int required = minimalCapacity - FIELDS_STORE_SIZE;
        if (required <= 0) throw new IllegalArgumentException();
        if (data == null) {
            int alloc = FIELDS_STORE_SIZE * 2;
            if (alloc < required) {
                alloc = required;
            }
            data = new Object[alloc];
        } else {
            int alloc = data.length;
            if (alloc < required) {
                   if (alloc <= FIELDS_STORE_SIZE) {
                    alloc = FIELDS_STORE_SIZE * 2;
                } else {
                    alloc *= 2;
                }
                if (alloc < required) {
                    alloc = required;
                }
                Object[] tmp = new Object[alloc];
                if (size > FIELDS_STORE_SIZE) {
                    System.arraycopy(data, 0, tmp, 0,
                                     size - FIELDS_STORE_SIZE);
                }
                data = tmp;
            }
        }
    }

    private static RuntimeException onInvalidIndex(int index, int upperBound)
    {
        // \u2209 is "NOT ELEMENT OF"
        String msg = index+" \u2209 [0, "+upperBound+')';
        throw new IndexOutOfBoundsException(msg);
    }

    private static RuntimeException onEmptyStackTopRead()
    {
        throw new RuntimeException("Empty stack");
    }

    private static RuntimeException onSeledMutation()
    {
        throw new IllegalStateException("Attempt to modify sealed array");
    }

    private void writeObject(ObjectOutputStream os) throws IOException
    {
        os.defaultWriteObject();
        int N = size;
        for (int i = 0; i != N; ++i) {
            Object obj = getImpl(i);
            os.writeObject(obj);
        }
    }

    private void readObject(ObjectInputStream is)
        throws IOException, ClassNotFoundException
    {
        is.defaultReadObject(); // It reads size
        int N = size;
        if (N > FIELDS_STORE_SIZE) {
            data = new Object[N - FIELDS_STORE_SIZE];
        }
        for (int i = 0; i != N; ++i) {
            Object obj = is.readObject();
            setImpl(i, obj);
        }
    }

// Number of data elements
    private int size;

    private boolean sealed;

    private static final int FIELDS_STORE_SIZE = 5;
    private transient Object f0, f1, f2, f3, f4;
    private transient Object[] data;
}
