/*
 * 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;

import org.apache.commons.vfs.FileContent;
import org.apache.commons.vfs.FileContentInfo;
import org.apache.commons.vfs.FileContentInfoFactory;
import org.apache.commons.vfs.FileObject;
import org.apache.commons.vfs.FileSystemException;
import org.apache.commons.vfs.RandomAccessContent;
import org.apache.commons.vfs.util.MonitorInputStream;
import org.apache.commons.vfs.util.MonitorOutputStream;
import org.apache.commons.vfs.util.MonitorRandomAccessContent;
import org.apache.commons.vfs.util.RandomAccessMode;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.cert.Certificate;
import java.util.Collections;
import java.util.Map;
import java.util.Set;

/**
 * The content of a file.
 *
 * @author <a href="mailto:adammurdoch@apache.org">Adam Murdoch</a>
 * @version $Revision: 764356 $ $Date: 2009-04-13 12:06:01 +0800 (一, 2009-04-13) $
 */
public final class DefaultFileContent implements FileContent
{
    /*
    static final int STATE_NONE = 0;
    static final int STATE_READING = 1;
    static final int STATE_WRITING = 2;
    static final int STATE_RANDOM_ACCESS = 3;
    */

    static final int STATE_CLOSED = 0;
    static final int STATE_OPENED = 1;

    private final AbstractFileObject file;
    private Map attrs;
    private Map roAttrs;
    private FileContentInfo fileContentInfo;
    private final FileContentInfoFactory fileContentInfoFactory;

    private final ThreadLocal threadData = new ThreadLocal();
    private boolean resetAttributes;

    /**
     * open streams counter for this file
     */
    private int openStreams = 0;

    public DefaultFileContent(final AbstractFileObject file, final FileContentInfoFactory fileContentInfoFactory)
    {
        this.file = file;
        this.fileContentInfoFactory = fileContentInfoFactory;
    }

    private FileContentThreadData getThreadData()
    {
        FileContentThreadData data = (FileContentThreadData) this.threadData.get();
        if (data == null)
        {
            data = new FileContentThreadData();
            this.threadData.set(data);
        }
        return data;
    }

    void streamOpened()
    {
        synchronized (this)
        {
            openStreams++;
        }
        ((AbstractFileSystem) file.getFileSystem()).streamOpened();
    }

    void streamClosed()
    {
        synchronized (this)
        {
            if (openStreams > 0)
            {
                openStreams--;
                if (openStreams < 1)
                {
                    file.notifyAllStreamsClosed();
                }
            }
        }
        ((AbstractFileSystem) file.getFileSystem()).streamClosed();
    }

    /**
     * Returns the file that this is the content of.
     */
    public FileObject getFile()
    {
        return file;
    }

    /**
     * Returns the size of the content (in bytes).
     */
    public long getSize() throws FileSystemException
    {
        // Do some checking
        if (!file.getType().hasContent())
        {
            throw new FileSystemException("vfs.provider/get-size-not-file.error", file);
        }
        /*
        if (getThreadData().getState() == STATE_WRITING || getThreadData().getState() == STATE_RANDOM_ACCESS)
        {
            throw new FileSystemException("vfs.provider/get-size-write.error", file);
        }
        */

        try
        {
            // Get the size
            return file.doGetContentSize();
        }
        catch (final Exception exc)
        {
            throw new FileSystemException("vfs.provider/get-size.error", new Object[]{file}, exc);
        }
    }

    /**
     * Returns the last-modified timestamp.
     */
    public long getLastModifiedTime() throws FileSystemException
    {
        /*
        if (getThreadData().getState() == STATE_WRITING || getThreadData().getState() == STATE_RANDOM_ACCESS)
        {
            throw new FileSystemException("vfs.provider/get-last-modified-writing.error", file);
        }
        */
        if (!file.getType().hasAttributes())
        {
            throw new FileSystemException("vfs.provider/get-last-modified-no-exist.error", file);
        }
        try
        {
            return file.doGetLastModifiedTime();
        }
        catch (final Exception e)
        {
            throw new FileSystemException("vfs.provider/get-last-modified.error", file, e);
        }
    }

    /**
     * Sets the last-modified timestamp.
     */
    public void setLastModifiedTime(final long modTime) throws FileSystemException
    {
        /*
        if (getThreadData().getState() == STATE_WRITING || getThreadData().getState() == STATE_RANDOM_ACCESS)
        {
            throw new FileSystemException("vfs.provider/set-last-modified-writing.error", file);
        }
        */
        if (!file.getType().hasAttributes())
        {
            throw new FileSystemException("vfs.provider/set-last-modified-no-exist.error", file);
        }
        try
        {
            if (!file.doSetLastModTime(modTime))
            {
                throw new FileSystemException("vfs.provider/set-last-modified.error", file);
            }
        }
        catch (final Exception e)
        {
            throw new FileSystemException("vfs.provider/set-last-modified.error", file, e);
        }
    }

   /**
     * Checks if an attribute exists.
     */
    public boolean hasAttribute(final String attrName) throws FileSystemException
    {
        if (!file.getType().hasAttributes())
        {
            throw new FileSystemException("vfs.provider/exists-attributes-no-exist.error", file);
        }
        getAttributes();
        return attrs.containsKey(attrName);
    }

