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

import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Random;

/**
 * This class contains the references to the "NodeStore,RelationshipStore,
 * PropertyStore and RelationshipTypeStore". NeoStore doesn't actually "store"
 * anything but extends the AbstractStore for the "type and version" validation
 * performed in there.
 */
public class NeoStore extends AbstractStore
{
    // neo store version, store should end with this string
    // (byte encoded)
    private static final String VERSION = "NeoStore v0.9.5";

    // 3 longs in header (long + in use), time | random | version
    private static final int RECORD_SIZE = 9;

    private NodeStore nodeStore;
    private PropertyStore propStore;
    private RelationshipStore relStore;
    private RelationshipTypeStore relTypeStore;
    
    private final int REL_GRAB_SIZE;

    public NeoStore( Map<?,?> config )
    {
        super( (String) config.get( "neo_store" ), config );
        if ( getConfig() != null )
        {
            String grabSize = (String) getConfig().get( "relationship_grab_size" );
            if ( grabSize != null )
            {
                REL_GRAB_SIZE = Integer.parseInt( grabSize ); 
            }
            else
            {
                REL_GRAB_SIZE = 100;
            }
        }
        else
        {
            REL_GRAB_SIZE = 100;
        }
    }

    public NeoStore( String fileName )
    {
        super( fileName );
        REL_GRAB_SIZE = 100;
    }

    /**
     * Initializes the node,relationship,property and relationship type stores.
     */
    @Override
    protected void initStorage()
    {
        relTypeStore = new RelationshipTypeStore( getStorageFileName()
            + ".relationshiptypestore.db", getConfig() );
        propStore = new PropertyStore( getStorageFileName()
            + ".propertystore.db", getConfig() );
        relStore = new RelationshipStore( getStorageFileName()
            + ".relationshipstore.db", getConfig() );
        nodeStore = new NodeStore( getStorageFileName() + ".nodestore.db",
            getConfig() );
    }
    
    /**
     * Closes the node,relationship,property and relationship type stores.
     */
    @Override
    protected void closeStorage()
    {
        if ( relTypeStore != null )
        {
            relTypeStore.close();
            relTypeStore = null;
        }
        if ( propStore != null )
        {
            propStore.close();
            propStore = null;
        }
        if ( relStore != null )
        {
            relStore.close();
            relStore = null;
        }
        if ( nodeStore != null )
        {
            nodeStore.close();
            nodeStore = null;
        }
    }

    public void flushAll()
    {
        if ( relTypeStore == null || propStore == null || relStore == null || 
                nodeStore == null )
        {
            return;
        }
        relTypeStore.flushAll();
        propStore.flushAll();
        relStore.flushAll();
        nodeStore.flushAll();
    }

    public String getTypeAndVersionDescriptor()
    {
        return VERSION;
    }

    public int getRecordSize()
    {
        return RECORD_SIZE;
    }

    private static final Random r = new Random( System.currentTimeMillis() );
    
    /**
     * Creates the neo,node,relationship,property and relationship type stores.
     * 
     * @param fileName
     *            The name of store
     * @throws IOException
     *             If unable to create stores or name null
     */
    public static void createStore( String fileName, Map<?,?> config )
    {
        createEmptyStore( fileName, VERSION );
        NodeStore.createStore( fileName + ".nodestore.db" );
        RelationshipStore.createStore( fileName + ".relationshipstore.db" );
        PropertyStore.createStore( fileName + ".propertystore.db", config );
        RelationshipTypeStore.createStore( fileName
            + ".relationshiptypestore.db" );
        NeoStore neoStore = new NeoStore( fileName );
        // created time | random long | backup version
        neoStore.nextId(); neoStore.nextId(); neoStore.nextId();
        long time = System.currentTimeMillis();
        neoStore.setCreationTime( time );
        neoStore.setRandomNumber( r.nextLong() );
        neoStore.setVersion( 0 );
        neoStore.close();
    }
    
    public long getCreationTime()
    {
        return getRecord( 0 );
    }
    
    public void setCreationTime( long time )
    {
        setRecord( 0, time );
    }
    
    public long getRandomNumber()
    {
        return getRecord( 1 );
    }
    
    public void setRandomNumber( long nr )
    {
        setRecord( 1, nr );
    }
    
