/**
 * 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 Lesser 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 Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */


package com.mucommander.commons.file.archive;

import com.mucommander.commons.file.AbstractFile;
import com.mucommander.commons.file.FileOperation;
import com.mucommander.commons.file.FilePermissions;
import com.mucommander.commons.file.FileURL;
import com.mucommander.commons.file.PermissionAccess;
import com.mucommander.commons.file.PermissionType;
import com.mucommander.commons.file.UnsupportedFileOperation;
import com.mucommander.commons.file.UnsupportedFileOperationException;
import com.mucommander.commons.file.filter.FileFilter;
import com.mucommander.commons.file.filter.FilenameFilter;
import com.mucommander.commons.io.ByteUtils;
import com.mucommander.commons.io.RandomAccessInputStream;
import com.mucommander.commons.io.RandomAccessOutputStream;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;


/**
 * <code>AbstractArchiveEntryFile</code> represents a file entry inside an archive.
 * An <code>AbstractArchiveEntryFile</code> is always associated with an {@link ArchiveEntry} object which contains
 * information about the entry (name, size, date, ...) and with an {@link AbstractArchiveFile} which acts as an entry
 * repository and provides operations such as listing a directory entry's files, adding or removing entries
 * (if the archive is writable), etc...
 *
 * <p>
 * <code>AbstractArchiveEntryFile</code> implements {@link com.mucommander.commons.file.AbstractFile} by delegating methods to
 * the <code>ArchiveEntry</code> and <code>AbstractArchiveFile</code> instances.
 * <code>AbstractArchiveEntryFile</code> is agnostic to the actual archive format. In other words, there is no need to
 * extend this class for a particular archive format, <code>ArchiveEntry</code> and <code>AbstractArchiveFile</code>
 * provide a generic framework that isolates from the archive format's specifics.
 * </p>
 * <p>
 * This class is abstract (as the name implies) and implemented by two subclasses:
 * <ul>
 *   <li>{@link ROArchiveEntryFile}: represents an entry inside a read-only archive</li>
 *   <li>{@link RWArchiveEntryFile}: represents an entry inside a {@link AbstractArchiveFile#isWritable() read-write} archive</li>
 * </ul>
 * </p>
 *
 * @see AbstractArchiveFile
 * @see ArchiveEntry
 * @author Maxence Bernard
 */
public abstract class AbstractArchiveEntryFile extends AbstractFile {

    /** The archive file that contains this entry */
    protected AbstractArchiveFile archiveFile;

    /** This entry file's parent, can be the archive file itself if this entry is located at the top level */
    protected AbstractFile parent;

    /** The ArchiveEntry object that contains information about this entry */
    protected ArchiveEntry entry;


    /**
     * Creates a new AbstractArchiveEntryFile.
     *
     * @param url the FileURL instance that represents this file's location
     * @param archiveFile the AbstractArchiveFile instance that contains this entry
     * @param entry the ArchiveEntry object that contains information about this entry
     */
    protected AbstractArchiveEntryFile(FileURL url, AbstractArchiveFile archiveFile, ArchiveEntry entry) {
        super(url);
        this.archiveFile = archiveFile;
        this.entry = entry;
    }


    /**
     * Returns the ArchiveEntry instance that contains information about the archive entry (path, size, date, ...).
     *
     * @return the ArchiveEntry instance that contains information about the archive entry (path, size, date, ...)
     */
    public ArchiveEntry getEntry() {
        return entry;
    }

    /**
     * Returns the {@link AbstractArchiveFile} that contains the entry represented by this file.
     *
     * @return the AbstractArchiveFile that contains the entry represented by this file
     */
    public AbstractArchiveFile getArchiveFile() {
        return archiveFile;
    }


