/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.jena.tdb.sys;

import java.io.FileNotFoundException ;
import java.io.IOException ;
import java.nio.ByteOrder ;
import java.util.Properties ;

import org.apache.jena.atlas.io.IO ;
import org.apache.jena.atlas.lib.PropertyUtils ;
import org.apache.jena.atlas.logging.Log ;
import org.apache.jena.query.ARQ ;
import org.apache.jena.sparql.engine.optimizer.reorder.ReorderLib ;
import org.apache.jena.sparql.engine.optimizer.reorder.ReorderTransformation ;
import org.apache.jena.sparql.util.Symbol ;
import org.apache.jena.system.JenaSystem ;
import org.apache.jena.tdb.TDB ;
import org.apache.jena.tdb.TDBException ;
import org.apache.jena.tdb.base.block.FileMode ;
import org.apache.jena.tdb.base.record.RecordFactory ;
import org.apache.jena.tdb.store.NodeId ;
import org.slf4j.Logger ;
import org.slf4j.LoggerFactory ;

public class SystemTDB
{
    static { JenaSystem.init(); }
    
    // NB Same logger as the TDB class because this class is the system info but kept out of TDB javadoc.
    // It's visibility is TDB, not really public. 
    private static final Logger log = LoggerFactory.getLogger(TDB.class) ;
    
    /** TDB System log - use for general messages (a few) and warnings.
     *  Generally, do not log events unless you want every user to see them every time.
     *  TDB is an embedded database - libraries and embedded systems should be seen and not heard.
     *  @see #errlog 
     */
    // This was added quite late in TDB so need to check it's used appropriately - check for Log.*
    public static final Logger syslog = LoggerFactory.getLogger("TDB") ;
    /** Send warnings and error */
    public static final Logger errlog = LoggerFactory.getLogger("TDB") ;
    
    // ---- Constants that can't be changed without invalidating on-disk data.  
    
    /** Size, in bytes, of a Java long */
    public static final int SizeOfLong              = Long.SIZE/Byte.SIZE ;
    
    /** Size, in bytes, of a Java int */
    public static final int SizeOfInt               = Integer.SIZE/Byte.SIZE ;
    
    /** Size, in bytes, of the persistent representation of a node id */
    public static final int SizeOfNodeId            = NodeId.SIZE ;

    /** Size, in bytes, of a pointer between blocks */
    public static final int SizeOfPointer           = SizeOfInt ;
    
    // ---- Node table related
    
    /** Size, in bytes, of a triple index record. */
    public static final int LenIndexTripleRecord    = 3 * NodeId.SIZE ;
    /** Size, in bytes, of a quad index record. */
    public static final int LenIndexQuadRecord      = 4 * NodeId.SIZE ;
    
    /** Size, in bytes, of a Node hash.
     * In TDB 0.7.X and before this was 8 bytes (64/8).
     * In TDB 0.8.0 and above it is 16 bytes (128/8).
     * These two systems are not compatible.
     */
    //public static final int LenNodeHash             = SizeOfLong ; // TDB <= 0.7.X
    public static final int LenNodeHash             = 128/8 ; // TDB >= 0.8.0

    // ---- Symbols and similar
    
    // ---- Record factories
    public final static RecordFactory indexRecordTripleFactory = new RecordFactory(LenIndexTripleRecord, 0) ;
    public final static RecordFactory indexRecordQuadFactory = new RecordFactory(LenIndexQuadRecord, 0) ;
    public final static RecordFactory nodeRecordFactory = new RecordFactory(LenNodeHash, SizeOfNodeId) ;

    // Test show no visable effect.
//    /** Unit of flushing the data when loading - data phase.  -1 means off, sync at end of load only.  */
//    public final static long LoadFlushTickPrimary   = -1 ; // 10*1000*1000 ;
//    /** Unit of flushing the data when loading - index phase  -1 means off, sync at end of load only.  */
//    public final static long LoadFlushTickSecondary = -1 ; // 10*1000*1000 ;

    /** Root of TDB-defined parameter names */
    public static final String symbolNamespace      = "http://jena.hpl.hp.com/TDB#" ;

    /** Root of TDB-defined parameter short names */  
    public static final String tdbSymbolPrefix      = "tdb" ;
    
    /** Root of any TDB-defined Java system properties */   
    public static final String tdbPropertyRoot      = "org.apache.jena.tdb" ;

    /** Log duplicates during loading */
    public static final Symbol symLogDuplicates     = allocSymbol("logDuplicates") ;

    /** File mode : one of "direct", "mapped", "default" */ 
    public static final Symbol symFileMode          = allocSymbol("fileMode") ;

    /** Index type */
    public static final Symbol symIndexType         = allocSymbol("indexType") ;

    /** Experimental : triple and quad filtering at scan level */
    public static final Symbol symTupleFilter       = allocSymbol("tupleFilter") ;

    private static final String propertyFileKey1    = tdbPropertyRoot+".settings" ;
    private static final String propertyFileKey2    = tdbSymbolPrefix+":settings" ;

