/*
 * FileBuffer.java February 2008
 *
 * Copyright (C) 2008, Niall Gallagher <niallg@users.sf.net>
 *
 * Licensed 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.simpleframework.util.buffer;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

/**
 * The <code>FileBuffer</code> object is used to create a buffer
 * which will write the appended data to an underlying file. This
 * is typically used for buffers that are too large for to allocate
 * in memory. Data appended to the buffer can be retrieved at a 
 * later stage by acquiring the <code>InputStream</code> for the
 * underlying file. To ensure that excessive file system space is
 * not occupied the buffer files are cleaned every five minutes.
 * 
 * @author Niall Gallagher
 * 
 * @see org.simpleframework.util.buffer.FileAllocator
 */
class FileBuffer implements Buffer {
   
   /**
    * This is the file output stream used for this buffer object.
    */
   private OutputStream buffer;
   
   /**
    * This represents the last file segment that has been created.
    */
   private Segment segment;
   
   /**
    * This is the path for the file that this buffer appends to.
    */
   private File file;
   
   /**
    * This is the number of bytes currently appended to the buffer.
    */
   private int count;
   
   /**
    * This is used to determine if this buffer has been closed.
    */
   private boolean closed;
   
   /**
    * Constructor for the <code>FileBuffer</code> object. This will
    * create a buffer using the provided file. All data appended to
    * this buffer will effectively written to the underlying file. 
    * If the appended data needs to be retrieved at a later stage
    * then it can be acquired using the buffers input stream.
    * 
    * @param file this is the file used for the file buffer
    */
   public FileBuffer(File file) throws IOException {
      this.buffer  = new FileOutputStream(file);
      this.file = file;
   }

   /**
    * This is used to allocate a segment within this buffer. If the
    * buffer is closed this will throw an exception, if however the
    * buffer is still open then a segment is created which will 
    * write all appended data to this buffer. However it can be
    * treated as an independent source of data.
    * 
    * @return this returns a buffer which is a segment of this 
    */
   public Buffer allocate() throws IOException {
      if(closed) {
         throw new BufferException("Buffer has been closed");
      }
      if(segment != null) {
         segment.close();
      }
      if(!closed) {
         segment = new Segment(this, count);
      }
      return segment;
   }

   /**
    * This is used to append the specified data to the underlying 
    * file. All bytes appended to the file can be consumed at a
    * later stage by acquiring the <code>InputStream</code> from
    * this buffer. Also if require the data can be encoded as a
    * string object in a required character set.
    * 
    * @param array this is the array to write the the file
    * 
    * @return this returns this buffer for further operations
    */
   public Buffer append(byte[] array) throws IOException {
      return append(array, 0, array.length);
   }

   /**
    * This is used to append the specified data to the underlying 
    * file. All bytes appended to the file can be consumed at a
    * later stage by acquiring the <code>InputStream</code> from
    * this buffer. Also if require the data can be encoded as a
    * string object in a required character set.
    * 
    * @param array this is the array to write the the file
    * @param off this is the offset within the array to write
    * @param size this is the number of bytes to be appended
    * 
    * @return this returns this buffer for further operations
    */
   public Buffer append(byte[] array, int off, int size) throws IOException {
      if(closed) {
         throw new BufferException("Buffer has been closed");
      }
      if(size > 0) {
         buffer.write(array, off, size);
         count += size;
      }
      return this;
   }

   /**
    * This method is used to acquire the buffered bytes as a string.
    * This is useful if the contents need to be manipulated as a
    * string or transferred into another encoding. If the UTF-8
    * content encoding is not supported the platform default is 
    * used, however this is unlikely as UTF-8 should be supported.
    *
    * @return this returns a UTF-8 encoding of the buffer contents
    */ 
   public String encode() throws IOException {
      return encode("UTF-8");
   }

   /**
    * This method is used to acquire the buffered bytes as a string.
    * This is useful if the contents need to be manipulated as a
    * string or transferred into another encoding. This will convert
    * the bytes using the specified character encoding format.
    * 
    * @param charset this is the charset to encode the data with
    *
    * @return this returns the encoding of the buffer contents
    */   
   public String encode(String charset) throws IOException {
      InputStream source = getInputStream();
      
      if(count <= 0) {
         return new String();
      }
      return convert(source, charset, count); 
   }
   
   /**
    * This method is used to acquire the buffered bytes as a string.
    * This is useful if the contents need to be manipulated as a
    * string or transferred into another encoding. This will convert
    * the bytes using the specified character encoding format.
    * 
    * @param source this is the source stream that is to be encoded
    * @param charset this is the charset to encode the data with
    * @param count this is the number of bytes to be encoded
    *
    * @return this returns the encoding of the buffer contents
    */   
   private String convert(InputStream source, String charset, int count) throws IOException {
      byte[] buffer = new byte[count];
      int left = count;
      int mark = 0;
      
      while(left > 0) {
         int size = source.read(buffer, 0, left);
         
         if(size == -1) {
            throw new BufferException("Could not read buffer");
         }
         left -= count;
         mark += count;
      }
      return new String(buffer, charset);
   }

