/*
 * 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.jackrabbit.core.persistence.mem;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;

import javax.jcr.RepositoryException;

import org.apache.commons.io.IOUtils;
import org.apache.jackrabbit.core.fs.FileSystem;
import org.apache.jackrabbit.core.fs.FileSystemResource;
import org.apache.jackrabbit.core.fs.local.LocalFileSystem;
import org.apache.jackrabbit.core.id.NodeId;
import org.apache.jackrabbit.core.id.PropertyId;
import org.apache.jackrabbit.core.persistence.PMContext;
import org.apache.jackrabbit.core.persistence.bundle.AbstractBundlePersistenceManager;
import org.apache.jackrabbit.core.persistence.util.BLOBStore;
import org.apache.jackrabbit.core.persistence.util.BundleBinding;
import org.apache.jackrabbit.core.persistence.util.ErrorHandling;
import org.apache.jackrabbit.core.persistence.util.FileSystemBLOBStore;
import org.apache.jackrabbit.core.persistence.util.NodePropBundle;
import org.apache.jackrabbit.core.persistence.util.Serializer;
import org.apache.jackrabbit.core.state.ItemStateException;
import org.apache.jackrabbit.core.state.NoSuchItemStateException;
import org.apache.jackrabbit.core.state.NodeReferences;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * <code>BundleInMemPersistenceManager</code> is a <code>HashMap</code>-based
 * <code>PersistenceManager</code> for Jackrabbit that keeps all data in memory
 * and that is capable of storing and loading its contents using a simple custom
 * binary serialization format (see {@link Serializer}).
 * <p/>
 * It is configured through the following properties:
 * <ul>
 * <li><code>initialCapacity</code>: initial capacity of the hash map used to store the data</li>
 * <li><code>loadFactor</code>: load factor of the hash map used to store the data</li>
 * <li><code>persistent</code>: if <code>true</code> the contents of the hash map
 * is loaded on startup and stored on shutdown;
 * if <code>false</code> nothing is persisted</li>
 * <li><code>useFileBlobStore</code>: if <code>true</code> the contents of the blobs
 * will be directly stored on the file system instead of in memory.</li>
 * <li><code>minBlobSize</code> use blob store for binaries properties larger 
 * than minBlobSite (bytes). Default is 4096.</li>
 * </ul>
 * <p/>
 * <b>Please note that this class should only be used for testing purposes.</b>
 *
 */
public class InMemBundlePersistenceManager extends AbstractBundlePersistenceManager {

    /** the default logger */
    private static Logger log = LoggerFactory.getLogger(InMemBundlePersistenceManager.class);

    /** flag indicating if this manager was initialized */
    protected boolean initialized;

    /**
     * Path where bundles are stored on shutdown.
     * (if <code>persistent==true</code>)
     */
    protected static final String BUNDLE_FILE_PATH = "/data/.bundle.bin";

    /**
     * Path where blobs are stored on shutdown.
     * (if <code>persistent==true</code> and <code>useFileBlobStore==false</code>)
     */
    protected static final String BLOBS_FILE_PATH = "/data/.blobs.bin";

    /**
     * Path where references are stored on shutdown.
     * (if <code>persistent==true</code>)
     */
    protected static final String REFS_FILE_PATH = "/data/.refs.bin";

    /**
     * File system where the content of the hash maps are read from/written to
     * (if <code>persistent==true</code>)
     */
    protected FileSystem wspFS;

    /**
     * File system where BLOB data is stored.
     * (if <code>useFileBlobStore==true</code>)
     */
    protected FileSystem blobFS;

    /** 
     * Initial size of buffer used to serialize objects. 
     */
    protected static final int INITIAL_BUFFER_SIZE = 1024;

    /**
     * the minimum size of a property until it gets written to the blob store
     * @see #setMinBlobSize(String)
     */
    private int minBlobSize = 0x1000;

    /**
     * Flag for error handling.
     */
    protected ErrorHandling errorHandling = new ErrorHandling();

    /**
     * the bundle binding
     */
    protected BundleBinding binding;

    /**
     * Bundle memory store.
     */
    protected Map<NodeId, byte[]> bundleStore;

