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

import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.ReadableByteChannel;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;

import org.neo4j.graphdb.Node;
import org.neo4j.graphdb.Relationship;
import org.neo4j.graphdb.RelationshipType;
import org.neo4j.kernel.Config;
import org.neo4j.kernel.impl.core.LockReleaser;
import org.neo4j.kernel.impl.core.PropertyIndex;
import org.neo4j.kernel.impl.nioneo.store.NeoStore;
import org.neo4j.kernel.impl.nioneo.store.PropertyStore;
import org.neo4j.kernel.impl.nioneo.store.Store;
import org.neo4j.kernel.impl.nioneo.store.WindowPoolStats;
import org.neo4j.kernel.impl.persistence.IdGenerationFailedException;
import org.neo4j.kernel.impl.transaction.LockManager;
import org.neo4j.kernel.impl.transaction.xaframework.XaCommand;
import org.neo4j.kernel.impl.transaction.xaframework.XaCommandFactory;
import org.neo4j.kernel.impl.transaction.xaframework.XaConnection;
import org.neo4j.kernel.impl.transaction.xaframework.XaContainer;
import org.neo4j.kernel.impl.transaction.xaframework.XaDataSource;
import org.neo4j.kernel.impl.transaction.xaframework.XaTransaction;
import org.neo4j.kernel.impl.transaction.xaframework.XaTransactionFactory;
import org.neo4j.kernel.impl.util.ArrayMap;

/**
 * A <CODE>NeoStoreXaDataSource</CODE> is a factory for
 * {@link NeoStoreXaConnection NeoStoreXaConnections}.
 * <p>
 * The {@link NioNeoDbPersistenceSource} will create a <CODE>NeoStoreXaDataSoruce</CODE>
 * and then Neo4j kernel will use it to create {@link XaConnection XaConnections} and
 * {@link XaResource XaResources} when running transactions and performing
 * operations on the node space.
 */
public class NeoStoreXaDataSource extends XaDataSource
{
    private static Logger logger = Logger.getLogger( 
        NeoStoreXaDataSource.class.getName() );

    private final NeoStore neoStore;
    private final XaContainer xaContainer;
    private final ArrayMap<Class<?>,Store> idGenerators;

    private final LockManager lockManager;
    private final LockReleaser lockReleaser;
    private final String storeDir;
    private boolean readOnly = false;
    private boolean backupSlave = false;
    
    private boolean logApplied = false;

    /**
     * Creates a <CODE>NeoStoreXaDataSource</CODE> using configuration from
     * <CODE>params</CODE>. First the map is checked for the parameter 
     * <CODE>config</CODE>.
     * If that parameter exists a config file with that value is loaded (via
     * {@link Properties#load}). Any parameter that exist in the config file
     * and in the map passed into this constructor will take the value from the
     * map.
     * <p>
     * If <CODE>config</CODE> parameter is set but file doesn't exist an
     * <CODE>IOException</CODE> is thrown. If any problem is found with that
     * configuration file or Neo4j store can't be loaded an <CODE>IOException is
     * thrown</CODE>.
     * 
     * @param params
     *            A map containing configuration parameters and/or configuration
     *            file.
     * @throws IOException
     *             If unable to create data source
     */
    public NeoStoreXaDataSource( Map<Object,Object> config ) throws IOException,
        InstantiationException
    {
        super( config );
        if ( config.get( "read_only" ) != null )
        {
            readOnly = (Boolean) config.get( "read_only" );
        }
        if ( "true".equals( config.get( "backup_slave" ) ) )
        {
            backupSlave = true;
        }
        this.lockManager = (LockManager) config.get( LockManager.class );
        this.lockReleaser = (LockReleaser) config.get( LockReleaser.class );
        storeDir = (String) config.get( "store_dir" );
        String store = (String) config.get( "neo_store" );
        if ( !config.containsKey( "rebuild_idgenerators_fast" ) )
        {
            config.put( "rebuild_idgenerators_fast", "true" );
        }
        File file = new File( store );
        String create = "" + config.get( "create" );
        if ( !readOnly && !file.exists() && "true".equals( create ) )
        {
            autoCreatePath( store );
            NeoStore.createStore( store, config );
        }

        neoStore = new NeoStore( config );
        xaContainer = XaContainer.create( (String) config.get( "logical_log" ),
            new CommandFactory( neoStore ), new TransactionFactory(), config );

        if ( !readOnly )
        {
            xaContainer.openLogicalLog();
        }
        if ( !xaContainer.getResourceManager().hasRecoveredTransactions() )
        {
            neoStore.makeStoreOk();
        }
        else
        {
            logger.fine( "Waiting for TM to take care of recovered " + 
                "transactions." );
        }
        idGenerators = new ArrayMap<Class<?>,Store>( 5, false, false );
        this.idGenerators.put( Node.class, neoStore.getNodeStore() );
        this.idGenerators.put( Relationship.class, 
            neoStore.getRelationshipStore() );
        this.idGenerators.put( RelationshipType.class, 
            neoStore.getRelationshipTypeStore() );
        this.idGenerators.put( PropertyStore.class, 
            neoStore.getPropertyStore() );
        this.idGenerators.put( PropertyIndex.class, 
            neoStore.getPropertyStore().getIndexStore() );
        xaContainer.getLogicalLog().setKeepLogs(
                shouldKeepLog( (String) config.get( Config.KEEP_LOGICAL_LOGS ), "nioneodb" ) );
    }
    
