/*
 * Copyright (c) 2002-2009 "Neo Technology,"
 *     Network Engine for Objects in Lund AB [http://neotechnology.com]
 *
 * This file is part of Neo4j.
 * 
 * Neo4j is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 * 
 * You should have received a copy of the GNU Affero General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */
package org.neo4j.kernel.impl.util;

import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;

public class ArrayMap<K,V>
{
    private ArrayEntry<K,V>[] arrayEntries;

    private volatile int arrayCount = 0;
    private int toMapThreshold = 5;
    private Map<K,V> propertyMap = null;
    private final boolean useThreadSafeMap;
    private boolean switchBackToArray = false;

    public ArrayMap()
    {
        useThreadSafeMap = false;
        arrayEntries = new ArrayEntry[toMapThreshold];
    }

    public ArrayMap( int mapThreshold, boolean threadSafe, boolean shrinkToArray )
    {
        this.toMapThreshold = mapThreshold;
        this.useThreadSafeMap = threadSafe;
        this.switchBackToArray = shrinkToArray;
        arrayEntries = new ArrayEntry[toMapThreshold];
    }

    public void put( K key, V value )
    {
        if ( useThreadSafeMap )
        {
            synchronizedPut( key, value );
            return;
        }
        for ( int i = 0; i < arrayCount; i++ )
        {
            if ( arrayEntries[i].getKey().equals( key ) )
            {
                arrayEntries[i].setNewValue( value );
                return;
            }
        }
        if ( arrayCount != -1 )
        {
            if ( arrayCount < arrayEntries.length )
            {
                arrayEntries[arrayCount++] = new ArrayEntry<K,V>( key, value );
            }
            else
            {
                propertyMap = new HashMap<K,V>( arrayEntries.length * 2 );
                for ( int i = 0; i < arrayCount; i++ )
                {
                    propertyMap.put( arrayEntries[i].getKey(), arrayEntries[i]
                        .getValue() );
                }
                arrayCount = -1;
                propertyMap.put( key, value );
            }
        }
        else
        {
            propertyMap.put( key, value );
        }
    }

    private synchronized void synchronizedPut( K key, V value )
    {
        for ( int i = 0; i < arrayCount; i++ )
        {
            if ( arrayEntries[i].getKey().equals( key ) )
            {
                arrayEntries[i].setNewValue( value );
                return;
            }
        }
        if ( arrayCount != -1 )
        {
            if ( arrayCount < arrayEntries.length )
            {
                arrayEntries[arrayCount++] = new ArrayEntry<K,V>( key, value );
            }
            else
            {
                propertyMap = new HashMap<K,V>( arrayEntries.length * 2 );
                for ( int i = 0; i < arrayCount; i++ )
                {
                    propertyMap.put( arrayEntries[i].getKey(), arrayEntries[i]
                        .getValue() );
                }
                arrayEntries = null;
                arrayCount = -1;
                propertyMap.put( key, value );
            }
        }
        else
        {
            propertyMap.put( key, value );
        }
    }

    public V get( K key )
    {
        if ( key == null )
        {
            return null;
        }
        if ( useThreadSafeMap )
        {
            return synchronizedGet( key );
        }
        int count = arrayCount;
        for ( int i = 0; i < count; i++ )
        {
            if ( key.equals( arrayEntries[i].getKey() ) )
            {
                return arrayEntries[i].getValue();
            }
        }
        if ( arrayCount == -1 )
        {
            return propertyMap.get( key );
        }
        return null;
    }

    private synchronized V synchronizedGet( K key )
    {
        for ( int i = 0; i < arrayCount; i++ )
        {
            if ( key.equals( arrayEntries[i].getKey() ) )
            {
                return arrayEntries[i].getValue();
            }
        }
        if ( arrayCount == -1 )
        {
            return propertyMap.get( key );
        }
        return null;
    }

    private synchronized V synchronizedRemove( K key )
    {
        for ( int i = 0; i < arrayCount; i++ )
        {
            if ( arrayEntries[i].getKey().equals( key ) )
            {
                V removedProperty = arrayEntries[i].getValue();
                arrayCount--;
                System.arraycopy( arrayEntries, i + 1, arrayEntries, i,
                    arrayCount - i );
                return removedProperty;
            }
        }
        if ( arrayCount == -1 )
        {
            V value = propertyMap.remove( key );
            if ( switchBackToArray && propertyMap.size() < toMapThreshold )
            {
                arrayEntries = new ArrayEntry[toMapThreshold];
                int tmpCount = 0;
                for ( Entry<K,V> entry : propertyMap.entrySet() )
                {
                    arrayEntries[tmpCount++] = new ArrayEntry<K,V>( entry
                        .getKey(), entry.getValue() );
                }
                arrayCount = tmpCount;
            }
            return value;
        }
        return null;
    }

    public V remove( K key )
    {
        if ( useThreadSafeMap )
        {
            return synchronizedRemove( key );
        }
        for ( int i = 0; i < arrayCount; i++ )
        {
            if ( arrayEntries[i].getKey().equals( key ) )
            {
                V removedProperty = arrayEntries[i].getValue();
                arrayCount--;
                System.arraycopy( arrayEntries, i + 1, arrayEntries, i,
                    arrayCount - i );
                return removedProperty;
            }
        }
        if ( arrayCount == -1 )
        {
            V value = propertyMap.remove( key );
            if ( switchBackToArray && propertyMap.size() < toMapThreshold )
            {
                arrayEntries = new ArrayEntry[toMapThreshold];
                int tmpCount = 0;
                for ( Entry<K,V> entry : propertyMap.entrySet() )
                {
                    arrayEntries[tmpCount++] = new ArrayEntry<K,V>( entry
                        .getKey(), entry.getValue() );
                }
                arrayCount = tmpCount;
            }
            return value;
        }
        return null;
    }

    static class ArrayEntry<K,V> implements Entry<K,V>
    {
        private K key;
        private V value;

        ArrayEntry( K key, V value )
        {
            this.key = key;
            this.value = value;
        }

        public K getKey()
        {
            return key;
        }

        public V getValue()
        {
            return value;
        }

        void setNewValue( V value )
        {
            this.value = value;
        }

        public V setValue( V value )
        {
            V oldValue = value;
            this.value = value;
            return oldValue;
        }
    }

    public Iterable<K> keySet()
    {
        if ( arrayCount == -1 )
        {
            return propertyMap.keySet();
        }
        List<K> keys = new LinkedList<K>();
        for ( int i = 0; i < arrayCount; i++ )
        {
            keys.add( arrayEntries[i].getKey() );
        }
        return keys;
    }

    public Iterable<V> values()
    {
        if ( arrayCount == -1 )
        {
            return propertyMap.values();
        }
        List<V> values = new LinkedList<V>();
        for ( int i = 0; i < arrayCount; i++ )
        {
            values.add( arrayEntries[i].getValue() );
        }
        return values;
    }
    
    public Set<Entry<K,V>> entrySet()
    {
        if ( arrayCount == -1 )
        {
            return propertyMap.entrySet();
        }
        Set<Entry<K,V>> entries = new HashSet<Entry<K,V>>();
        for ( int i = 0; i < arrayCount; i++ )
        {
            entries.add( arrayEntries[i] );
        }
        return entries;
    }

    public int size()
    {
        if ( arrayCount != -1 )
        {
            return arrayCount;
        }
        return propertyMap.size();
    }

    public void clear()
    {
        if ( arrayCount != -1 )
        {
            arrayCount = 0;
        }
        else
        {
            propertyMap.clear();
        }
    }
}