    /**
     * References memory store.
     */
    protected Map<NodeId, byte[]> refsStore;

    /**
     * Blob store.
     */
    protected BLOBStore blobStore;

    /**
     * Blobs memory store used by the InMemBlobStore.
     * (if <code>useFileBlobStore==false</code>)
     */
    private Map<String, byte[]> blobs;

    /**
     *  initial capacity
     */
    protected int initialCapacity = 32768;

    /**
     *  load factor for the hash map
     */
    protected float loadFactor = 0.75f;

    /**
     *  Should hash map be persisted?
     */
    protected boolean persistent = true;

    /**
     * Store blobs on file system instead of memory.
     */
    protected boolean useFileBlobStore = false;

    /**
     * Creates a new <code>InMemBundlePersistenceManager</code> instance.
     */
    public InMemBundlePersistenceManager() {
        initialized = false;
    }

    public void setInitialCapacity(int initialCapacity) {
        this.initialCapacity = initialCapacity;
    }

    public void setInitialCapacity(String initialCapacity) {
        this.initialCapacity = Integer.parseInt(initialCapacity);
    }

    public String getInitialCapacity() {
        return Integer.toString(initialCapacity);
    }

    public void setLoadFactor(float loadFactor) {
        this.loadFactor = loadFactor;
    }

    public void setLoadFactor(String loadFactor) {
        this.loadFactor = Float.parseFloat(loadFactor);
    }

    public String getLoadFactor() {
        return Float.toString(loadFactor);
    }

    public boolean isPersistent() {
        return persistent;
    }

    public void setPersistent(boolean persistent) {
        this.persistent = persistent;
    }

    public void setPersistent(String persistent) {
        this.persistent = Boolean.valueOf(persistent).booleanValue();
    }

    public void setUseFileBlobStore(boolean useFileBlobStore) {
        this.useFileBlobStore = useFileBlobStore;
    }

    public void setUseFileBlobStore(String useFileBlobStore) {
        this.useFileBlobStore = Boolean.valueOf(useFileBlobStore).booleanValue();
    }

    public String getMinBlobSize() {
        return String.valueOf(minBlobSize);
    }

    public void setMinBlobSize(String minBlobSize) {
        this.minBlobSize = Integer.decode(minBlobSize).intValue();
    }

    /**
     * Reads the content of the hash maps from the file system
     *
     * @throws Exception if an error occurs
     */
    public synchronized void loadContents() throws Exception {
        // read item states
        FileSystemResource fsRes = new FileSystemResource(wspFS, BUNDLE_FILE_PATH);
        if (!fsRes.exists()) {
            return;
        }
        BufferedInputStream bis = new BufferedInputStream(fsRes.getInputStream());
        DataInputStream in = new DataInputStream(bis);

        try {
            int n = in.readInt(); // number of entries
            while (n-- > 0) {
                String s = in.readUTF(); // id
                NodeId id = NodeId.valueOf(s);
                int length = in.readInt(); // data length
                byte[] data = new byte[length];
                in.readFully(data); // data
                // store in map
                bundleStore.put(id, data);
            }
        } finally {
            in.close();
        }

        // read references
        fsRes = new FileSystemResource(wspFS, REFS_FILE_PATH);
        bis = new BufferedInputStream(fsRes.getInputStream());
        in = new DataInputStream(bis);

        try {
            int n = in.readInt(); // number of entries
            while (n-- > 0) {
                String s = in.readUTF(); // target id
                NodeId id = NodeId.valueOf(s);
                int length = in.readInt(); // data length
                byte[] data = new byte[length];
                in.readFully(data); // data
                // store in map
                refsStore.put(id, data);
            }
        } finally {
            in.close();
        }

        if (!useFileBlobStore) {
            // read blobs
            fsRes = new FileSystemResource(wspFS, BLOBS_FILE_PATH);
            bis = new BufferedInputStream(fsRes.getInputStream());
            in = new DataInputStream(bis);

            try {
                int n = in.readInt(); // number of entries
                while (n-- > 0) {
                    String id = in.readUTF(); // id
                    int length = in.readInt(); // data length
                    byte[] data = new byte[length];
                    in.readFully(data); // data
                    // store in map
                    blobs.put(id, data);
                }
            } finally {
                in.close();
            }
        }
    }

