/*
 * Copyright (C) 2006 Alfresco, Inc.
 *
 * Licensed under the Mozilla Public License version 1.1 
 * with a permitted attribution clause. You may obtain a
 * copy of the License at
 *
 *   http://www.alfresco.org/legal/license.txt
 *
 * 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.alfresco.filesys.avm;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.filesys.server.filesys.AccessDeniedException;
import org.alfresco.filesys.server.filesys.FileAttribute;
import org.alfresco.filesys.server.filesys.NetworkFile;
import org.alfresco.filesys.smb.SeekType;
import org.alfresco.service.cmr.avm.AVMNodeDescriptor;
import org.alfresco.service.cmr.avm.AVMService;
import org.alfresco.service.cmr.repository.ContentReader;
import org.alfresco.service.cmr.repository.ContentWriter;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * AVM Network File Class
 * 
 * <p>Holds the details of an open file, and provides access to the file data.
 *
 * @author GKSpencer
 */
public class AVMNetworkFile extends NetworkFile {

	// Logging
	
	private static final Log logger = LogFactory.getLog(AVMNetworkFile.class);

	// AVM service
	
	private AVMService m_avmService;
	
	// AVM path to the file/folder and store version
	
	private String m_avmPath;
	private int m_avmVersion;
	
	// Flag to indicate if the file has been modified
	
	private boolean m_modified;
	
	// Access to the file data, flag to indicate if the file channel is writable
	
	private FileChannel m_channel;
	private boolean m_writable;
	
	// Mime type, if a writer is opened
	
	private String m_mimeType;
	
	/**
	 * Class constructor
	 * 
	 * @param details AVMNodeDescriptor
	 * @param avmPath String
	 * @param avmVersion int
	 * @param avmService AVMService
	 */
	public AVMNetworkFile( AVMNodeDescriptor details, String avmPath, int avmVersion, AVMService avmService)
	{
		super( details.getName());
	
		// Save the service, apth and version
		
		m_avmService = avmService;
		m_avmPath    = avmPath;
		m_avmVersion = avmVersion;
		
		// Copy the file details
		
		setAccessDate( details.getAccessDate());
		setCreationDate( details.getCreateDate());
		setModifyDate( details.getModDate());
		
		if ( details.isFile())
			setFileSize( details.getLength());
		else
			setFileSize( 0L);
		
		int attr = 0;
		
		if ( details.isDirectory())
			attr += FileAttribute.Directory;
		
		if ( avmVersion != AVMContext.VERSION_HEAD)
			attr += FileAttribute.ReadOnly;
		
		setAttributes( attr);
	}

	/**
	 * Check if there is an open file channel to the content
	 * 
	 * @return boolean
	 */
	public final boolean hasContentChannel()
	{
		return m_channel != null ? true : false;
	}
	
	/**
	 * Return the mime type
	 * 
	 * @return String
	 */
	public final String getMimeType()
	{
		return m_mimeType;
	}
	
	/**
	 * Set the mime type
	 * 
	 * @param mimeType String
	 */
	public final void setMimeType(String mimeType)
	{
		m_mimeType = mimeType;
	}
	
	/**
     * Open the file
     * 
     * @param createFlag boolean
     * @exception IOException
     */
    public void openFile(boolean createFlag)
    	throws IOException
    {
    	// Nothing to do, content is opened on first read/write
    }

    /**
     * Read from the file.
     * 
     * @param buf byte[]
     * @param len int
     * @param pos int
     * @param fileOff long
     * @return Length of data read.
     * @exception IOException
     */
    public int readFile(byte[] buf, int len, int pos, long fileOff)
    	throws java.io.IOException
    {
    	// DEBUG
    	
    	if ( logger.isDebugEnabled())
    		logger.debug("Read file " + getName() + ", len=" + len + ", offset=" + fileOff);
    	
        // Open the channel for reading
        
        openContent(false, false);
        
        // Read from the channel
        
        ByteBuffer byteBuffer = ByteBuffer.wrap(buf, pos, len);
        int count = m_channel.read(byteBuffer, fileOff);
        if (count < 0)
        {
        	// Return a zero count at end of file
        	
            count = 0;
        }
        
        // Return the length of data read
        
        return count;
    }

    /**
     * Write a block of data to the file.
     * 
     * @param buf byte[]
     * @param len int
     * @param pos int
     * @param fileOff long
     * @exception IOException
     */
    public void writeFile(byte[] buf, int len, int pos, long fileOff)
    	throws java.io.IOException
    {
    	// DEBUG
    	
    	if ( logger.isDebugEnabled())
    		logger.debug("Write file " + getName() + ", len=" + len + ", offset=" + fileOff);
    	
        // Open the channel for writing
        
        openContent(true, false);
        
        // Write to the channel
        
        ByteBuffer byteBuffer = ByteBuffer.wrap(buf, pos, len);
        int count = m_channel.write(byteBuffer, fileOff);
        
        // Set modification flag
        
        m_modified = true;

        // Update the current file size
        
        setFileSize( m_channel.size());
    }

    /**
     * Seek to the specified file position.
     * 
     * @param pos long
     * @param typ int
     * @return int
     * @exception IOException
     */
    public long seekFile(long pos, int typ)
    	throws IOException
    {
        //  Open the file, if not already open

        openContent( false, false);

        //  Check if the current file position is the required file position

        long curPos = m_channel.position();
        
        switch (typ) {

          //  From start of file

          case SeekType.StartOfFile :
            if (curPos != pos)
              m_channel.position( pos);
            break;

            //  From current position

          case SeekType.CurrentPos :
            m_channel.position( curPos + pos);
            break;

            //  From end of file

          case SeekType.EndOfFile :
            {
              long newPos = m_channel.size() + pos;
              m_channel.position(newPos);
            }
            break;
        }

        //  Return the new file position

        return m_channel.position();
    }

