/*
 * This file is part of muCommander, http://www.mucommander.com
 *
 * muCommander is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * muCommander is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.mucommander.bookmark.file;

import com.mucommander.bookmark.Bookmark;
import com.mucommander.bookmark.BookmarkBuilder;
import com.mucommander.bookmark.BookmarkManager;
import com.mucommander.commons.file.*;
import com.mucommander.commons.file.protocol.ProtocolFile;
import com.mucommander.commons.io.FileTransferException;
import com.mucommander.commons.io.RandomAccessInputStream;
import com.mucommander.commons.io.RandomAccessOutputStream;

import java.io.*;

/**
 * Represents a file in the <code>bookmark://</code> file system.
 * @author Nicolas Rinaudo
 */
public class BookmarkFile extends ProtocolFile {
    // - Instance fields -------------------------------------------------------
    // -------------------------------------------------------------------------
    /** Bookmark wrapped by this abstract file. */
    private Bookmark     bookmark;
    /** Underlying abstract file. */
    private AbstractFile file;

    /** Permissions for all bookmark files: rw- (600 octal). Only the 'user' permissions bits are supported. */
    final static FilePermissions PERMISSIONS = new SimpleFilePermissions(384, 448);


    // - Initialisation --------------------------------------------------------
    // -------------------------------------------------------------------------
    /**
     * Creates a new bookmark file wrapping the specified bookmark.
     * @param  bookmark    bookmark to wrap.
     * @throws IOException if the specified bookmark's URL cannot be resolved.
     */
    protected BookmarkFile(Bookmark bookmark) throws IOException {
        super(FileURL.getFileURL(BookmarkProtocolProvider.BOOKMARK + "://" + java.net.URLEncoder.encode(bookmark.getName(), "UTF-8")));
        this.bookmark = bookmark;
    }



    // - Helper methods --------------------------------------------------------
    // -------------------------------------------------------------------------
    /**
     * Returns the <code>AbstractFile</code> this instance wraps.
     * <p>
     * Some methods need to have access to the underlying file. This, however, requires
     * resolving the path which can be time consuming. Using this method ensures that the
     * path is only resolved if necessary, and at most once.
     * </p>
     * @return the <code>AbstractFile</code> this instance wraps.
     */
    private synchronized AbstractFile getUnderlyingFile() {
        // Resolves the file if necessary.
        if(file == null)
            file = FileFactory.getFile(bookmark.getLocation());

        return file;
    }

    /**
     * Returns the underlying bookmark.
     * @return the underlying bookmark.
     */
    public Bookmark getBookmark() {return bookmark;}



    // - AbstractFile methods --------------------------------------------------
    // -------------------------------------------------------------------------
    /**
     * Returns the underlying bookmark's name.
     * @return the underlying bookmark's name.
     */
    @Override
    public String getName() {return bookmark.getName();}

    /**
     * Returns the wrapped file's descendants.
     * @return             the wrapped file's descendants.
     * @throws IOException                       if an I/O error occurs.
     * @throws UnsupportedFileOperationException if this operation is not supported by the underlying filesystem,
     * or is not implemented.
     */
    @Override
    public AbstractFile[] ls() throws IOException, UnsupportedFileOperationException {
        return getUnderlyingFile().ls();
    }

    /**
     * Returns the wrapped file's parent.
     * @return             the wrapped file's parent.
     * @see                #setParent(AbstractFile)
     */
    @Override
    public AbstractFile getParent() {
        try {
            return new BookmarkRoot();
        }
        catch(IOException e) {
            return null;
        }
    }

    /**
     * Returns the result of the wrapped file's <code>getFreeSpace()</code> methods.
     * @return the result of the wrapped file's <code>getFreeSpace()</code> methods.
     * @throws IOException if an I/O error occurred
     * @throws UnsupportedFileOperationException if this operation is not supported by the underlying filesystem,
     * or is not implemented.
     */
    @Override
    public long getFreeSpace() throws IOException, UnsupportedFileOperationException {return getUnderlyingFile().getFreeSpace();}