   /**
    * This method is used so that a buffer can be represented as a
    * stream of bytes. This provides a quick means to access the data
    * that has been written to the buffer. It wraps the buffer within
    * an input stream so that it can be read directly.
    *
    * @return a stream that can be used to read the buffered bytes
    */ 
   public InputStream getInputStream() throws IOException {
      if(!closed) {
         close();
      }
      return getInputStream(file);
   }
   
   /**
    * This method is used so that a buffer can be represented as a
    * stream of bytes. This provides a quick means to access the data
    * that has been written to the buffer. It wraps the buffer within
    * an input stream so that it can be read directly.
    *
    * @param file this is the file used to create the input stream
    *
    * @return a stream that can be used to read the buffered bytes
    */ 
   private InputStream getInputStream(File file) throws IOException {
      InputStream source = new FileInputStream(file);
      
      if(count <= 0) {
         source.close(); // release file descriptor
      }
      return new Range(source, count);
   }

   /**
    * This will clear all data from the buffer. This simply sets the
    * count to be zero, it will not clear the memory occupied by the
    * instance as the internal buffer will remain. This allows the
    * memory occupied to be reused as many times as is required.
    */   
   public void clear() throws IOException {
      if(closed) {
         throw new BufferException("Buffer has been closed");
      }
   }

   /**
    * This method is used to ensure the buffer can be closed. Once
    * the buffer is closed it is an immutable collection of bytes and
    * can not longer be modified. This ensures that it can be passed
    * by value without the risk of modification of the bytes.
    */   
   public void close() throws IOException {
      if(!closed) {
         buffer.close();
         closed = true;
      }
      if(segment != null) {
         segment.close();
      }
   }

   /**
    * The <code>Segment</code> object is used to create a segment of
    * the parent buffer. The segment will write to the parent however
    * if can be read as a unique range of bytes starting with the 
    * first sequence of bytes appended to the segment. A segment can
    * be used to create a collection of buffers backed by the same
    * underlying file, as is require with multipart uploads.
    * 
    * @author Niall Gallagher
    */
   private class Segment implements Buffer {
        
      /**
       * This is an internal segment created from this buffer object.
       */
      private Segment segment;
      
      /**
       * This is the parent buffer that bytes are to be appended to.
       */
      private Buffer parent;
      
      /**
       * This is the offset of the first byte within the sequence.
       */
      private int first;
      
      /**
       * This is the last byte within the segment for this segment.
       */
      private int last;
      
      /**
       * This determines if the segment is currently open or closed.
       */
      private boolean closed;
      
      /**
       * Constructor for the <code>Segment</code> object. This is used
       * to create a segment from a parent buffer. A segment is a part
       * of the parent buffer and appends its bytes to the parent. It
       * can however be treated as an independent source of bytes.
       * 
       * @param parent this is the parent buffer to be appended to
       * @param first this is the offset for the first byte in this
       */
      public Segment(Buffer parent, int first) {
         this.parent = parent;
         this.first = first;
         this.last = first;
      }

      /**
       * This is used to allocate a segment within this buffer. If the
       * buffer is closed this will throw an exception, if however the
       * buffer is still open then a segment is created which will 
       * write all appended data to this buffer. However it can be
       * treated as an independent source of data.
       * 
       * @return this returns a buffer which is a segment of this 
       */
      public Buffer allocate() throws IOException {
         if(closed) {
            throw new BufferException("Buffer has been closed");
         }
         if(segment != null) {
            segment.close();
         }
         if(!closed) {
            segment = new Segment(this, last);
         }
         return segment;
      }

      /**
       * This is used to append the specified data to the underlying 
       * file. All bytes appended to the file can be consumed at a
       * later stage by acquiring the <code>InputStream</code> from
       * this buffer. Also if require the data can be encoded as a
       * string object in a required character set.
       * 
       * @param array this is the array to write the the file
       * 
       * @return this returns this buffer for further operations
       */
      public Buffer append(byte[] array) throws IOException {
         return append(array, 0, array.length);
      }

      /**
       * This is used to append the specified data to the underlying 
       * file. All bytes appended to the file can be consumed at a
       * later stage by acquiring the <code>InputStream</code> from
       * this buffer. Also if require the data can be encoded as a
       * string object in a required character set.
       * 
       * @param array this is the array to write the the file
       * @param off this is the offset within the array to write
       * @param size this is the number of bytes to be appended
       * 
       * @return this returns this buffer for further operations
       */
      public Buffer append(byte[] array, int off, int size) throws IOException {
         if(closed) {
            throw new BufferException("Buffer has been closed");
         }
         if(size > 0) {
            parent.append(array, off, size);
            last += size;
         }
         return this;
      }

      /**
       * This method is used to acquire the buffered bytes as a string.
       * This is useful if the contents need to be manipulated as a
       * string or transferred into another encoding. If the UTF-8
       * content encoding is not supported the platform default is 
       * used, however this is unlikely as UTF-8 should be supported.
       *
       * @return this returns a UTF-8 encoding of the buffer contents
       */ 
      public String encode() throws IOException {
         return encode("UTF-8");
      }