    /**
     * Returns the relative path of this entry, with respect to the archive file. The path separator of the returned
     * path is the one returned by {@link #getSeparator()}. As a relative path, the returned path does not start
     * with a separator character.
     *
     * @return the relative path of this entry, with respect to the archive file.
     */
    public String getRelativeEntryPath() {
        String path = entry.getPath();

        // Replace all occurrences of the entry's separator by the archive file's separator, only if the separator is
        // not "/" (i.e. the entry path separator).
        String separator = getSeparator();
        if(!separator.equals("/"))
            path = path.replace("/", separator);

        return path;
    }


    /////////////////////////////////
    // AbstractFile implementation //
    /////////////////////////////////

    @Override
    public long getDate() {
        return entry.getDate();
    }

    @Override
    public long getSize() {
        return entry.getSize();
    }
	
    @Override
    public boolean isDirectory() {
        return entry.isDirectory();
    }

    @Override
    public boolean isArchive() {
        // Archive entries files may be wrapped by archive files but they are not archive files per se
        return false;
    }

    @Override
    public AbstractFile[] ls() throws IOException, UnsupportedFileOperationException {
        return archiveFile.ls(this, null, null);
    }

    @Override
    public AbstractFile[] ls(FilenameFilter filter) throws IOException, UnsupportedFileOperationException {
        return archiveFile.ls(this, filter, null);
    }
	
    @Override
    public AbstractFile[] ls(FileFilter filter) throws IOException, UnsupportedFileOperationException {
        return archiveFile.ls(this, null, filter);
    }

    @Override
    public AbstractFile getParent() {
        return parent;
    }
	
    @Override
    public void setParent(AbstractFile parent) {
        this.parent = parent;	
    }

    /**
     * Returns <code>true</code> if this entry exists within the archive file.
     *
     * @return true if this entry exists within the archive file
     */
    @Override
    public boolean exists() {
        return entry.exists();
    }
	
    @Override
    public FilePermissions getPermissions() {
        // Return the entry's permissions
        return entry.getPermissions();
    }

    @Override
    public void changePermission(PermissionAccess access, PermissionType permission, boolean enabled) throws IOException, UnsupportedFileOperationException {
        changePermissions(ByteUtils.setBit(getPermissions().getIntValue(), (permission.toInt() << (access.toInt()*3)), enabled));
    }

    @Override
    public String getOwner() {
        return entry.getOwner();
    }

    @Override
    public boolean canGetOwner() {
        return entry.getOwner()!=null;
    }

    @Override
    public String getGroup() {
        return entry.getGroup();
    }

    @Override
    public boolean canGetGroup() {
        return entry.getGroup()!=null;
    }

    /**
     * Always returns <code>false</code>.
     */
    @Override
    public boolean isSymlink() {
        return false;
    }

    /**
     * Always returns <code>false</code>.
     */
    @Override
    public boolean isSystem() {
        return false;
    }

    /**
     * Delegates to the archive file's {@link AbstractArchiveFile#getFreeSpace()} method.
     *
     * @throws IOException if an I/O error occurred
     * @throws UnsupportedFileOperationException if the underlying archive file does not support
     * {@link FileOperation#GET_FREE_SPACE} operations.
     */
    @Override
    public long getFreeSpace() throws IOException, UnsupportedFileOperationException {
        return archiveFile.getFreeSpace();
    }

    /**
     * Delegates to the archive file's {@link AbstractArchiveFile#getTotalSpace()} method.
     *
     * @throws IOException if an I/O error occurred
     * @throws UnsupportedFileOperationException if the underlying archive file does not support
     * {@link FileOperation#GET_TOTAL_SPACE} operations.
     */
    @Override
    public long getTotalSpace() throws IOException, UnsupportedFileOperationException {
        return archiveFile.getTotalSpace();
    }

    /**
     * Delegates to the archive file's {@link AbstractArchiveFile#getEntryInputStream(ArchiveEntry,ArchiveEntryIterator)}}
     * method.
     *
     * @throws UnsupportedFileOperationException if the underlying archive file does not support
     * {@link FileOperation#READ_FILE} operations.
     */
    @Override
    public InputStream getInputStream() throws IOException, UnsupportedFileOperationException {
        return archiveFile.getEntryInputStream(entry, null);
    }