    /**
     * Returns the result of the wrapped file's <code>getTotalSpace()</code> methods.
     * @return the result of the wrapped file's <code>getTotalSpace()</code> methods.
     * @throws IOException if an I/O error occurred
     * @throws UnsupportedFileOperationException if this operation is not supported by the underlying filesystem,
     * or is not implemented.
     */
    @Override
    public long getTotalSpace() throws IOException, UnsupportedFileOperationException {return getUnderlyingFile().getTotalSpace();}

    /**
     * Returns <code>false</code>.
     * @return <code>false</code>.
     */
    @Override
    public boolean isDirectory() {return true;}

    /**
     * Sets the wrapped file's parent.
     * @param parent object to use as the wrapped file's parent.
     * @see          AbstractFile#getParent()
     */
    @Override
    public void setParent(AbstractFile parent) {
        getUnderlyingFile().setParent(parent);}

    /**
     * Returns <code>true</code> if the specified bookmark exists.
     * <p>
     * A bookmark is said to exist if and only if it is known to the {@link com.mucommander.bookmark.BookmarkManager}.
     * </p>
     * @return <code>true</code> if the specified bookmark exists, <code>false</code> otherwise.
     */
    @Override
    public boolean exists() {return BookmarkManager.getBookmark(bookmark.getName()) != null;}

    @Override
    public void mkfile() {BookmarkManager.addBookmark(bookmark);}

    public boolean equals(Object o) {
        // Makes sure we're working with an abstract file.
        if(!(o instanceof AbstractFile))
            return false;

        // Retrieves the actual file instance.
        // We might have received a Proxied or Cached file, so we need to make sure
        // we 'unwrap' that before comparing.
        AbstractFile file = ((AbstractFile)o).getAncestor();

        // We only know how to compare one bookmark file to the other.
        if(file instanceof BookmarkFile)
            return bookmark.equals(((BookmarkFile)file).getBookmark());
        return false;
    }

    @Override
    public String getCanonicalPath() {return bookmark.getLocation();}



    // - Bookmark renaming -----------------------------------------------------
    // -------------------------------------------------------------------------

    /**
     * Attempts to rename the bookmark to the specified destination.
     * The operation will only be carried out if the specified destination is a <code>BookmarkFile</code> or has an
     * ancestor that is.
     *
     * @param  destination where to move the bookmark to.
     * @throws IOException if the operation could not be carried out.
     */
    @Override
    public void renameTo(AbstractFile destination) throws IOException {
        checkRenamePrerequisites(destination, true, true);

        Bookmark oldBookmark;
        Bookmark newBookmark;

        destination = destination.getTopAncestor();

        // Makes sure we're working with a bookmark.
        if(!(destination instanceof BookmarkFile))
            throw new IOException();

        // Creates the new bookmark and checks for conflicts.
        newBookmark = new Bookmark(destination.getName(), bookmark.getLocation());
        if((oldBookmark = BookmarkManager.getBookmark(newBookmark.getName())) != null)
            BookmarkManager.removeBookmark(oldBookmark);

        // Adds the new bookmark and deletes its 'old' version.
        BookmarkManager.addBookmark(newBookmark);
        BookmarkManager.removeBookmark(bookmark);
    }

    // TODO: bookmark deleting is currently disabled as a quick fix for #329    
//    /**
//     * Deletes the bookmark.
//     * <p>
//     * Deleting a bookmark means unregistering it from the {@link com.mucommander.bookmark.BookmarkManager}.
//     * </p>
//     */
//    @Override
//    public void delete() {
//        BookmarkManager.removeBookmark(bookmark);
//    }

    @Override
    @UnsupportedFileOperation
    public void delete() throws UnsupportedFileOperationException {
        throw new UnsupportedFileOperationException(FileOperation.DELETE);
    }



    // - Bookmark duplication --------------------------------------------------
    // -------------------------------------------------------------------------

    /**
     * Tries to copy the bookmark to the specified destination.
     * <p>
     * If the specified destination is an instance of <code>BookmarkFile</code>,
     * this will duplicate the bookmark. Otherwise, this method will fail.
     * </p>
     * @param  destination           where to copy the bookmark to.
     * @throws FileTransferException if the specified destination is not an instance of <code>BookmarkFile</code>.
     */
    @Override
    public void copyRemotelyTo(AbstractFile destination) throws IOException {
        // Makes sure we're working with a bookmark.
        destination = destination.getTopAncestor();
        if(!(destination instanceof BookmarkFile))
            throw new IOException();

        // Copies this bookmark to the specified destination.
        BookmarkManager.addBookmark(new Bookmark(destination.getName(), bookmark.getLocation()));
    }