    /**
     * Returns a read-only map of this file's attributes.
     */
    public Map getAttributes() throws FileSystemException
    {
        if (!file.getType().hasAttributes())
        {
            throw new FileSystemException("vfs.provider/get-attributes-no-exist.error", file);
        }
        if (resetAttributes || roAttrs == null)
        {
            try
            {
                synchronized(this)
                {
                    attrs = file.doGetAttributes();
                    roAttrs = Collections.unmodifiableMap(attrs);
                    resetAttributes = false;
                }
            }
            catch (final Exception e)
            {
                throw new FileSystemException("vfs.provider/get-attributes.error", file, e);
            }
        }
        return roAttrs;
    }

    /**
     * Used internally to flag situations where the file attributes should be
     * reretrieved.
     */
    public void resetAttributes()
    {
        resetAttributes = true;
    }

    /**
     * Lists the attributes of this file.
     */
    public String[] getAttributeNames() throws FileSystemException
    {
        getAttributes();
        final Set names = attrs.keySet();
        return (String[]) names.toArray(new String[names.size()]);
    }

    /**
     * Gets the value of an attribute.
     */
    public Object getAttribute(final String attrName)
        throws FileSystemException
    {
        getAttributes();
        return attrs.get(attrName);
    }

    /**
     * Sets the value of an attribute.
     */
    public void setAttribute(final String attrName, final Object value)
        throws FileSystemException
    {
        if (!file.getType().hasAttributes())
        {
            throw new FileSystemException("vfs.provider/set-attribute-no-exist.error", new Object[]{attrName, file});
        }
        try
        {
            file.doSetAttribute(attrName, value);
        }
        catch (final Exception e)
        {
            throw new FileSystemException("vfs.provider/set-attribute.error", new Object[]{attrName, file}, e);
        }

        if (attrs != null)
        {
            attrs.put(attrName, value);
        }
    }

    /**
     * Removes an attribute.
     */
    public void removeAttribute(final String attrName) throws FileSystemException
    {
        if (!file.getType().hasAttributes())
        {
            throw new FileSystemException("vfs.provider/remove-attribute-no-exist.error", file);
        }

        try
        {
            file.doRemoveAttribute(attrName);
        }
        catch (final Exception e)
        {
            throw new FileSystemException("vfs.provider/remove-attribute.error", new Object[]{attrName, file}, e);
        }

        if (attrs != null)
        {
            attrs.remove(attrName);
        }
    }

    /**
     * Returns the certificates used to sign this file.
     */
    public Certificate[] getCertificates() throws FileSystemException
    {
        if (!file.exists())
        {
            throw new FileSystemException("vfs.provider/get-certificates-no-exist.error", file);
        }
        /*
        if (getThreadData().getState() == STATE_WRITING || getThreadData().getState() == STATE_RANDOM_ACCESS)
        {
            throw new FileSystemException("vfs.provider/get-certificates-writing.error", file);
        }
        */

        try
        {
            final Certificate[] certs = file.doGetCertificates();
            if (certs != null)
            {
                return certs;
            }
            else
            {
                return new Certificate[0];
            }
        }
        catch (final Exception e)
        {
            throw new FileSystemException("vfs.provider/get-certificates.error", file, e);
        }
    }

    /**
     * Returns an input stream for reading the content.
     */
    public InputStream getInputStream() throws FileSystemException
    {
        /*
        if (getThreadData().getState() == STATE_WRITING || getThreadData().getState() == STATE_RANDOM_ACCESS)
        {
            throw new FileSystemException("vfs.provider/read-in-use.error", file);
        }
        */

        // Get the raw input stream
        final InputStream instr = file.getInputStream();

        final InputStream wrappedInstr = new FileContentInputStream(file, instr);

        this.getThreadData().addInstr(wrappedInstr);
        streamOpened();

        // setState(STATE_OPENED);
        return wrappedInstr;
    }

    /**
     * Returns an input/output stream to use to read and write the content of the file in an
     * random manner.
     */
    public RandomAccessContent getRandomAccessContent(final RandomAccessMode mode) throws FileSystemException
    {
        /*
        if (getThreadData().getState() != STATE_NONE)
        {
            throw new FileSystemException("vfs.provider/read-in-use.error", file);
        }
        */

        // Get the content
        final RandomAccessContent rastr = file.getRandomAccessContent(mode);

        FileRandomAccessContent rac = new FileRandomAccessContent(file, rastr);
        this.getThreadData().addRastr(rac);
        streamOpened();

        // setState(STATE_OPENED);
        return rac;
    }

    /**
     * Returns an output stream for writing the content.
     */
    public OutputStream getOutputStream() throws FileSystemException
    {
        return getOutputStream(false);
    }