    private static String propertyFileName = null ;
    static {
        propertyFileName = System.getProperty(propertyFileKey1) ;
        if ( propertyFileName == null )
            propertyFileName = System.getProperty(propertyFileKey2) ;
    }
    
    public static final boolean is64bitSystem = determineIf64Bit() ;

    private static Properties properties = readPropertiesFile() ;

    /** Size, in bytes, of a block */
    public static final int BlockSize               = 8*1024 ; // intValue("BlockSize", 8*1024) ;

    /** Size, in bytes, of a block for testing */
    public static final int BlockSizeTest           = 1024 ; // intValue("BlockSizeTest", 1024) ;

    /** Size, in bytes, of a block for testing */
    public static final int BlockSizeTestMem         = 500 ;

//    /** Size, in bytes, of a memory block */
//    public static final int BlockSizeMem            = 32*8 ; //intValue("BlockSizeMem", 32*8 ) ;

    /** order of an in-memory BTree or B+Tree */
    public static final int OrderMem                = 5 ; // intValue("OrderMem", 5) ;
    
    /** Size, in bytes, of a segment (used for memory mapped files) */
    public static final int SegmentSize             = 8*1024*1024 ; // intValue("SegmentSize", 8*1024*1024) ;
    
    // ---- Cache sizes (within the JVM)
    
    public static final int ObjectFileWriteCacheSize = 8*1024 ;
    
    /** Size of Node to NodeId cache.
     *  Used to map from Node to NodeId spaces.
     *  Used for loading and for query preparation.
     */
    public static final int Node2NodeIdCacheSize    = intValue("Node2NodeIdCacheSize", ( is64bitSystem ? 100*1000 : 50*1000 )) ;

    /** Size of NodeId to Node cache.
     *  Used to map from NodeId to Node spaces.
     *  Used for retriveing results.
     */
    public static final int NodeId2NodeCacheSize    = intValue("NodeId2NodeCacheSize", ( is64bitSystem ? 500*1000 : 50*1000 ) ) ;
    
    /** Size of Node lookup miss cache. */
    public static final int NodeMissCacheSize       = 100 ;
    
    /** Size of the delayed-write block cache (32 bit systems only) (per file) */
    public static final int BlockWriteCacheSize     = intValue("BlockWriteCacheSize", 2*1000) ;

    /** Size of read block cache (32 bit systems only).  Increase JVM size as necessary. Per file. */
    public static final int BlockReadCacheSize      = intValue("BlockReadCacheSize", 10*1000) ;
    
    // ---- Misc
    
//    /** Number of adds/deletes between calls to sync (-ve to disable) */
//    public static final int SyncTick                = intValue("SyncTick", -1) ;

    /** Default BGP optimizer */
    public static ReorderTransformation defaultReorderTransform = ReorderLib.fixed() ;

    public static final ByteOrder NetworkOrder      = ByteOrder.BIG_ENDIAN ;
    
    /** Unsupported (for non-standard setups) 
     * @see #enableInlineLiterals
     */
    public static String propertyEnableInlineLiterals1 = "org.apache.jena.tdb.store.enableInlineLiterals" ;
    /** Unsupported (for non-standard setups) 
     * @see #enableInlineLiterals
     */
    public static String propertyEnableInlineLiterals2 = "tdb:store.enableInlineLiterals" ;
    /** <b>Unsupported</b> (for non-standard setups).
     * This controls whether literal values are inlined into NodeIds.
     * This is a major efficiency boost and is the default setting.
     * It can be set false with {@code -Dtdb:store.enableInlineLiterals=false}. 
     * Do not mix databases created with this set to different values.
     * Chaos and incorrect results will result.
     * Use with care. No support. 
     * Default setting is {@code true}   
     */
    public static final boolean enableInlineLiterals ;
    static { // Set enableInlineLiterals from system properties.
        Properties sysProperties = System.getProperties() ;
        String key = null ; 
        if ( sysProperties.containsKey(propertyEnableInlineLiterals1) )
            key = propertyFileKey1 ;
        else if ( sysProperties.containsKey(propertyEnableInlineLiterals2) )
            key = propertyFileKey2 ;
        if ( key == null )
            enableInlineLiterals = true ;  // Normal value. 
        else
            enableInlineLiterals = Boolean.valueOf(sysProperties.getProperty(key)) ;
    }

//    public static void setNullOut(boolean nullOut)
//    { SystemTDB.NullOut = nullOut ; }
//
//    /** Are we nulling out unused space in bytebuffers (records, points etc) */ 
//    public static boolean getNullOut()
//    { return SystemTDB.NullOut ; }

    /** null out (with the FillByte) freed up space in buffers */
    public static boolean NullOut = false ;
    
    /** FillByte value for NullOut */
    public static final byte FillByte = (byte)0xFF ;

    public static boolean Checking = false ;       // This isn't used enough!
    
    /**
     * New feature introduced by JENA-648 to help prevent one common cause of TDB corruption.
     * <p>
     * When enabled lock files are written to disk locations with the current owner process PID,
     * other processes will refuse to access that location while another live process owns the PID.
     * </p>
     */
    public static boolean DiskLocationMultiJvmUsagePrevention = true;

