/*
 * 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.core;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;

import javax.transaction.Transaction;
import javax.transaction.TransactionManager;

import org.neo4j.graphdb.NotFoundException;
import org.neo4j.graphdb.NotInTransactionException;
import org.neo4j.graphdb.TransactionFailureException;
import org.neo4j.kernel.impl.nioneo.store.PropertyIndexData;
import org.neo4j.kernel.impl.persistence.IdGenerator;
import org.neo4j.kernel.impl.persistence.PersistenceManager;
import org.neo4j.kernel.impl.util.ArrayMap;

public class PropertyIndexManager
{
    private ArrayMap<String,List<PropertyIndex>> indexMap = 
        new ArrayMap<String,List<PropertyIndex>>( 5, true, false );
    private ArrayMap<Integer,PropertyIndex> idToIndexMap = 
        new ArrayMap<Integer,PropertyIndex>( 9, true, false );

    private ArrayMap<Transaction,TxCommitHook> txCommitHooks = 
        new ArrayMap<Transaction,TxCommitHook>( 5, true, false );

    private final TransactionManager transactionManager;
    private final PersistenceManager persistenceManager;
    private final IdGenerator idGenerator;

    private boolean hasAll = false;

    PropertyIndexManager( TransactionManager transactionManager,
        PersistenceManager persistenceManager, IdGenerator idGenerator )
    {
        this.transactionManager = transactionManager;
        this.persistenceManager = persistenceManager;
        this.idGenerator = idGenerator;
    }

    void clear()
    {
        indexMap = new ArrayMap<String,List<PropertyIndex>>( 5, true, false );
        idToIndexMap = new ArrayMap<Integer,PropertyIndex>( 9, true, false );
        txCommitHooks.clear();
    }

    public Iterable<PropertyIndex> index( String key )
    {
        List<PropertyIndex> list = indexMap.get( key );
        TxCommitHook hook = txCommitHooks.get( getTransaction() );
        if ( hook != null )
        {
            PropertyIndex index = hook.getIndex( key );
            if ( index != null )
            {
                List<PropertyIndex> added = new ArrayList<PropertyIndex>();
                if ( list != null )
                {
                    added.addAll( list );
                }
                added.add( index );
                return added;
            }
        }
        if ( list == null )
        {
            list = Collections.emptyList();
        }
        return list;
    }

    void setHasAll( boolean status )
    {
        hasAll = status;
    }

    boolean hasAll()
    {
        return hasAll;
    }

    public boolean hasIndexFor( int keyId )
    {
        return idToIndexMap.get( keyId ) != null;
    }

    void addPropertyIndexes( PropertyIndexData[] indexes )
    {
        for ( PropertyIndexData rawIndex : indexes )
        {
            addPropertyIndex( new PropertyIndex( rawIndex.getValue(), 
                rawIndex.getKeyId() ) );
        }
    }

    void addPropertyIndex( PropertyIndexData rawIndex )
    {
        addPropertyIndex( new PropertyIndex( rawIndex.getValue(), 
            rawIndex.getKeyId() ) );
    }
    
    public PropertyIndex getIndexFor( int keyId )
    {
        PropertyIndex index = idToIndexMap.get( keyId );
        if ( index == null )
        {
            TxCommitHook commitHook = txCommitHooks.get( getTransaction() );
            if ( commitHook != null )
            {
                index = commitHook.getIndex( keyId );
                if ( index != null )
                {
                    return index;
                }
            }
            String indexString;
            indexString = persistenceManager.loadIndex( keyId );
            if ( indexString == null )
            {
                throw new NotFoundException( "Index not found [" + keyId + "]" );
            }
            index = new PropertyIndex( indexString, keyId );
            addPropertyIndex( index );
        }
        return index;
    }

    // need synch here so we don't create multiple lists
    private synchronized void addPropertyIndex( PropertyIndex index )
    {
        List<PropertyIndex> list = indexMap.get( index.getKey() );
        if ( list == null )
        {
            list = new CopyOnWriteArrayList<PropertyIndex>();
            indexMap.put( index.getKey(), list );
        }
        list.add( index );
        idToIndexMap.put( index.getKeyId(), index );
    }

    private Transaction getTransaction()
    {
        try
        {
            return transactionManager.getTransaction();
        }
        catch ( Exception e )
        {
            throw new TransactionFailureException( 
                "Unable to get transaction.", e );
        }
    }

    // concurent transactions may create duplicate keys, oh well
    PropertyIndex createPropertyIndex( String key )
    {
        Transaction tx = getTransaction();
        if ( tx == null )
        {
            throw new NotInTransactionException(
                "Unable to create property index for " + key );
        }
        TxCommitHook hook = txCommitHooks.get( tx );
        if ( hook == null )
        {
            hook = new TxCommitHook();
            txCommitHooks.put( tx, hook );
        }
        PropertyIndex index = hook.getIndex( key );
        if ( index != null )
        {
            return index;
        }
        int id = idGenerator.nextId( PropertyIndex.class );
        index = new PropertyIndex( key, id );
        hook.addIndex( index );
        persistenceManager.createPropertyIndex( key, id );
        return index;
    }

    void setRollbackOnly()
    {
        try
        {
            transactionManager.setRollbackOnly();
        }
        catch ( javax.transaction.SystemException se )
        {
            se.printStackTrace();
        }
    }
    
    void commit( Transaction tx )
    {
        if ( tx != null )
        {
            TxCommitHook hook = txCommitHooks.remove( tx );
            if ( hook != null )
            {
                for ( PropertyIndex index : hook.getAddedPropertyIndexes() )
                {
                    addPropertyIndex( index );
                }
            }
        }
    }
    
    void rollback( Transaction tx )
    {
        txCommitHooks.remove( tx );
    }

    private static class TxCommitHook
    {
        private Map<String,PropertyIndex> createdIndexes = 
            new HashMap<String,PropertyIndex>();
        private Map<Integer,PropertyIndex> idToIndex = 
            new HashMap<Integer,PropertyIndex>();


        TxCommitHook()
        {
        }

        void addIndex( PropertyIndex index )
        {
            assert !createdIndexes.containsKey( index.getKey() );
            createdIndexes.put( index.getKey(), index );
            idToIndex.put( index.getKeyId(), index );
        }

        PropertyIndex getIndex( String key )
        {
            return createdIndexes.get( key );
        }

        PropertyIndex getIndex( int keyId )
        {
            return idToIndex.get( keyId );
        }

        Iterable<PropertyIndex> getAddedPropertyIndexes()
        {
            return createdIndexes.values();
        }
    }
}