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

import java.io.File;

import org.apache.pdfbox.io.RandomAccessStreamCache.StreamCacheCreateFunction;

/**
 * Controls how memory/temporary files are used for
 * buffering streams etc.
 */
public final class MemoryUsageSetting
{
    private final boolean useMainMemory;
    private final boolean useTempFile;
    
    /** maximum number of main-memory bytes allowed to be used;
     *  <code>-1</code> means 'unrestricted' */
    private final long maxMainMemoryBytes;
    
    /** maximum number of bytes allowed for storage at all (main-memory+file);
     *  <code>-1</code> means 'unrestricted' */
    private final long maxStorageBytes;
    
    /** directory to be used for scratch file */
    private File tempDir;

    /**
     * Implementation of the function to create an instance of ScratchFile using the current settings.
     */
    public final StreamCacheCreateFunction streamCache = () -> new ScratchFile(this);

    /**
     * Private constructor for setup buffering memory usage called by one of the setup methods.
     * 
     * @param useMainMemory if <code>true</code> main memory usage is enabled; in case of
     *                      <code>false</code> and <code>useTempFile</code> is <code>false</code> too
     *                      we set this to <code>true</code>
     * @param useTempFile if <code>true</code> using of temporary file(s) is enabled
     * @param maxMainMemoryBytes maximum number of main-memory to be used;
     *                           if <code>-1</code> means 'unrestricted';
     *                           if <code>0</code> we only use temporary file if <code>useTempFile</code>
     *                           is <code>true</code> otherwise main-memory usage will have restriction
     *                           defined by maxStorageBytes
     * @param maxStorageBytes maximum size the main-memory and temporary file(s) may have all together;
     *                        <code>0</code>  or less will be ignored; if it is less than
     *                        maxMainMemoryBytes we use maxMainMemoryBytes value instead 
     */
    private MemoryUsageSetting(boolean useMainMemory, boolean useTempFile,
                        long maxMainMemoryBytes, long maxStorageBytes)
    {
        // do some checks; adjust values as needed to get consistent setting
        boolean locUseMainMemory = !useTempFile || useMainMemory;
        long    locMaxMainMemoryBytes = useMainMemory ? maxMainMemoryBytes : -1;
        long    locMaxStorageBytes = maxStorageBytes > 0 ? maxStorageBytes : -1;
        
        if (locMaxMainMemoryBytes < -1)
        {
            locMaxMainMemoryBytes = -1;
        }
        
        if (locUseMainMemory && (locMaxMainMemoryBytes == 0))
        {
            if (useTempFile) {
                locUseMainMemory = false;
            }
            else
            {
                locMaxMainMemoryBytes = locMaxStorageBytes;
            }
        }
        
        if (locUseMainMemory && (locMaxStorageBytes > -1) &&
            ((locMaxMainMemoryBytes == -1) || (locMaxMainMemoryBytes > locMaxStorageBytes)))
        {
            locMaxStorageBytes = locMaxMainMemoryBytes;
        }
            
        
        this.useMainMemory = locUseMainMemory;
        this.useTempFile = useTempFile;
        this.maxMainMemoryBytes = locMaxMainMemoryBytes;
        this.maxStorageBytes = locMaxStorageBytes;
    }
    
    /**
     * Setups buffering memory usage to only use main-memory (no temporary file) which is not restricted in size.
     * 
     * @return returns an instance of MemoryUsageSetting set up to use unlimited memory
     */
    public static MemoryUsageSetting setupMainMemoryOnly()
    {
        return setupMainMemoryOnly(-1);
    }

    /**
     * Setups buffering memory usage to only use main-memory with the defined maximum.
     * 
     * @param maxMainMemoryBytes maximum number of main-memory to be used; <code>-1</code> for no restriction;
     * <code>0</code> will also be interpreted here as no restriction
     * 
     * @return returns an instance of MemoryUsageSetting set up to use main memory
     */
    public static MemoryUsageSetting setupMainMemoryOnly(long maxMainMemoryBytes)
    {
        return new MemoryUsageSetting(true, false, maxMainMemoryBytes, maxMainMemoryBytes);
    }

    /**
     * Setups buffering memory usage to only use temporary file(s) (no main-memory) with not restricted size.
     * 
     * @return returns an instance of MemoryUsageSetting set up to use temporary files with not restricted size
     */
    public static MemoryUsageSetting setupTempFileOnly()
    {
        return setupTempFileOnly(-1);
    }
    