    // BDB related.
    //public static final int BDB_cacheSizePercent    = intValue("BDB_cacheSizePercent", 75) ;
    
    public static void panic(Class<?> clazz, String string)
    {
        Log.error(clazz, string) ;
        throw new TDBException(string) ;
    }
    
    public static Symbol allocSymbol(String shortName)
    { 
        if ( shortName.startsWith(SystemTDB.tdbSymbolPrefix)) 
            throw new TDBException("Symbol short name begins with the TDB namespace prefix: "+shortName) ;
        if ( shortName.startsWith("http:")) 
            throw new TDBException("Symbol short name begins with http: "+shortName) ;
        return allocSymbol(SystemTDB.symbolNamespace, shortName) ;
    }
    
    private static Symbol allocSymbol(String namespace, String shortName)
    {
        return Symbol.create(namespace+shortName) ;
    }
    
    // ----
    
    private static int intValue(String prefix, String name, int defaultValue)
    {
        if ( ! prefix.endsWith(".") )
            name = prefix+"."+name ;
        else
            name = prefix+name ;
        return intValue(name, defaultValue) ;
    }
    
    private static int intValue(String name, int defaultValue)
    {
        if ( name == null ) return defaultValue ;
        if ( name.length() == 0 ) throw new TDBException("Empty string for value name") ;
        
        if ( properties == null )
            return defaultValue ;

        String x = properties.getProperty(name) ;
        if ( x == null )
            return defaultValue ; 
        TDB.logInfo.info("Set: "+name+" = "+x) ;
        int v = Integer.parseInt(x) ;
        return v ;
    }

    private static Properties readPropertiesFile()
    {
        if ( propertyFileName == null )
            return null ;
        
        Properties p = new Properties() ;
        try
        {
            TDB.logInfo.info("Using properties from '"+propertyFileName+"'") ;
            PropertyUtils.loadFromFile(p, propertyFileName) ;
        } catch (FileNotFoundException ex)
        { 
            log.debug("No system properties file ("+propertyFileName+")") ;
            return null ;
        } catch (IOException ex) { IO.exception(ex) ; }
        return p ;
    }

    // --------
    
    public static final boolean isWindows = determineIfWindows() ;	// Memory mapped files behave differently.

    //Or look in File.listRoots.
    //Alternative method:
    //  http://stackoverflow.com/questions/1293533/name-of-the-operating-system-in-java-not-os-name
    
    private static boolean determineIfWindows() {
    	String s = System.getProperty("os.name") ;
    	if ( s == null )
    		return false ;
    	return s.startsWith("Windows ") ;
	}

    private static boolean determineIf64Bit()
    {
        String s = System.getProperty("sun.arch.data.model") ;
        if ( s != null )
        {
            boolean b = s.equals("64") ; 
            TDB.logInfo.debug("System architecture: "+(b?"64 bit":"32 bit")) ;
            return b ;
        }
        // Not a SUN VM
        s = System.getProperty("java.vm.info") ;
        if ( s == null )
        {
            log.warn("Can't determine the data model") ;
            return false ;    
        }
        log.debug("Can't determine the data model from 'sun.arch.data.model' - using java.vm.info") ;
        boolean b = s.contains("64") ;
        TDB.logInfo.debug("System architecture: (from java.vm.info) "+(b?"64 bit":"32 bit")) ;
        return b ;
    }
    
    // ---- File mode
    
    private static FileMode fileMode = null ;
    public static FileMode fileMode()
    { 
        if ( fileMode == null )
            fileMode = determineFileMode() ;
        return fileMode ;
    }

    public static void setFileMode(FileMode newFileMode)
    {
        if ( fileMode != null )
        {
            log.warn("System file mode already determined - setting it has no effect") ;
            return ;
        }
        fileMode = newFileMode ;
    }
    
    // So the test suite can setup thing up ... very carefully.
    /*package*/ static void internalSetFileMode(FileMode newFileMode)
    {
        fileMode = newFileMode ;
    }
    
    private static FileMode determineFileMode()
    {
        // Be careful that this is not called very, very early, before --set might be seen.
        // Hence delayed access above in fileMode().
        
        String x = ARQ.getContext().getAsString(SystemTDB.symFileMode, "default") ;

        if ( x.equalsIgnoreCase("direct") )
        {
            TDB.logInfo.info("File mode: direct (forced)") ;
            return FileMode.direct ;
        }
        if ( x.equalsIgnoreCase("mapped") )
        {
            TDB.logInfo.info("File mode: mapped (forced)") ;
            return FileMode.mapped ;
        }
        
        if ( x.equalsIgnoreCase("default") )
        {
            if ( is64bitSystem )
            {
                TDB.logInfo.debug("File mode: Mapped") ;
                return FileMode.mapped ;
            }
            TDB.logInfo.debug("File mode: Direct") ;
            return FileMode.direct ;
        }
        throw new TDBException("Unrecognized file mode (not one of 'default', 'direct' or 'mapped': "+x) ;
    }
}