    /**
     * Always throws an {@link UnsupportedFileOperationException}: append is not available for archive entries.
     *
     * @throws UnsupportedFileOperationException always
     */
    @Override
    @UnsupportedFileOperation
    public OutputStream getAppendOutputStream() throws UnsupportedFileOperationException {
        throw new UnsupportedFileOperationException(FileOperation.APPEND_FILE);
    }

    /**
     * Always throws an {@link UnsupportedFileOperationException}: random read access is not available for archive
     * entries.
     *
     * @throws UnsupportedFileOperationException always
     */
    @Override
    @UnsupportedFileOperation
    public RandomAccessInputStream getRandomAccessInputStream() throws UnsupportedFileOperationException {
        throw new UnsupportedFileOperationException(FileOperation.RANDOM_READ_FILE);
    }

    /**
     * Always throws an {@link UnsupportedFileOperationException}: random write access is not available for archive
     * entries.
     *
     * @throws UnsupportedFileOperationException always
     */
    @Override
    @UnsupportedFileOperation
    public RandomAccessOutputStream getRandomAccessOutputStream() throws UnsupportedFileOperationException {
        throw new UnsupportedFileOperationException(FileOperation.RANDOM_WRITE_FILE);
    }

    /**
     * Always throws an {@link UnsupportedFileOperationException} when called.
     *
     * @throws UnsupportedFileOperationException always
     */
    @Override
    @UnsupportedFileOperation
    public void copyRemotelyTo(AbstractFile destFile) throws UnsupportedFileOperationException {
        // TODO: we could consider adding remote copy support to RWArchiveEntryFile
        throw new UnsupportedFileOperationException(FileOperation.COPY_REMOTELY);
    }

    /**
     * Always throws an {@link UnsupportedFileOperationException} when called.
     *
     * @throws UnsupportedFileOperationException always
     */
    @Override
    @UnsupportedFileOperation
    public void renameTo(AbstractFile destFile) throws UnsupportedFileOperationException {
        // TODO: we could consider adding renaming support to RWArchiveEntryFile
        throw new UnsupportedFileOperationException(FileOperation.RENAME);
    }

    /**
     * Returns the same ArchiveEntry instance as {@link #getEntry()}.
     */
    @Override
    public Object getUnderlyingFileObject() {
        return entry;
    }

    
    ////////////////////////
    // Overridden methods //
    ////////////////////////

    /**
     * This method is overridden to return the separator of the {@link #getArchiveFile() archive file} that contains
     * this entry.
     *
     * @return the separator of the archive file that contains this entry
     */
    @Override
    public String getSeparator() {
        return archiveFile.getSeparator();
    }

    /**
     * This method is overridden to use the archive file's absolute path as the base path of this entry file.
     */
    @Override
    public String getAbsolutePath() {
        // Use the archive file's absolute path and append the entry's relative path to it
        return archiveFile.getAbsolutePath(true)+getRelativeEntryPath();
    }

    /**
     * This method is overridden to use the archive file's canonical path as the base path of this entry file.
     */
    @Override
    public String getCanonicalPath() {
        // Use the archive file's canonical path and append the entry's relative path to it
        return archiveFile.getCanonicalPath(true)+getRelativeEntryPath();
    }

    /**
     * This method is overridden to return the archive's root folder.
     */
    @Override
    public AbstractFile getRoot() {
        return archiveFile.getRoot();
    }

    /**
     * This method is overridden to blindly return <code>false</code>, an archive entry cannot be a root folder.
     *
     * @return <code>false</code>, always
     */
    @Override
    public boolean isRoot() {
        return false;
    }

    /**
     * This method is overridden to return the archive's volume folder.
     */
    @Override
    public AbstractFile getVolume() {
        return archiveFile.getVolume();
    }
}