    /**
     * Flush any buffered output to the file
     * 
     * @throws IOException
     */
    public void flushFile()
    	throws IOException
    {
    	// If the file channel is open for write then flush the channel
    	
    	if ( m_channel != null && m_writable)
    		m_channel.force( false);
    }
    
    /**
     * Truncate the file to the specified file size
     * 
     * @param siz long
     * @exception IOException
     */
    public void truncateFile(long siz)
    	throws IOException
    {
    	// DEBUG
    	
    	if ( logger.isDebugEnabled())
    		logger.debug("Truncate file " + getName() + ", size=" + siz);
    	
        // If the content data channel has not been opened yet and the requested size is zero
        // then this is an open for overwrite so the existing content data is not copied
        
        if ( m_channel == null && siz == 0L)
        {
            // Open content for overwrite, no need to copy existing content data
            
            openContent(true, true);
        }
        else
        {
            // Normal open for write
            
            openContent(true, false);

            // Truncate or extend the channel
            
            m_channel.truncate(siz);
        }
        
        // Set modification flag
        
        m_modified = true;
    }

    /**
     * Close the database file
     */
    public void closeFile()
    	throws IOException
    {
    	// If the file is a directory or the file channel has not been opened then there is nothing to do
    	
    	if ( isDirectory() || m_channel == null)
    		return;
    	
    	// Close the file channel
    		
    	try
    	{
    		m_channel.close();
    		m_channel = null;
    	}
    	catch ( IOException ex)
    	{
    		logger.error("Failed to close file channel for " + getName(), ex);
    	}
    }
    
    /**
     * Open a file channel to the file content, switching to a writable file channel if required.
     * 
     * @param write boolean
     * @param trunc boolean
     * @throws AccessDeniedException If this network file is read only
     * @throws AlfrescoRuntimeException If this network file represents a directory
     */
    private void openContent(boolean write, boolean trunc)
    	throws AccessDeniedException, AlfrescoRuntimeException
    {
    	// Check if this network file is a directory, no content to open
    	
        if ( isDirectory())
            throw new AlfrescoRuntimeException("Unable to open channel for a directory network file: " + this);
        
        // Check if write access is required and the current channel is read-only

        long curPos = 0L;
        
        if ( write && m_writable == false && m_channel != null)
        {
            // Close the existing read-only channel
            
            try
            {
            	// Save the current file position
            	
            	curPos = m_channel.position();
            	
            	// Close the read-only file channel
            	
                m_channel.close();
                m_channel = null;
            }
            catch (IOException ex)
            {
                logger.error("Error closing read-only channel", ex);
            }
            
            // Debug
            
            if ( logger.isDebugEnabled())
                logger.debug("Switching to writable channel for " + getName());
        }
        else if ( m_channel != null)
        {
            // File channel already open
        	
            return;
        }
        
        // We need to create the channel
        
        if (write && getGrantedAccess() == NetworkFile.READONLY)
            throw new AccessDeniedException("The network file was created for read-only: " + this);

        // Access the content data and get a file channel to the data
        
        if ( write)
        {
        	// Access the content data for write

        	ContentWriter cWriter = null;
        	
        	try {
        		
        		// Create a writer to access the file data
        		
        		cWriter = m_avmService.createContentWriter( m_avmPath);
        		
        		// Set the mime-type

        		cWriter.setMimetype( getMimeType());
        	}
        	catch (Exception ex) {
        		logger.debug( ex);
        		ex.printStackTrace();
        	}

        	// Indicate that we have a writable channel to the file
            
            m_writable = true;
            
            // Get the writable channel, do not copy existing content data if the file is to be truncated
            
            m_channel = cWriter.getFileChannel( trunc);
            
            // Reset the file position to match the read-only file channel position, unless we truncated the file
            
            if ( curPos != 0L && trunc == false)
            {
            	try
            	{
            		m_channel.position( curPos);
            	}
            	catch (IOException ex)
            	{
            		logger.error("Failed to set file position for " + getName(), ex);
            	}
            }
        }
        else
        {
        	// Access the content data for read
        	
            ContentReader cReader = m_avmService.getContentReader( m_avmVersion, m_avmPath);
            
            // Indicate that we only have a read-only channel to the data
            
            m_writable = false;
            
            // Get the read-only channel
            
            m_channel = cReader.getFileChannel();
        }
    }
    
    /**
     * Return the writable state of the content channel
     * 
     * @return boolean
     */
    public final boolean isWritable()
    {
    	return m_writable;
    }
    
    /**
     * Return the network file details as a string
     * 
     * @return String
     */
    public String toString()
    {
    	StringBuilder str = new StringBuilder();
    	
    	str.append( "[");
    	str.append( getName());
    	str.append( ":");
    	str.append( isDirectory() ? "Dir," : "File,");
    	str.append( getFileSize());
    	str.append( "-Channel=");
    	str.append( m_channel);
    	str.append( m_writable ? ",Write" : ",Read");
    	str.append( m_modified ? ",Modified" : "");
    	str.append( "]");
    	
    	return str.toString();
    }
}