/*
 * 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.commons.vfs2.provider.ram;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Serializable;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import org.apache.commons.vfs2.Capability;
import org.apache.commons.vfs2.FileName;
import org.apache.commons.vfs2.FileObject;
import org.apache.commons.vfs2.FileSystemException;
import org.apache.commons.vfs2.FileSystemOptions;
import org.apache.commons.vfs2.FileType;
import org.apache.commons.vfs2.provider.AbstractFileName;
import org.apache.commons.vfs2.provider.AbstractFileSystem;

/**
 * A RAM File System.
 */
public class RamFileSystem extends AbstractFileSystem implements Serializable {

    private static final int BUFFER_SIZE = 512;

    /**
     * serialVersionUID format is YYYYMMDD for the date of the last binary change.
     */
    private static final long serialVersionUID = 20101208L;

    /**
     * Cache of RAM File Data
     */
    private final Map<FileName, RamFileData> cache;

    /**
     * @param rootName The root file name.
     * @param fileSystemOptions The FileSystem options.
     */
    protected RamFileSystem(final FileName rootName, final FileSystemOptions fileSystemOptions) {
        super(rootName, null, fileSystemOptions);
        this.cache = Collections.synchronizedMap(new HashMap<FileName, RamFileData>());
        // create root
        final RamFileData rootData = new RamFileData(rootName);
        rootData.setType(FileType.FOLDER);
        rootData.setLastModified(System.currentTimeMillis());
        this.cache.put(rootName, rootData);
    }

    /*
     * (non-Javadoc)
     *
     * @see org.apache.commons.vfs2.provider.AbstractFileSystem#createFile(org.apache.commons.vfs2.FileName)
     */
    @Override
    protected FileObject createFile(final AbstractFileName name) throws Exception {
        return new RamFileObject(name, this);
    }

    /*
     * (non-Javadoc)
     *
     * @see org.apache.commons.vfs2.provider.AbstractFileSystem#addCapabilities(java.util.Collection)
     */
    @Override
    protected void addCapabilities(final Collection<Capability> caps) {
        caps.addAll(RamFileProvider.capabilities);
    }

    /**
     * @param name The name of the file.
     * @return children The names of the children.
     */
    String[] listChildren(final FileName name) {
        final RamFileData data = this.cache.get(name);
        if (data == null || !data.getType().hasChildren()) {
            return null;
        }
        final Collection<RamFileData> children = data.getChildren();
        String[] names;

        synchronized (children) {
            names = new String[children.size()];

            int pos = 0;
            final Iterator<RamFileData> iter = children.iterator();
            while (iter.hasNext()) {
                final RamFileData childData = iter.next();
                names[pos] = childData.getName().getBaseName();
                pos++;
            }
        }

        return names;
    }

    /**
     * Delete a file
     *
     * @param file
     * @throws FileSystemException
     */
    void delete(final RamFileObject file) throws FileSystemException {
        // root is read only check
        FileSystemException.requireNonNull(file.getParent(), "unable to delete root");

        // Remove reference from cache
        this.cache.remove(file.getName());
        // Notify the parent
        final RamFileObject parent = (RamFileObject) this.resolveFile(file.getParent().getName());
        parent.getData().removeChild(file.getData());
        parent.close();
        // Close the file
        file.getData().clear();
        file.close();
    }

    /**
     * Saves a file
     *
     * @param file
     * @throws FileSystemException
     */
    void save(final RamFileObject file) throws FileSystemException {

        // Validate name
        if (file.getData().getName() == null) {
            throw new FileSystemException(new IllegalStateException("The data has no name. " + file));
        }

        // Add to the parent
        if (file.getName().getDepth() > 0) {
            final RamFileData parentData = this.cache.get(file.getParent().getName());
            // Only if not already added
            if (!parentData.hasChildren(file.getData())) {
                final RamFileObject parent = (RamFileObject) file.getParent();
                parent.getData().addChild(file.getData());
                parent.close();
            }
        }
        // Store in cache
        cache.put(file.getName(), file.getData());
        file.getData().updateLastModified();
        file.close();
    }

    /**
     * @param from The original file.
     * @param to The new file.
     * @throws FileSystemException if an error occurs.
     */
    void rename(final RamFileObject from, final RamFileObject to) throws FileSystemException {
        if (!this.cache.containsKey(from.getName())) {
            throw new FileSystemException("File does not exist: " + from.getName());
        }
        // Copy data

        to.getData().setContent(from.getData().getContent());
        to.getData().setLastModified(from.getData().getLastModified());
        to.getData().setType(from.getData().getType());

        this.save(to);
        this.delete(from);
    }

    public void attach(final RamFileObject fo) {
        if (fo.getName() == null) {
            throw new IllegalArgumentException("Null argument");
        }
        RamFileData data = this.cache.get(fo.getName());
        if (data == null) {
            data = new RamFileData(fo.getName());
        }
        fo.setData(data);
    }

    /**
     * Import a Tree.
     *
     * @param file The File
     * @throws FileSystemException if an error occurs.
     */
    public void importTree(final File file) throws FileSystemException {
        final FileObject fileFo = getFileSystemManager().toFileObject(file);
        this.toRamFileObject(fileFo, fileFo);
    }

    /**
     * Import the given file with the name relative to the given root
     *
     * @param fo
     * @param root
     * @throws FileSystemException
     */
    void toRamFileObject(final FileObject fo, final FileObject root) throws FileSystemException {
        final RamFileObject memFo = (RamFileObject) this
                .resolveFile(fo.getName().getPath().substring(root.getName().getPath().length()));
        if (fo.getType().hasChildren()) {
            // Create Folder
            memFo.createFolder();
            // Import recursively
            final FileObject[] fos = fo.getChildren();
            for (final FileObject child : fos) {
                this.toRamFileObject(child, root);
            }
        } else if (fo.isFile()) {
            // Read bytes
            try {
                final InputStream is = fo.getContent().getInputStream();
                try {
                    final OutputStream os = new BufferedOutputStream(memFo.getOutputStream(), BUFFER_SIZE);
                    int i;
                    while ((i = is.read()) != -1) {
                        os.write(i);
                    }
                    os.close();
                } finally {
                    try {
                        is.close();
                    } catch (final IOException ignored) {
                        /* ignore on close exception. */
                    }
                    // TODO: close os
                }
            } catch (final IOException e) {
                throw new FileSystemException(e.getClass().getName() + " " + e.getMessage());
            }
        } else {
            throw new FileSystemException("File is not a folder nor a file " + memFo);
        }
    }

    /**
     * @return Returns the size of the FileSystem
     */
    long size() {
        long size = 0;
        synchronized (cache) {
            final Iterator<RamFileData> iter = cache.values().iterator();
            while (iter.hasNext()) {
                final RamFileData data = iter.next();
                size += data.size();
            }
        }
        return size;
    }

    /**
     * Close the RAMFileSystem.
     */
    @Override
    public void close() {
        this.cache.clear();
        super.close();
    }
}