    public void setRecoveredStatus( boolean status )
    {
        if ( status )
        {
            setRecovered();
        }
        else
        {
            unsetRecovered();
        }
    }
    
    public long getVersion()
    {
        return getRecord( 2 );
    }
    
    public void setVersion( long version )
    {
        setRecord( 2, version );
    }
    
    public long incrementVersion()
    {
        long current = getVersion();
        setVersion( current + 1 );
        return current;
    }
    
    private long getRecord( int id )
    {
        PersistenceWindow window = acquireWindow( id, OperationType.READ );
        try
        {
            Buffer buffer = window.getOffsettedBuffer( id );
            buffer.get();
            return buffer.getLong();
        }
        finally
        {
            releaseWindow( window );
        }
    }
    
    private void setRecord( int id, long value )
    {
        PersistenceWindow window = acquireWindow( id, OperationType.WRITE );
        try
        {
            Buffer buffer = window.getOffsettedBuffer( id );
            buffer.put( Record.IN_USE.byteValue() ).putLong( value );
        }
        finally
        {
            releaseWindow( window );
        }
    }

    /**
     * Returns the node store.
     * 
     * @return The node store
     */
    public NodeStore getNodeStore()
    {
        return nodeStore;
    }

    /**
     * The relationship store.
     * 
     * @return The relationship store
     */
    public RelationshipStore getRelationshipStore()
    {
        return relStore;
    }

    /**
     * Returns the relationship type store.
     * 
     * @return The relationship type store
     */
    public RelationshipTypeStore getRelationshipTypeStore()
    {
        return relTypeStore;
    }

    /**
     * Returns the property store.
     * 
     * @return The property store
     */
    public PropertyStore getPropertyStore()
    {
        return propStore;
    }

    @Override
    public void makeStoreOk()
    {
        relTypeStore.makeStoreOk();
        propStore.makeStoreOk();
        relStore.makeStoreOk();
        nodeStore.makeStoreOk();
        super.makeStoreOk();
    }
    
    
    public void rebuildIdGenerators()
    {
        relTypeStore.rebuildIdGenerators();
        propStore.rebuildIdGenerators();
        relStore.rebuildIdGenerators();
        nodeStore.rebuildIdGenerators();
        super.rebuildIdGenerators();
    }

    public void updateIdGenerators()
    {
        this.updateHighId();
        relTypeStore.updateIdGenerators();
        propStore.updateIdGenerators();
        relStore.updateHighId();
        nodeStore.updateHighId();
    }
    
    @Override
    protected boolean versionFound( String version )
    {
        if ( !version.startsWith( "NeoStore" ) )
        {
            // non clean shutdown, need to do recover with right neo
            return false;
        }
        if ( version.equals( "NeoStore v0.9.3" ) )
        {
            ByteBuffer buffer = ByteBuffer.wrap( new byte[ 3 * RECORD_SIZE ] );
            long time = System.currentTimeMillis();
            long random = r.nextLong();
            buffer.put( Record.IN_USE.byteValue() ).putLong( time );
            buffer.put( Record.IN_USE.byteValue() ).putLong( random );
            buffer.put( Record.IN_USE.byteValue() ).putLong( 0 );
            buffer.flip();
            try
            {
                getFileChannel().write( buffer, 0 );
            }
            catch ( IOException e )
            {
                throw new UnderlyingStorageException( e );
            }
            rebuildIdGenerator();
            closeIdGenerator();
            return true;
        }
        else if ( version.equals( "NeoStore v0.9.4" ) )
        {
            rebuildIdGenerator();
            closeIdGenerator();
            return true;
        }
        throw new IllegalStoreVersionException( "Store version [" + version  + 
            "]. Please make sure you are not running old Neo4j kernel " + 
            " towards a store that has been created by newer version " + 
            " of Neo4j." );
    }

    public int getRelationshipGrabSize()
    {
        return REL_GRAB_SIZE;
    }
    
    public List<WindowPoolStats> getAllWindowPoolStats()
    {
        List<WindowPoolStats> list = new ArrayList<WindowPoolStats>();
        list.addAll( nodeStore.getAllWindowPoolStats() );
        list.addAll( propStore.getAllWindowPoolStats() );
        list.addAll( relStore.getAllWindowPoolStats() );
        list.addAll( relTypeStore.getAllWindowPoolStats() );
        return list;
    }
}