/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.apache.openjpa.lib.rop;

import java.io.ObjectStreamException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;


/**
 * Random-access result list implementation. It maintains a map
 * of the items that we have already instantiated.
 *
 * @author Marc Prud'hommeaux
 * @author Abe White
 */
public class RandomAccessResultList extends AbstractNonSequentialResultList {

    private static final int OPEN = 0;
    private static final int FREED = 1;
    private static final int CLOSED = 2;

    // data provider
    private ResultObjectProvider _rop = null;

    // holds all the row values that have been instantiated so far
    private Map _rows = null;
    private Object[] _full = null;

    // bookkeeping
    private long _requests = 0;
    private int _state = OPEN;
    private int _size = -1;

    public RandomAccessResultList(ResultObjectProvider rop) {
        _rop = rop;
        _rows = newRowMap();

        try {
            _rop.open();
        } catch (RuntimeException re) {
            close();
            throw re;
        } catch (Exception e) {
            close();
            _rop.handleCheckedException(e);
        }
    }

    /**
     * Override this method to control what kind of map is used for
     * the instantiated rows.
     */
    protected Map newRowMap() {
        return new HashMap();
    }

    public boolean isProviderOpen() {
        return _state == OPEN;
    }

    public boolean isClosed() {
        return _state == CLOSED;
    }

    public void close() {
        if (_state != CLOSED) {
            free();
            _state = CLOSED;
        }
    }

    protected Object getInternal(int index) {
        if (_full != null) {
            if (index >= _full.length)
                return PAST_END;
            return _full[index];
        }

        Integer i = index;
        Object ret = _rows.get(i);
        if (ret != null) {
            if (ret instanceof Null)
                return null;
            return ret;
        }

        ret = instantiateRow(i);
        return (ret == null) ? PAST_END : ret;
    }

    /**
     * Instantiate the row object at the specified index.
     */
    private Object instantiateRow(Integer i) {
        _requests++;
        try {
            if (!_rop.absolute(i.intValue()))
                return PAST_END;

            Object ob = _rop.getResultObject();
            if (ob == null)
                ob = new Null();

            // cache the result
            _rows.put(i, ob);

            // check to see if our map is full
            checkComplete();
            return ob;
        } catch (RuntimeException re) {
            close();
            throw re;
        } catch (Exception e) {
            close();
            _rop.handleCheckedException(e);
            return null;
        }
    }

    /**
     * Check to see if the soft map is the same size as all the
     * rows in the Result: if so, we copy over the values to a
     * hard reference HashSet and close the Result object associated with
     * this endeavour.
     */
    private void checkComplete() {
        // only check if we've actually gotten the size for some reason already
        if (_size == -1 || _rows.size() != _size)
            return;

        Object[] full = new Object[_size];
        int count = 0;
        Integer key;
        for (Iterator itr = _rows.keySet().iterator(); itr.hasNext(); count++) {
            key = (Integer) itr.next();
            full[key.intValue()] = _rows.get(key);
        }

        // double-check, in case any of the soft references were
        // cleaned up between the time we checked the size and the
        // time we completed the copy to the hard reference map
        if (count == _size) {
            _full = full;
            free();
        }
    }

    public int size() {
        assertOpen();
        if (_size != -1)
            return _size;
        if (_full != null)
            return _full.length;
        try {
            _size = _rop.size();
            return _size;
        } catch (RuntimeException re) {
            close();
            throw re;
        } catch (Exception e) {
            close();
            _rop.handleCheckedException(e);
            return -1;
        }
    }

    private void free() {
        if (_state == OPEN) {
            try {
                _rop.close();
            } catch (Exception e) {
            }
            _rows = null;
            _state = FREED;
        }
    }

    public Object writeReplace() throws ObjectStreamException {
        if (_full != null)
            return new ListResultList(Arrays.asList(_full));
        ArrayList list = new ArrayList();
        for (Iterator itr = iterator(); itr.hasNext();)
            list.add(itr.next());
        return list;
    }

    public String toString() {
        return getClass().getName()
            + "; identity: " + System.identityHashCode(this)
            + "; cached: " + _rows.size()
            + "; requests: " + _requests;
    }

    public int hashCode() {
        // superclass tries to traverses entire list for hashcode
        return System.identityHashCode(this);
    }

    public boolean equals(Object other) {
        // superclass tries to traverse entire list for equality
        return other == this;
    }

    /**
     * Used to represent nulls in the result list. Can't use a singleton
     * pattern, because then there will always be a hard ref to all the
     * nulls, and they'll never get GC'd; this is bad in the unlikely
     * event of a huge result set with lots of nulls.
     */
    private static class Null {

    }
}