    /**
     * Writes the content of the hash maps stores to the file system.
     *
     * @throws Exception if an error occurs
     */
    public synchronized void storeContents() throws Exception {
        // write bundles
        FileSystemResource fsRes = new FileSystemResource(wspFS, BUNDLE_FILE_PATH);
        fsRes.makeParentDirs();
        BufferedOutputStream bos = new BufferedOutputStream(fsRes.getOutputStream());
        DataOutputStream out = new DataOutputStream(bos);

        try {
            out.writeInt(bundleStore.size()); // number of entries
            // entries
            for (NodeId id : bundleStore.keySet()) {
                out.writeUTF(id.toString()); // id

                byte[] data = bundleStore.get(id);
                out.writeInt(data.length); // data length
                out.write(data); // data
            }
        } finally {
            out.close();
        }

        // write references
        fsRes = new FileSystemResource(wspFS, REFS_FILE_PATH);
        fsRes.makeParentDirs();
        bos = new BufferedOutputStream(fsRes.getOutputStream());
        out = new DataOutputStream(bos);

        try {
            out.writeInt(refsStore.size()); // number of entries
            // entries
            for (NodeId id : refsStore.keySet()) {
                out.writeUTF(id.toString()); // target id

                byte[] data = refsStore.get(id);
                out.writeInt(data.length); // data length
                out.write(data); // data
            }
        } finally {
            out.close();
        }

        if (!useFileBlobStore) {
            // write blobs
            fsRes = new FileSystemResource(wspFS, BLOBS_FILE_PATH);
            fsRes.makeParentDirs();
            bos = new BufferedOutputStream(fsRes.getOutputStream());
            out = new DataOutputStream(bos);

            try {
                out.writeInt(blobs.size()); // number of entries
                // entries
                for (String id : blobs.keySet()) {
                    out.writeUTF(id); // id
                    byte[] data = blobs.get(id);
                    out.writeInt(data.length); // data length
                    out.write(data); // data
                }
            } finally {
                out.close();
            }
        }
    }

    //---------------------------------------------------< PersistenceManager >
    /**
     * {@inheritDoc}
     */
    public void init(PMContext context) throws Exception {
        if (initialized) {
            throw new IllegalStateException("already initialized");
        }
        super.init(context);
        // initialize mem stores
        bundleStore = new HashMap<NodeId, byte[]>(initialCapacity, loadFactor);
        refsStore = new HashMap<NodeId, byte[]>(initialCapacity, loadFactor);

        // Choose a FileSystem for the BlobStore based on whether data is persistent or not 
        if (useFileBlobStore) {
            blobFS = new LocalFileSystem();
            ((LocalFileSystem) blobFS).setRoot(new File(context.getHomeDir(), "blobs"));
            blobFS.init();
            blobStore = new FileSystemBLOBStore(blobFS);
        } else {
            blobStore = new InMemBLOBStore();
        }

        wspFS = context.getFileSystem();

        // load namespaces
        binding = new BundleBinding(errorHandling, blobStore, getNsIndex(), getNameIndex(), context.getDataStore());
        binding.setMinBlobSize(minBlobSize);

        if (persistent) {
            // deserialize contents of the stores
            loadContents();
        }
        initialized = true;
    }

    /**
     * {@inheritDoc}
     */
    public synchronized void close() throws Exception {
        if (!initialized) {
            throw new IllegalStateException("not initialized");
        }

        try {
            if (persistent) {
                // serialize contents of state and refs stores
                storeContents();
            } else if (useFileBlobStore) {
                blobFS.close();
                // not persistent, clear out blobs
                wspFS.deleteFolder("blobs");
            }
        } finally {
            initialized = false;
        }
    }

