/*
 * 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.vfs.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.vfs.FileName;
import org.apache.commons.vfs.FileObject;
import org.apache.commons.vfs.FileSystemException;
import org.apache.commons.vfs.FileSystemOptions;
import org.apache.commons.vfs.FileType;
import org.apache.commons.vfs.provider.AbstractFileSystem;

/**
 * A RAM File System
 */
public class RamFileSystem extends AbstractFileSystem implements Serializable
{
    /**
     * Cache of RAM File Data
     */
    private Map cache;

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

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

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

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

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

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

        return names;
    }

    /**
     * Delete a file
     *
     * @param file
     * @throws FileSystemException
     */
    void delete(RamFileObject file) throws FileSystemException
    {
        // root is read only check
        if (file.getParent()==null) {
            throw new FileSystemException("unable to delete root");
        }

        // Remove reference from cache
        this.cache.remove(file.getName());
        // Notify the parent
        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)
        {
            RamFileData parentData = (RamFileData) this.cache.get(file
                    .getParent().getName());
            // Only if not already added
            if (!parentData.hasChildren(file.getData()))
            {
                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
     * @param to
     * @throws FileSystemException
     */
    void rename(RamFileObject from, RamFileObject to)
            throws FileSystemException
    {
        if (!this.cache.containsKey(from.getName()))
        {
            throw new FileSystemException("File does not exist: "
                    + from.getName());
        }
        // Copy data

        to.getData().setBuffer(from.getData().getBuffer());
        to.getData().setLastModified(from.getData().getLastModified());
        to.getData().setType(from.getData().getType());

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

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

    /**
     * Import a Tree
     *
     * @param file
     * @throws FileSystemException
     */
    public void importTree(File file) throws FileSystemException
    {
        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(FileObject fo, FileObject root)
            throws FileSystemException
    {
        RamFileObject memFo = (RamFileObject) this.resolveFile(fo.getName()
                .getPath().substring(root.getName().getPath().length()));
        if (fo.getType().hasChildren())
        {
            // Create Folder
            memFo.createFolder();
            // Import recursively
            FileObject[] fos = fo.getChildren();
            for (int i = 0; i < fos.length; i++)
            {
                FileObject child = fos[i];
                this.toRamFileObject(child, root);
            }
        }
        else if (fo.getType().equals(FileType.FILE))
        {
            // Read bytes
            try
            {
                InputStream is = fo.getContent().getInputStream();
                try
                {
                    OutputStream os = new BufferedOutputStream(memFo
                            .getOutputStream(), 512);
                    int i;
                    while ((i = is.read()) != -1)
                    {
                        os.write(i);
                    }
                    os.flush();
                    os.close();
                }
                finally
                {
                    try
                    {
                        is.close();
                    }
                    catch (IOException e)
                    {
                        // ignore on close exception
                    }
                }
            }
            catch (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
     */
    int size()
    {
        int size = 0;
        Iterator iter = cache.values().iterator();
        while (iter.hasNext())
        {
            RamFileData data = (RamFileData) iter.next();
            size += data.size();
        }
        return size;
    }

    /**
     * Close the RAMFileSystem
     */
    public void close()
    {
        this.cache = null;
        super.close();
    }
}
