/*
 * 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.Collection;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

/**
 * Implementation of the property store. This implementation has two dynamic
 * stores. One used to store keys and another for string property values.
 */
public class PropertyStore extends AbstractStore implements Store
{
    // store version, each store ends with this string (byte encoded)
    private static final String VERSION = "PropertyStore v0.9.5";

    // record header size
    // in_use(byte)+type(int)+key_indexId(int)+prop_blockId(long)+
    // prev_prop_id(int)+next_prop_id(int)
    private static final int RECORD_SIZE = 25;

    private DynamicStringStore stringPropertyStore;
    private PropertyIndexStore propertyIndexStore;
    private DynamicArrayStore arrayPropertyStore;

    /**
     * See {@link AbstractStore#AbstractStore(String, Map)}
     */
    public PropertyStore( String fileName, Map<?,?> config )
    {
        super( fileName, config );
    }

    /**
     * See {@link AbstractStore#AbstractStore(String)}
     */
    public PropertyStore( String fileName )
    {
        super( fileName );
    }

    @Override
    protected void initStorage()
    {
        stringPropertyStore = new DynamicStringStore( getStorageFileName()
            + ".strings", getConfig() );
        propertyIndexStore = new PropertyIndexStore( getStorageFileName()
            + ".index", getConfig() );
        arrayPropertyStore = new DynamicArrayStore( getStorageFileName()
            + ".arrays", getConfig() );
    }

    @Override
    protected void setRecovered()
    {
        super.setRecovered();
        stringPropertyStore.setRecovered();
        propertyIndexStore.setRecovered();
        arrayPropertyStore.setRecovered();
    }

    @Override
    protected void unsetRecovered()
    {
        super.unsetRecovered();
        stringPropertyStore.unsetRecovered();
        propertyIndexStore.unsetRecovered();
        arrayPropertyStore.unsetRecovered();
    }

    @Override
    protected void closeStorage()
    {
        stringPropertyStore.close();
        stringPropertyStore = null;
        propertyIndexStore.close();
        propertyIndexStore = null;
        arrayPropertyStore.close();
        arrayPropertyStore = null;
    }

    @Override
    public void flushAll()
    {
        stringPropertyStore.flushAll();
        propertyIndexStore.flushAll();
        arrayPropertyStore.flushAll();
        super.flushAll();
    }

    public String getTypeAndVersionDescriptor()
    {
        return VERSION;
    }

    public int getRecordSize()
    {
        return RECORD_SIZE;
    }

    /**
     * Creates a new property store contained in <CODE>fileName</CODE> If
     * filename is <CODE>null</CODE> or the file already exists an 
     * <CODE>IOException</CODE> is thrown.
     * 
     * @param fileName
     *            File name of the new property store
     * @throws IOException
     *             If unable to create property store or name null
     */
    public static void createStore( String fileName, Map<?,?> config )
    {
        createEmptyStore( fileName, VERSION );
        int stringStoreBlockSize = 120;
        int arrayStoreBlockSize = 120;
        try
        {
            String stringBlockSize = (String) config.get( "string_block_size" );
            String arrayBlockSize = (String) config.get( "array_block_size" );
            if ( stringBlockSize != null )
            {
                int value = Integer.parseInt( stringBlockSize );
                if ( value > 0 )
                {
                    stringStoreBlockSize = value;
                }
            }
            if ( arrayBlockSize != null )
            {
                int value = Integer.parseInt( arrayBlockSize );
                if ( value > 0 )
                {
                    arrayStoreBlockSize = value;
                }
            }
        }
        catch ( Exception e )
        {
            e.printStackTrace();
        }

        DynamicStringStore.createStore( fileName + ".strings",
            stringStoreBlockSize );
        PropertyIndexStore.createStore( fileName + ".index" );
        DynamicArrayStore.createStore( fileName + ".arrays",
            arrayStoreBlockSize );
    }

    private int nextStringBlockId()
    {
        return stringPropertyStore.nextBlockId();
    }

    public void freeStringBlockId( int blockId )
    {
        stringPropertyStore.freeBlockId( blockId );
    }

    private int nextArrayBlockId()
    {
        return arrayPropertyStore.nextBlockId();
    }

    public void freeArrayBlockId( int blockId )
    {
        arrayPropertyStore.freeBlockId( blockId );
    }

    public PropertyIndexStore getIndexStore()
    {
        return propertyIndexStore;
    }

    public void updateRecord( PropertyRecord record, boolean recovered )
    {
        assert recovered;
        setRecovered();
        try
        {
            updateRecord( record );
        }
        finally
        {
            unsetRecovered();
        }
    }