    private void autoCreatePath( String store ) throws IOException
    {
        String fileSeparator = System.getProperty( "file.separator" );
        int index = store.lastIndexOf( fileSeparator );
        String dirs = store.substring( 0, index );
        File directories = new File( dirs );
        if ( !directories.exists() )
        {
            if ( !directories.mkdirs() )
            {
                throw new IOException( "Unable to create directory path["
                    + dirs + "] for Neo4j store." );
            }
        }
    }

    /**
     * Creates a data source with minimum (no memory mapped) configuration.
     * 
     * @param neoStoreFileName
     *            The file name of the store
     * @param logicalLogPath
     *            The file name of the logical log
     * @throws IOException
     *             If unable to open store
     */
    public NeoStoreXaDataSource( String neoStoreFileName,
        String logicalLogPath, LockManager lockManager,
        LockReleaser lockReleaser )
        throws IOException, InstantiationException
    {
        super( null );
        this.lockManager = lockManager;
        this.lockReleaser = lockReleaser;
        storeDir = logicalLogPath;
        neoStore = new NeoStore( neoStoreFileName );
        xaContainer = XaContainer.create( logicalLogPath, new CommandFactory(
            neoStore ), new TransactionFactory(), null );

        xaContainer.openLogicalLog();
        if ( !xaContainer.getResourceManager().hasRecoveredTransactions() )
        {
            neoStore.makeStoreOk();
        }
        else
        {
            logger.info( "Waiting for TM to take care of recovered " + 
                "transactions." );
        }
        idGenerators = new ArrayMap<Class<?>,Store>( 5, false, false );
        this.idGenerators.put( Node.class, neoStore.getNodeStore() );
        this.idGenerators.put( Relationship.class, 
            neoStore.getRelationshipStore() );
        this.idGenerators.put( RelationshipType.class, 
            neoStore.getRelationshipTypeStore() );
        // get TestXa unit test to run
        this.idGenerators.put( PropertyStore.class, 
            neoStore.getPropertyStore() );
        this.idGenerators.put( PropertyIndex.class, 
            neoStore.getPropertyStore().getIndexStore() );
    }
    
    NeoStore getNeoStore()
    {
        return neoStore;
    }

    public void close()
    {
        if ( !readOnly )
        {
            neoStore.flushAll();
        }
        xaContainer.close();
        if ( logApplied )
        {
            neoStore.rebuildIdGenerators();
            logApplied = false;
        }
        neoStore.close();
        logger.fine( "NeoStore closed" );
    }

    public XaConnection getXaConnection()
    {
        return new NeoStoreXaConnection( neoStore, 
            xaContainer.getResourceManager(), getBranchId() );
    }

    private static class CommandFactory extends XaCommandFactory
    {
        private NeoStore neoStore = null;

        CommandFactory( NeoStore neoStore )
        {
            this.neoStore = neoStore;
        }

        public XaCommand readCommand( ReadableByteChannel byteChannel, 
            ByteBuffer buffer ) throws IOException
        {
            Command command = Command.readCommand( neoStore, byteChannel,
                buffer );
            if ( command != null )
            {
                command.setRecovered();
            }
            return command;
        }
    }

    private class TransactionFactory extends XaTransactionFactory
    {
        TransactionFactory()
        {
        }

        public XaTransaction create( int identifier )
        {
            return new WriteTransaction( identifier, getLogicalLog(), neoStore,
                lockReleaser, lockManager );
        }