    /**
     * Returns an output stream for writing the content in append mode.
     */
    public OutputStream getOutputStream(boolean bAppend) throws FileSystemException
    {
        /*
        if (getThreadData().getState() != STATE_NONE)
        */
        if (this.getThreadData().getOutstr() != null)
        {
            throw new FileSystemException("vfs.provider/write-in-use.error", file);
        }

        // Get the raw output stream
        final OutputStream outstr = file.getOutputStream(bAppend);

        // Create wrapper
        this.getThreadData().setOutstr(new FileContentOutputStream(file, outstr));
        streamOpened();

        // setState(STATE_OPENED);
        return this.getThreadData().getOutstr();
    }

    /**
     * Closes all resources used by the content, including all streams, readers
     * and writers.
     */
    public void close() throws FileSystemException
    {
        try
        {
            // Close the input stream
            while (getThreadData().getInstrsSize() > 0)
            {
                final FileContentInputStream instr = (FileContentInputStream) getThreadData().removeInstr(0);
                instr.close();
            }

            // Close the randomAccess stream
            while (getThreadData().getRastrsSize() > 0)
            {
                final RandomAccessContent ra = (RandomAccessContent) getThreadData().removeRastr(0);
                try
                {
                    ra.close();
                }
                catch (IOException e)
                {
                    throw new FileSystemException(e);
                }
            }

            // Close the output stream
            if (this.getThreadData().getOutstr() != null)
            {
                this.getThreadData().closeOutstr();
            }
        }
        finally
        {
            threadData.set(null);
        }
    }

    /**
     * Handles the end of input stream.
     */
    private void endInput(final FileContentInputStream instr)
    {
        getThreadData().removeInstr(instr);
        streamClosed();
        /*
        if (!getThreadData().hasStreams())
        {
            setState(STATE_CLOSED);
        }
        */
    }

    /**
     * Handles the end of random access.
     */
    private void endRandomAccess(RandomAccessContent rac)
    {
        getThreadData().removeRastr(rac);
        streamClosed();
        // setState(STATE_CLOSED);
    }

    /**
     * Handles the end of output stream.
     */
    private void endOutput() throws Exception
    {
        streamClosed();

        this.getThreadData().setOutstr(null);
        // setState(STATE_CLOSED);

        file.endOutput();
    }

    /*
    private void setState(int state)
    {
        getThreadData().setState(state);
    }
    */

    /**
     * check if a input and/or output stream is open.<br />
     * This checks only the scope of the current thread.
     *
     * @return true if this is the case
     */
    public boolean isOpen()
    {
        // return getThreadData().getState() == STATE_OPENED;
        return getThreadData().hasStreams();
    }

    /**
     * check if a input and/or output stream is open.<br />
     * This checks all threads.
     *
     * @return true if this is the case
     */
    public boolean isOpenGlobal()
    {
        synchronized (this)
        {
            return openStreams > 0;
        }
    }

    /**
     * An input stream for reading content.  Provides buffering, and
     * end-of-stream monitoring.
     */
    private final class FileContentInputStream
        extends MonitorInputStream
    {
        // avoid gc
        private final FileObject file;

        FileContentInputStream(final FileObject file, final InputStream instr)
        {
            super(instr);
            this.file = file;
        }

        /**
         * Closes this input stream.
         */
        public void close() throws FileSystemException
        {
            try
            {
                super.close();
            }
            catch (final IOException e)
            {
                throw new FileSystemException("vfs.provider/close-instr.error", file, e);
            }
        }

        /**
         * Called after the stream has been closed.
         */
        protected void onClose() throws IOException
        {
            try
            {
                super.onClose();
            }
            finally
            {
                endInput(this);
            }
        }
    }

    /**
     * An input/output stream for reading/writing content on random positions
     */
    private final class FileRandomAccessContent extends MonitorRandomAccessContent
    {
        // avoid gc
        private final FileObject file;
        private final RandomAccessContent content;

        FileRandomAccessContent(final FileObject file, final RandomAccessContent content)
        {
            super(content);
            this.file = file;
            this.content = content;
        }

        /**
         * Called after the stream has been closed.
         */
        protected void onClose() throws IOException
        {
            try
            {
                super.onClose();
            }
            finally
            {
                endRandomAccess(this);
            }
        }
    }

    /**
     * An output stream for writing content.
     */
    final class FileContentOutputStream extends MonitorOutputStream
    {
        // avoid gc
        private final FileObject file;

        FileContentOutputStream(final FileObject file, final OutputStream outstr)
        {
            super(outstr);
            this.file = file;
        }

        /**
         * Closes this output stream.
         */
        public void close() throws FileSystemException
        {
            try
            {
                super.close();
            }
            catch (final IOException e)
            {
                throw new FileSystemException("vfs.provider/close-outstr.error", file, e);
            }
        }

        /**
         * Called after this stream is closed.
         */
        protected void onClose() throws IOException
        {
            try
            {
                super.onClose();
            }
            finally
            {
                try
                {
                    endOutput();
                }
                catch (Exception e)
                {
                    throw new FileSystemException("vfs.provider/close-outstr.error", file, e);
                }
            }
        }
    }

    /**
     * get the content info. e.g. content-type, content-encoding
     */
    public FileContentInfo getContentInfo() throws FileSystemException
    {
        if (fileContentInfo == null)
        {
            fileContentInfo = fileContentInfoFactory.create(this);
        }

        return fileContentInfo;
    }
}