    public void updateRecord( PropertyRecord record )
    {
        PersistenceWindow window = acquireWindow( record.getId(),
            OperationType.WRITE );
        try
        {
            updateRecord( record, window );
        }
        finally
        {
            releaseWindow( window );
        }
        if ( !record.isLight() )
        {
            for ( DynamicRecord valueRecord : record.getValueRecords() )
            {
                if ( valueRecord.getType() == PropertyType.STRING.intValue() )
                {
                    stringPropertyStore.updateRecord( valueRecord );
                }
                else if ( valueRecord.getType() == 
                    PropertyType.ARRAY.intValue() )
                {
                    arrayPropertyStore.updateRecord( valueRecord );
                }
                else
                {
                    throw new InvalidRecordException( "Unknown dynamic record" );
                }
            }
        }
    }

    // in_use(byte)+type(int)+key_blockId(int)+prop_blockId(long)+
    // prev_prop_id(int)+next_prop_id(int)

    private void updateRecord( PropertyRecord record, PersistenceWindow window )
    {
        int id = record.getId();
        Buffer buffer = window.getOffsettedBuffer( id );
        if ( record.inUse() )
        {
            buffer.put( Record.IN_USE.byteValue() ).putInt(
                record.getType().intValue() ).putInt( record.getKeyIndexId() )
                .putLong( record.getPropBlock() ).putInt( record.getPrevProp() )
                .putInt( record.getNextProp() );
        }
        else
        {
            buffer.put( Record.NOT_IN_USE.byteValue() );
            if ( !isInRecoveryMode() )
            {
                freeId( id );
            }
        }
    }

    public PropertyRecord getLightRecord( int id )
    {
        PersistenceWindow window = acquireWindow( id, OperationType.READ );
        try
        {
            PropertyRecord record = getRecord( id, window );
            record.setIsLight( true );
            return record;
        }
        finally
        {
            releaseWindow( window );
        }
    }

    public void makeHeavy( PropertyRecord record )
    {
        record.setIsLight( false );
        if ( record.getType() == PropertyType.STRING )
        {
            Collection<DynamicRecord> stringRecords = 
                stringPropertyStore.getLightRecords( 
                    (int) record.getPropBlock() );
            for ( DynamicRecord stringRecord : stringRecords )
            {
                stringRecord.setType( PropertyType.STRING.intValue() );
                record.addValueRecord( stringRecord );
            }
        }
        else if ( record.getType() == PropertyType.ARRAY )
        {
            Collection<DynamicRecord> arrayRecords = 
                arrayPropertyStore.getLightRecords( 
                    (int) record.getPropBlock() );
            for ( DynamicRecord arrayRecord : arrayRecords )
            {
                arrayRecord.setType( PropertyType.ARRAY.intValue() );
                record.addValueRecord( arrayRecord );
            }
        }
    }

    public PropertyRecord getRecord( int id )
    {
        PropertyRecord record;
        PersistenceWindow window = acquireWindow( id, OperationType.READ );
        try
        {
            record = getRecord( id, window );
        }
        finally
        {
            releaseWindow( window );
        }
        if ( record.getType() == PropertyType.STRING )
        {
            Collection<DynamicRecord> stringRecords = 
                stringPropertyStore.getLightRecords( 
                    (int) record.getPropBlock() );
            record.setIsLight( false );
            for ( DynamicRecord stringRecord : stringRecords )
            {
                stringRecord.setType( PropertyType.STRING.intValue() );
                record.addValueRecord( stringRecord );
            }
        }
        else if ( record.getType() == PropertyType.ARRAY )
        {
            Collection<DynamicRecord> arrayRecords = 
                arrayPropertyStore.getLightRecords( 
                    (int) record.getPropBlock() );
            record.setIsLight( false );
            for ( DynamicRecord arrayRecord : arrayRecords )
            {
                arrayRecord.setType( PropertyType.ARRAY.intValue() );
                record.addValueRecord( arrayRecord );
            }
        }
        return record;
    }

    private PropertyRecord getRecord( int id, PersistenceWindow window )
    {
        Buffer buffer = window.getOffsettedBuffer( id );
        if ( buffer.get() != Record.IN_USE.byteValue() )
        {
            throw new InvalidRecordException( "Record[" + id + "] not in use" );
        }
        PropertyRecord record = new PropertyRecord( id );
        record.setType( getEnumType( buffer.getInt() ) );
        record.setInUse( true );
        record.setKeyIndexId( buffer.getInt() );
        record.setPropBlock( buffer.getLong() );
        record.setPrevProp( buffer.getInt() );
        record.setNextProp( buffer.getInt() );
        return record;
    }

