/*
 * Copyright (c) 2002-2010 "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;

import java.io.File;
import java.lang.management.ManagementFactory;
import java.lang.management.OperatingSystemMXBean;
import java.lang.reflect.Method;
import java.util.Map;

public class AutoConfigurator
{
    private final int totalPhysicalMemMb;
    private final int maxVmUsageMb;
    private final String dbPath;
    private final boolean useMemoryMapped;

    public AutoConfigurator( String dbPath, boolean useMemoryMapped, boolean dump )
    {
        this.dbPath = dbPath;
        this.useMemoryMapped = useMemoryMapped;
        OperatingSystemMXBean osBean =
            ManagementFactory.getOperatingSystemMXBean();
        long mem = -1;
        try
        {
            Class<?> beanClass =
                Class.forName( "com.sun.management.OperatingSystemMXBean" );
            Method method = beanClass.getMethod( "getTotalPhysicalMemorySize" );
            mem = (Long) method.invoke( osBean );
        }
        catch ( Exception e )
        { // ok we tried but probably 1.5 JVM or other class library implementation
        }
        if ( mem != -1 )
        {
            totalPhysicalMemMb = (int) (mem / 1024 / 1024 );
        }
        else
        {
            totalPhysicalMemMb = -1;
        }
        mem = Runtime.getRuntime().maxMemory();
        maxVmUsageMb = (int) ( mem / 1024 / 1024 );
        if ( dump )
        {
            System.out.println( "Physical mem: " + totalPhysicalMemMb + "MB" );
            System.out.println( "Heap size: " + maxVmUsageMb + "MB" );
        }
    }

    public void configure( Map<Object,Object> config )
    {
        if ( totalPhysicalMemMb > 0 )
        {
            if ( useMemoryMapped )
            {
                int availableMem = (totalPhysicalMemMb - maxVmUsageMb );
                // leave 15% for OS and other progs
                availableMem -= (int) ( availableMem * 0.15f );
                assignMemory( config, availableMem );
            }
            else
            {
                // use half of heap (if needed) for buffers
                assignMemory( config, maxVmUsageMb / 2 );
            }
        }
    }

    private int calculate( int memLeft, int storeSize, float use, float expand,
            boolean canExpand )
    {
        int size = memLeft;
        if ( storeSize > (memLeft * use) )
        {
            size = (int) (memLeft * use);
        }
        else if ( canExpand  )
        {
            if ( (storeSize * expand * 5 <  memLeft * use ) )
            {
                size = (int) (memLeft * use / 5);
            }
            else
            {
                size = (int) (memLeft * use);
            }
        }
        else
        {
            size = storeSize;
        }
        return size;
    }

    private void assignMemory( Map<Object, Object> config, int availableMem )
    {
        int nodeStore = getFileSizeMb( "nodestore.db" );
        int relStore = getFileSizeMb( "relationshipstore.db" );
        int propStore = getFileSizeMb( "propertystore.db" );
        int stringStore = getFileSizeMb( "propertystore.db.strings" );
        int arrayStore = getFileSizeMb( "propertyStore.db.arrays" );

        int totalSize =
            nodeStore + relStore + propStore + stringStore + arrayStore;
        boolean expand = false;
        if ( totalSize * 1.15f < availableMem )
        {
            expand = true;
        }
        int memLeft = availableMem;
        relStore = calculate( memLeft, relStore, 0.75f, 1.1f, expand );
        memLeft -= relStore;
        nodeStore = calculate( memLeft, nodeStore, 0.2f, 1.1f, expand );
        memLeft -= nodeStore;
        propStore = calculate( memLeft, propStore, 0.75f, 1.1f, expand );
        memLeft -= propStore;
        stringStore = calculate( memLeft, stringStore, 0.75f, 1.1f, expand );
        memLeft -= stringStore;
        arrayStore = calculate( memLeft, arrayStore, 1.0f, 1.1f, expand );
        memLeft -= arrayStore;

        configPut( config, "nodestore.db", nodeStore );
        configPut( config, "relationshipstore.db", relStore );
        configPut( config, "propertystore.db", propStore );
        configPut( config, "propertystore.db.strings", stringStore );
        configPut( config, "propertystore.db.arrays", arrayStore );
    }

    private void configPut( Map<Object, Object> config, String store,
            int size )
    {
        config.put( "neostore." + store + ".mapped_memory", size + "M" );
    }

    private int getFileSizeMb( String file )
    {
        long length = new File( dbPath + "/neostore." + file ).length();
        int mb = (int) ( length / 1024 / 1024 );
        if ( mb > 0 )
        {
            return mb;
        }
        // default return 1MB if small or empty file
        return 1;
    }
}