      /**
       * This method is used to acquire the buffered bytes as a string.
       * This is useful if the contents need to be manipulated as a
       * string or transferred into another encoding. This will convert
       * the bytes using the specified character encoding format.
       * 
       * @param charset this is the charset to encode the data with
       *
       * @return this returns the encoding of the buffer contents
       */  
      public String encode(String charset) throws IOException {
         InputStream source = getInputStream();
         int count = last - first;
         
         if(count <= 0) {
            return new String();
         }
         return convert(source, charset, count);    
      }

      /**
       * This method is used so that a buffer can be represented as a
       * stream of bytes. This provides a quick means to access the data
       * that has been written to the buffer. It wraps the buffer within
       * an input stream so that it can be read directly.
       *
       * @return a stream that can be used to read the buffered bytes
       */ 
      public InputStream getInputStream() throws IOException {
         InputStream source = new FileInputStream(file);
         int length = last - first;
         
         if(first > 0) {
            source.skip(first);
         }
         return new Range(source, length);
      }
      
      /**
       * This will clear all data from the buffer. This simply sets the
       * count to be zero, it will not clear the memory occupied by the
       * instance as the internal buffer will remain. This allows the
       * memory occupied to be reused as many times as is required.
       */  
      public void clear() throws IOException {
         if(closed) {
            throw new BufferException("Buffer is closed");
         }
      }

      /**
       * This method is used to ensure the buffer can be closed. Once
       * the buffer is closed it is an immutable collection of bytes and
       * can not longer be modified. This ensures that it can be passed
       * by value without the risk of modification of the bytes.
       */   
      public void close() throws IOException {
         if(!closed) {
            closed = true;
         }
         if(segment != null) {
            segment.close();
         }
      }
   }
   
   /**
    * The <code>Range</code> object is used to provide a stream that
    * can read a range of bytes from a provided input stream. This
    * allows buffer segments to be allocated from the main buffer.
    * Providing a range in this manner ensures that only one backing
    * file is needed for the primary buffer allocated.
    * 
    * @author Niall Gallagher
    */
   private class Range extends FilterInputStream {
        
      /**
       * This is the length of the bytes that exist in the range.
       */
      private int length;
      
      /**
       * This is used to close the stream once it has been read.
       */
      private boolean closed;
      
      /**
       * Constructor for the <code>Range</code> object. This ensures
       * that only a limited number of bytes can be consumed from a
       * backing input stream giving the impression of an independent
       * stream of bytes for a segmented region of the parent buffer.
       * 
       * @param source this is the input stream used to read data
       * @param length this is the number of bytes that can be read
       */ 
      public Range(InputStream source, int length) {
         super(source);
         this.length = length;
      }
    
      /**
       * This will read data from the underlying stream up to the 
       * number of bytes this range is allowed to read. When all of
       * the bytes are exhausted within the stream this returns -1.
       * 
       * @return this returns the octet from the underlying stream
       */
      @Override
      public int read() throws IOException {
         if(length-- > 0) {
            return in.read();
         }
         if(length <= 0) {
            close();
         }
         return -1;
      }
      
      /**
       * This will read data from the underlying stream up to the 
       * number of bytes this range is allowed to read. When all of
       * the bytes are exhausted within the stream this returns -1.
       * 
       * @param array this is the array to read the bytes in to
       * @param off this is the start offset to append the bytes to
       * @param size this is the number of bytes that are required
       * 
       * @return this returns the number of bytes that were read
       */
      @Override
      public int read(byte[] array, int off, int size) throws IOException {
         int left = Math.min(length, size);
         
         if(left > 0) {
            int count = in.read(array, off, left);
            
            if(count > 0){
               length -= count;
            }
            if(length <= 0) {
               close();
            }
            return count;
         }
         return -1;
      }
      
      /**
       * This returns the number of bytes that can be read from the
       * range. This will be the actual number of bytes the range
       * contains as the underlying file will not block reading.
       * 
       * @return this returns the number of bytes within the range
       */
      @Override
      public int available() throws IOException {
         return length;
      }
      
      /**
       * This is the number of bytes to skip from the buffer. This 
       * will allow up to the number of remaining bytes within the
       * range to be read. When all the bytes have been read this
       * will return zero indicating no bytes were skipped.
       * 
       * @param size this returns the number of bytes to skip
       * 
       * @return this returns the number of bytes that were skipped
       */
      @Override
      public long skip(long size) throws IOException {
         long left = Math.min(length, size);
         long skip = in.skip(left);
         
         if(skip > 0) {
            length -= skip;
         } 
         if(length <= 0) {
            close();
         }
         return skip;
      }
      
      /**
       * This is used to close the range once all of the content has
       * been fully read. The <code>Range</code> object forces the
       * close of the stream once all the content has been consumed 
       * to ensure that excessive file descriptors are used. Also
       * this will ensure that the files can be deleted.
       */
      @Override
      public void close() throws IOException {
         if(!closed) {
            in.close();
            closed =true;
         }
      }
   }
}