    private PropertyType getEnumType( int type )
    {
        switch ( type )
        {
            case 1:
                return PropertyType.INT;
            case 2:
                return PropertyType.STRING;
            case 3:
                return PropertyType.BOOL;
            case 4:
                return PropertyType.DOUBLE;
            case 5:
                return PropertyType.FLOAT;
            case 6:
                return PropertyType.LONG;
            case 7:
                return PropertyType.BYTE;
            case 8:
                return PropertyType.CHAR;
            case 9:
                return PropertyType.ARRAY;
            case 10:
                return PropertyType.SHORT;
            default:
                throw new InvalidRecordException( "Unknown enum type:" + type );
        }
    }
    
    public Object getValue( PropertyRecord propertyRecord )
    {
        PropertyType type = propertyRecord.getType();
        if ( type == PropertyType.INT )
        {
            return (int) propertyRecord.getPropBlock();
        }
        if ( type == PropertyType.STRING )
        {
            return getStringFor( propertyRecord );
        }
        if ( type == PropertyType.BOOL )
        {
            if ( propertyRecord.getPropBlock() == 1 )
            {
                return Boolean.valueOf( true );
            }
            return Boolean.valueOf( false );
        }
        if ( type == PropertyType.DOUBLE )
        {
            return new Double( Double.longBitsToDouble( 
                propertyRecord.getPropBlock() ) );
        }
        if ( type == PropertyType.FLOAT )
        {
            return new Float( Float.intBitsToFloat( 
                (int) propertyRecord.getPropBlock() ) );
        }
        if ( type == PropertyType.LONG )
        {
            return propertyRecord.getPropBlock();
        }
        if ( type == PropertyType.BYTE )
        {
            return (byte) propertyRecord.getPropBlock();
        }
        if ( type == PropertyType.CHAR )
        {
            return (char) propertyRecord.getPropBlock();
        }
        if ( type == PropertyType.ARRAY )
        {
            return getArrayFor( propertyRecord );
        }
        if ( type == PropertyType.SHORT )
        {
            return (short) propertyRecord.getPropBlock();
        }
        throw new InvalidRecordException( "Unknown type: " + type  + 
            " for record " + propertyRecord );
    }

    @Override
    public void makeStoreOk()
    {
        propertyIndexStore.makeStoreOk();
        stringPropertyStore.makeStoreOk();
        arrayPropertyStore.makeStoreOk();
        super.makeStoreOk();
    }

    @Override
    public void rebuildIdGenerators()
    {
        propertyIndexStore.rebuildIdGenerators();
        stringPropertyStore.rebuildIdGenerators();
        arrayPropertyStore.rebuildIdGenerators();
        super.rebuildIdGenerators();
    }
    
    public void updateIdGenerators()
    {
        propertyIndexStore.updateIdGenerators();
        stringPropertyStore.updateHighId();
        arrayPropertyStore.updateHighId();
        this.updateHighId();
    }    
    
    private Collection<DynamicRecord> allocateStringRecords( int valueBlockId,
        char[] chars )
    {
        return stringPropertyStore.allocateRecords( valueBlockId, chars );
    }

    private Collection<DynamicRecord> allocateArrayRecords( int valueBlockId,
        Object array )
    {
        return arrayPropertyStore.allocateRecords( valueBlockId, array );
    }

    public void encodeValue( PropertyRecord record, Object value )
    {
        if ( value instanceof String )
        {
            int stringBlockId = nextStringBlockId();
            record.setPropBlock( stringBlockId );
            String string = (String) value;
            int length = string.length();
            char[] chars = new char[length];
            string.getChars( 0, length, chars, 0 );
            Collection<DynamicRecord> valueRecords = allocateStringRecords(
                stringBlockId, chars );
            for ( DynamicRecord valueRecord : valueRecords )
            {
                valueRecord.setType( PropertyType.STRING.intValue() );
                record.addValueRecord( valueRecord );
            }
            record.setType( PropertyType.STRING );
        }
        else if ( value instanceof Integer )
        {
            record.setPropBlock( ((Integer) value).intValue() );
            record.setType( PropertyType.INT );
        }
        else if ( value instanceof Boolean )
        {
            record.setPropBlock( (((Boolean) value).booleanValue() ? 1 : 0) );
            record.setType( PropertyType.BOOL );
        }
        else if ( value instanceof Float )
        {
            record.setPropBlock( Float.floatToRawIntBits( ((Float) value)
                .floatValue() ) );
            record.setType( PropertyType.FLOAT );
        }
        else if ( value instanceof Long )
        {
            record.setPropBlock( ((Long) value).longValue() );
            record.setType( PropertyType.LONG );
        }
        else if ( value instanceof Double )
        {
            record.setPropBlock( Double.doubleToRawLongBits( ((Double) value)
                .doubleValue() ) );
            record.setType( PropertyType.DOUBLE );
        }
        else if ( value instanceof Byte )
        {
            record.setPropBlock( ((Byte) value).byteValue() );
            record.setType( PropertyType.BYTE );
        }
        else if ( value instanceof Character )
        {
            record.setPropBlock( ((Character) value).charValue() );
            record.setType( PropertyType.CHAR );
        }
        else if ( value.getClass().isArray() )
        {
            int arrayBlockId = nextArrayBlockId();
            record.setPropBlock( arrayBlockId );
            Collection<DynamicRecord> arrayRecords = allocateArrayRecords(
                arrayBlockId, value );
            for ( DynamicRecord valueRecord : arrayRecords )
            {
                valueRecord.setType( PropertyType.ARRAY.intValue() );
                record.addValueRecord( valueRecord );
            }
            record.setType( PropertyType.ARRAY );
        }
        else if ( value instanceof Short )
        {
            record.setPropBlock( ((Short) value).shortValue() );
            record.setType( PropertyType.SHORT );
        }
        else
        {
            throw new IllegalArgumentException( "Unknown property type on: "
                + value );
        }
    }