    /**
     * Setups buffering memory usage to only use temporary file(s) (no main-memory) with the specified maximum size.
     * 
     * @param maxStorageBytes maximum size the temporary file(s) may have all together; <code>-1</code> for no
     * restriction; <code>0</code> will also be interpreted here as no restriction
     * 
     * @return returns an instance of MemoryUsageSetting set up to use temporary files with restricted size
     */
    public static MemoryUsageSetting setupTempFileOnly(long maxStorageBytes)
    {
        return new MemoryUsageSetting(false, true, 0, maxStorageBytes);
    }
    
    /**
     * Setups buffering memory usage to use a portion of main-memory and additionally temporary file(s) in case the
     * specified portion is exceeded.
     * 
     * @param maxMainMemoryBytes maximum number of main-memory to be used; if <code>-1</code> this is the same as
     * {@link #setupMainMemoryOnly()}; if <code>0</code> this is the same as {@link #setupTempFileOnly()}
     * 
     * @return returns an instance of MemoryUsageSetting set up to use a mixed setting
     */
    public static MemoryUsageSetting setupMixed(long maxMainMemoryBytes)
    {
        return setupMixed(maxMainMemoryBytes, -1);
    }
    
    /**
     * Setups buffering memory usage to use a portion of main-memory and additionally temporary file(s) in case the
     * specified portion is exceeded.
     * 
     * @param maxMainMemoryBytes maximum number of main-memory to be used; if <code>-1</code> this is the same as
     * {@link #setupMainMemoryOnly()}; if <code>0</code> this is the same as {@link #setupTempFileOnly()}
     * @param maxStorageBytes maximum size the main-memory and temporary file(s) may have all together; <code>0</code>
     * or less will be ignored; if it is less than maxMainMemoryBytes we use maxMainMemoryBytes value instead
     * 
     * @return returns an instance of MemoryUsageSetting set up to use a mixed setting
     */
    public static MemoryUsageSetting setupMixed(long maxMainMemoryBytes, long maxStorageBytes)
    {
        return new MemoryUsageSetting(true, true, maxMainMemoryBytes, maxStorageBytes);
    }

    /**
     * Sets directory to be used for temporary files.
     * 
     * @param tempDir directory for temporary files
     * 
     * @return this instance
     */
    public MemoryUsageSetting setTempDir(File tempDir)
    {
        this.tempDir = tempDir;
        return this;
    }
    
    /**
     * Returns <code>true</code> if main-memory is to be used.
     * 
     * <p>
     * If this returns <code>false</code> it is ensured {@link #useTempFile()} returns <code>true</code>.
     * </p>
     * 
     * @return true if this instance is set up to use main memory
     */
    public boolean useMainMemory()
    {
        return useMainMemory;
    }
    
    /**
     * Returns <code>true</code> if temporary file is to be used.
     * 
     * <p>
     * If this returns <code>false</code> it is ensured {@link #useMainMemory} returns <code>true</code>.
     * </p>
     * 
     * @return true if this instance is set up to use temporary files
     */
    public boolean useTempFile()
    {
        return useTempFile;
    }
    
    /**
     * Returns <code>true</code> if maximum main memory is restricted to a specific number of bytes.
     * 
     * @return true if this instance is set up to restrict main memory
     */
    public boolean isMainMemoryRestricted()
    {
        return maxMainMemoryBytes >= 0;
    }
    
    /**
     * Returns <code>true</code> if maximum amount of storage is restricted to a specific number of bytes.
     * 
     * @return true if this instance is set up to restrict storage size
     */
    public boolean isStorageRestricted()
    {
        return maxStorageBytes > 0;
    }
    
    /**
     * Returns maximum size of main-memory in bytes to be used.
     * 
     * @return the maximum number of main-memory in bytes to be used
     */
    public long getMaxMainMemoryBytes()
    {
        return maxMainMemoryBytes;
    }
    
    /**
     * Returns maximum size of storage bytes to be used (main-memory in temporary files all together).
     * 
     * @return the maximum size of storage bytes to be used
     */
    public long getMaxStorageBytes()
    {
        return maxStorageBytes;
    }
    
    /**
     * Returns directory to be used for temporary files or <code>null</code> if it was not set.
     * 
     * @return the temp dir to bes used for temporary files or null
     */
    public File getTempDir()
    {
        return tempDir;
    }
    
    @Override
    public String toString()
    {
        return useMainMemory ?
                   (useTempFile ? "Mixed mode with max. of " + maxMainMemoryBytes + " main memory bytes" +
                                  (isStorageRestricted() ? " and max. of " + maxStorageBytes + " storage bytes" :
                                                           " and unrestricted scratch file size") :
                                  (isMainMemoryRestricted() ? "Main memory only with max. of " + maxMainMemoryBytes + " bytes" :
                                                              "Main memory only with no size restriction")):
                   (isStorageRestricted() ? "Scratch file only with max. of " + maxStorageBytes + " bytes" :
                                            "Scratch file only with no size restriction");
    }
}