    // - Permissions -----------------------------------------------------------
    // -------------------------------------------------------------------------
    /**
     * Returns the same permissions for all boookmark files: rw- (600 octal).
     * Only the 'user' permissions bits are supported.

     * @return            this file's permissions.
     * @see               #changePermission(int,int,boolean)
     */
    @Override
    public FilePermissions getPermissions() {return PERMISSIONS;}

    /**
     * Always throws an {@link UnsupportedFileOperationException} when called: bookmarks always have all permissions,
     * this is not changeable.
     *
     * @param  access     ignored.
     * @param  permission ignored.
     * @param  enabled    ignored.
     * @see               #getPermissions()
     */
    @Override
    @UnsupportedFileOperation
    public void changePermission(PermissionAccess access, PermissionType permission, boolean enabled) throws UnsupportedFileOperationException {
        throw new UnsupportedFileOperationException(FileOperation.CHANGE_PERMISSION);
    }


    // - Import / export -------------------------------------------------------
    // -------------------------------------------------------------------------
    @Override
    public InputStream getInputStream() throws IOException {
        BookmarkBuilder       builder;
        ByteArrayOutputStream stream;

        builder = BookmarkManager.getBookmarkWriter(stream = new ByteArrayOutputStream());
        try {
            builder.startBookmarks();
            builder.addBookmark(bookmark.getName(), bookmark.getLocation());
            builder.endBookmarks();
        }
        // If an exception occured, we have to look for its root cause.
        catch(Throwable e) {
            Throwable e2;

            // Looks for the cause.
            while((e2 = e.getCause()) != null)
                e = e2;

            // If the cause is an IOException, thow it.
            if(e instanceof IOException)
                throw (IOException)e;

            // Otherwise, throw the exception as an IOException with a the underlying cause's message.
            throw new IOException(e.getMessage());
        }

        return new ByteArrayInputStream(stream.toByteArray());
    }

    @Override
    public OutputStream getOutputStream() throws IOException {return new BookmarkOutputStream();}


// - Unused methods --------------------------------------------------------
    // -------------------------------------------------------------------------
    // The following methods are not used by BookmarkFile. They will throw an exception or
    // return an 'operation non supported' / default value.

    @Override
    @UnsupportedFileOperation
    public void mkdir() throws UnsupportedFileOperationException {throw new UnsupportedFileOperationException(FileOperation.CREATE_DIRECTORY);}
    @Override
    public long getDate() {return 0;}
    @Override
    public PermissionBits getChangeablePermissions() {return PermissionBits.EMPTY_PERMISSION_BITS;}
    @Override
    @UnsupportedFileOperation
    public void changeDate(long lastModified) throws UnsupportedFileOperationException {throw new UnsupportedFileOperationException(FileOperation.CHANGE_DATE);}
    @Override
    public long getSize() {return -1;}
    @Override
    @UnsupportedFileOperation
    public RandomAccessInputStream getRandomAccessInputStream() throws UnsupportedFileOperationException {throw new UnsupportedFileOperationException(FileOperation.RANDOM_READ_FILE);}
    @Override
    @UnsupportedFileOperation
    public OutputStream getAppendOutputStream() throws UnsupportedFileOperationException {throw new UnsupportedFileOperationException(FileOperation.APPEND_FILE);}
    @Override
    @UnsupportedFileOperation
    public RandomAccessOutputStream getRandomAccessOutputStream() throws UnsupportedFileOperationException {throw new UnsupportedFileOperationException(FileOperation.RANDOM_WRITE_FILE);}
    @Override
    public Object getUnderlyingFileObject() {return null;}
    @Override
    public boolean isSymlink() {return false;}
    @Override
    public boolean isSystem() {return false;}
    @Override
    public String getOwner() {return null;}
    @Override
    public boolean canGetOwner() {return false;}
    @Override
    public String getGroup() {return null;}
    @Override
    public boolean canGetGroup() {return false;}
}