    public Object getStringFor( PropertyRecord propRecord )
    {
        int recordToFind = (int) propRecord.getPropBlock();
        Map<Integer,DynamicRecord> recordsMap = new HashMap<Integer,DynamicRecord>();
        for ( DynamicRecord record : propRecord.getValueRecords() )
        {
            recordsMap.put( record.getId(), record );
        }
        List<char[]> charList = new LinkedList<char[]>();
        int totalSize = 0;
        while ( recordToFind != Record.NO_NEXT_BLOCK.intValue() )
        {
            DynamicRecord record = recordsMap.get( recordToFind );
            if ( record.isLight() )
            {
                stringPropertyStore.makeHeavy( record );
            }
            if ( !record.isCharData() )
            {
                ByteBuffer buf = ByteBuffer.wrap( record.getData() );
                char[] chars = new char[record.getData().length / 2];
                totalSize += chars.length;
                buf.asCharBuffer().get( chars );
                charList.add( chars );
            }
            else
            {
                charList.add( record.getDataAsChar() );
            }
            recordToFind = record.getNextBlock();
        }
        StringBuffer buf = new StringBuffer();
        for ( char[] str : charList )
        {
            buf.append( str );
        }
        return buf.toString();
    }

    public Object getArrayFor( PropertyRecord propertyRecord )
    {
        int recordToFind = (int) propertyRecord.getPropBlock();
        Map<Integer,DynamicRecord> recordsMap = new HashMap<Integer,DynamicRecord>();
        for ( DynamicRecord record : propertyRecord.getValueRecords() )
        {
            recordsMap.put( record.getId(), record );
        }
        List<byte[]> byteList = new LinkedList<byte[]>();
        int totalSize = 0;
        while ( recordToFind != Record.NO_NEXT_BLOCK.intValue() )
        {
            DynamicRecord record = recordsMap.get( recordToFind );
            if ( record.isLight() )
            {
                arrayPropertyStore.makeHeavy( record );
            }
            if ( !record.isCharData() )
            {
                ByteBuffer buf = ByteBuffer.wrap( record.getData() );
                byte[] bytes = new byte[record.getData().length];
                totalSize += bytes.length;
                buf.get( bytes );
                byteList.add( bytes );
            }
            else
            {
                throw new InvalidRecordException( 
                    "Expected byte data on record " + record );
            }
            recordToFind = record.getNextBlock();
        }
        byte[] bArray = new byte[totalSize];
        int offset = 0;
        for ( byte[] currentArray : byteList )
        {
            System.arraycopy( currentArray, 0, bArray, offset,
                currentArray.length );
            offset += currentArray.length;
        }
        return arrayPropertyStore.getRightArray( bArray );
    }
    
    @Override
    protected boolean versionFound( String version )
    {
        if ( !version.startsWith( "PropertyStore" ) )
        {
            // non clean shutdown, need to do recover with right neo
            return false;
        }
        if ( version.equals( "PropertyStore v0.9.3" ) )
        {
            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 List<WindowPoolStats> getAllWindowPoolStats()
    {
        List<WindowPoolStats> list = new ArrayList<WindowPoolStats>();
        list.add( stringPropertyStore.getWindowPoolStats() );
        list.add( arrayPropertyStore.getWindowPoolStats() );
        list.add( getWindowPoolStats() );
        return list;
    }
    
    public int getStringBlockSize()
    {
        return stringPropertyStore.getBlockSize();
    }
    
    public int getArrayBlockSize()
    {
        return arrayPropertyStore.getBlockSize();
    }
}