        public void recoveryComplete()
        {
            logger.fine( "Recovery complete, "
                + "all transactions have been resolved" );
            logger.fine( "Rebuilding id generators as needed. "
                + "This can take a while for large stores..." );
            neoStore.flushAll();
            neoStore.makeStoreOk();
            logger.fine( "Rebuild of id generators complete." );
        }

        @Override
        public long getCurrentVersion()
        {
            if ( getLogicalLog().scanIsComplete() )
            {
                return neoStore.getVersion();
            }
            neoStore.setRecoveredStatus( true );
            try
            {
                return neoStore.getVersion();
            }
            finally
            {
                neoStore.setRecoveredStatus( false );
            }
        }
        
        @Override
        public long getAndSetNewVersion()
        {
            return neoStore.incrementVersion();
        }
        
        @Override
        public void flushAll()
        {
            neoStore.flushAll();
        }
    }

    public int nextId( Class<?> clazz )
    {
        Store store = idGenerators.get( clazz );

        if ( store == null )
        {
            throw new IdGenerationFailedException( "No IdGenerator for: "
                + clazz );
        }
        return store.nextId();
    }

    public long getHighestPossibleIdInUse( Class<?> clazz )
    {
        Store store = idGenerators.get( clazz );
        if ( store == null )
        {
            throw new IdGenerationFailedException( "No IdGenerator for: "
                + clazz );
        }
        return store.getHighestPossibleIdInUse();
    }

    public long getNumberOfIdsInUse( Class<?> clazz )
    {
        Store store = idGenerators.get( clazz );
        if ( store == null )
        {
            throw new IdGenerationFailedException( "No IdGenerator for: "
                + clazz );
        }
        return store.getNumberOfIdsInUse();
    }

    public String getStoreDir()
    {
        return storeDir;
    }
    
    @Override
    public void keepLogicalLogs( boolean keep )
    {
        xaContainer.getLogicalLog().setKeepLogs( keep );
    }
    
    @Override
    public boolean isLogicalLogKept()
    {
        return xaContainer.getLogicalLog().isLogsKept();
    }
    
    @Override
    public long getCreationTime()
    {
        return neoStore.getCreationTime();
    }
    
    @Override
    public long getRandomIdentifier()
    {
        return neoStore.getRandomNumber();
    }
    
    @Override
    public long getCurrentLogVersion()
    {
        return neoStore.getVersion();
    }

    public long incrementAndGetLogVersion()
    {
        return neoStore.incrementVersion();
    }

    public void setCurrentLogVersion( long version )
    {
        neoStore.setVersion( version );
    }
    
    @Override
    public void applyLog( ReadableByteChannel byteChannel ) throws IOException
    {
        logApplied = true;
        xaContainer.getLogicalLog().applyLog( byteChannel );
    }
    
    @Override
    public void rotateLogicalLog() throws IOException
    {
        // flush done inside rotate
        xaContainer.getLogicalLog().rotate();
    }
    
    @Override
    public ReadableByteChannel getLogicalLog( long version ) throws IOException
    {
        return xaContainer.getLogicalLog().getLogicalLog( version );
    }
    
    public long getLogicalLogLength( long version )
    {
        return xaContainer.getLogicalLog().getLogicalLogLength( version );
    }

    @Override
    public boolean hasLogicalLog( long version )
    {
        return xaContainer.getLogicalLog().hasLogicalLog( version );
    }
    
    @Override
    public boolean deleteLogicalLog( long version )
    {
        return xaContainer.getLogicalLog().deleteLogicalLog( version );
    }
    
    @Override
    public void setAutoRotate( boolean rotate )
    {
        xaContainer.getLogicalLog().setAutoRotateLogs( rotate );
    }
    
    @Override
    public void setLogicalLogTargetSize( long size )
    {
        xaContainer.getLogicalLog().setLogicalLogTargetSize( size );
    }
    
    @Override
    public void makeBackupSlave()
    {
        xaContainer.getLogicalLog().makeBackupSlave();
    }
    
    ReadTransaction getReadOnlyTransaction()
    {
        return new ReadTransaction( neoStore );
    }

    public boolean isReadOnly()
    {
        return readOnly;
    }

    public String getFileName( long version )
    {
        return xaContainer.getLogicalLog().getFileName( version );
    }
    
    public List<WindowPoolStats> getWindowPoolStats()
    {
        return neoStore.getAllWindowPoolStats();
    }
}