    /**
     * {@inheritDoc}
     */
    public NodeReferences loadReferencesTo(NodeId id) throws NoSuchItemStateException, ItemStateException {
        if (!initialized) {
            throw new IllegalStateException("not initialized");
        }
        if (!refsStore.containsKey(id)) {
            throw new NoSuchItemStateException(id.toString());
        }

        try {
            NodeReferences refs = new NodeReferences(id);
            Serializer.deserialize(refs, new ByteArrayInputStream(refsStore.get(id)));
            return refs;
        } catch (Exception e) {
            String msg = "failed to read references: " + id;
            log.error(msg, e);
            throw new ItemStateException(msg, e);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected void store(NodeReferences refs) throws ItemStateException {
        try {
            ByteArrayOutputStream out = new ByteArrayOutputStream(INITIAL_BUFFER_SIZE);
            // serialize references
            Serializer.serialize(refs, out);
            // store in serialized format in map for better memory efficiency
            refsStore.put(refs.getTargetId(), out.toByteArray());
        } catch (Exception e) {
            String msg = "failed to store " + refs;
            log.debug(msg);
            throw new ItemStateException(msg, e);
        }
    }

    /**
     * {@inheritDoc}
     */
    public boolean existsReferencesTo(NodeId targetId) throws ItemStateException {
        if (!initialized) {
            throw new IllegalStateException("not initialized");
        }
        return refsStore.containsKey(targetId);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected void destroy(NodeReferences refs) throws ItemStateException {
        refsStore.remove(refs.getTargetId());
    }

    /**
     * {@inheritDoc}
     */
    public Iterable<NodeId> getAllNodeIds(NodeId after, int maxCount) throws ItemStateException, RepositoryException {
        // ignore after and count parameters.
        return bundleStore.keySet();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected NodePropBundle loadBundle(NodeId id) throws ItemStateException {
        if (!bundleStore.containsKey(id)) {
            return null;
        }
        try {
            return binding.readBundle(new ByteArrayInputStream(bundleStore.get(id)), id);
        } catch (Exception e) {
            String msg = "failed to read bundle: " + id + ": " + e;
            log.error(msg);
            throw new ItemStateException(msg, e);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected void storeBundle(NodePropBundle bundle) throws ItemStateException {
        ByteArrayOutputStream out = new ByteArrayOutputStream(INITIAL_BUFFER_SIZE);
        try {
            binding.writeBundle(out, bundle);
            bundleStore.put(bundle.getId(), out.toByteArray());
        } catch (IOException e) {
            String msg = "failed to write bundle: " + bundle.getId();
            log.error(msg, e);
            throw new ItemStateException(msg, e);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected void destroyBundle(NodePropBundle bundle) throws ItemStateException {
        bundleStore.remove(bundle.getId());
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected BLOBStore getBlobStore() {
        return blobStore;
    }

    /**
     * Helper interface for closeable stores
     */
    protected static interface CloseableBLOBStore extends BLOBStore {
        void close();
    }

    /**
     * Trivial {@link BLOBStore} implementation that is backed by a {@link HashMap}.
     */
    protected class InMemBLOBStore implements CloseableBLOBStore {

        public InMemBLOBStore() {
            blobs = new HashMap<String, byte[]>();
        }

        /**
         * {@inheritDoc}
         */
        public String createId(PropertyId id, int index) {
            StringBuilder buf = new StringBuilder();
            buf.append(id.getParentId().toString());
            buf.append('.');
            buf.append(getNsIndex().stringToIndex(id.getName().getNamespaceURI()));
            buf.append('.');
            buf.append(getNameIndex().stringToIndex(id.getName().getLocalName()));
            buf.append('.');
            buf.append(index);
            return buf.toString();
        }

        /**
         * {@inheritDoc}
         */
        public void put(String blobId, InputStream in, long size) throws Exception {
            blobs.put(blobId, IOUtils.toByteArray(in));
        }

        /**
         * {@inheritDoc}
         */
        public InputStream get(String blobId) throws Exception {
            if (blobs.containsKey(blobId)) {
                return new ByteArrayInputStream(blobs.get(blobId));
            } else {
                return null;
            }
        }

        /**
         * {@inheritDoc}
         */
        public boolean remove(String blobId) throws Exception {
            return blobs.remove(blobId) != null;
        }

        /**
         * {@inheritDoc}
         */
        public void close() {
            blobs.clear();
        }
    }
}
