<?php

/*
 * This file is part of SwiftMailer.
 * (c) 2004-2009 Chris Corbyn
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */


/**
 * Allows reading and writing of bytes to and from a file.
 * @package Swift
 * @subpackage ByteStream
 * @author Chris Corbyn
 */
class Swift_ByteStream_FileByteStream
  extends Swift_ByteStream_AbstractFilterableInputStream
  implements Swift_FileStream
{
  
  /** The internal pointer offset */
  private $_offset = 0;
  
  /** The path to the file */
  private $_path;
  
  /** The mode this file is opened in for writing */
  private $_mode;
  
  /** A lazy-loaded resource handle for reading the file */
  private $_reader;
  
  /** A lazy-loaded resource handle for writing the file */
  private $_writer;
  
  /** If magic_quotes_runtime is on, this will be true */
  private $_quotes = false;

  /** If stream is seekable true/false, or null if not known */
  private $_seekable = null;
  
  /**
   * Create a new FileByteStream for $path.
   * @param string $path
   * @param string $writable if true
   */
  public function __construct($path, $writable = false)
  {
    $this->_path = $path;
    $this->_mode = $writable ? 'w+b' : 'rb';
    
    if (function_exists('get_magic_quotes_runtime') && @get_magic_quotes_runtime() == 1)
    {
      $this->_quotes = true;
    }
  }
  
  /**
   * Get the complete path to the file.
   * @return string
   */
  public function getPath()
  {
    return $this->_path;
  }
  
  /**
   * Reads $length bytes from the stream into a string and moves the pointer
   * through the stream by $length. If less bytes exist than are requested the
   * remaining bytes are given instead. If no bytes are remaining at all, boolean
   * false is returned.
   * @param int $length
   * @return string
   * @throws Swift_IoException
   */
  public function read($length)
  {
    $fp = $this->_getReadHandle();
    if (!feof($fp))
    {
      if ($this->_quotes)
      {
        ini_set('magic_quotes_runtime', 0);
      }
      $bytes = fread($fp, $length);
      if ($this->_quotes)
      {
        ini_set('magic_quotes_runtime', 1);
      }
      $this->_offset = ftell($fp);
      return $bytes;
    }
    else
    {
      return false;
    }
  }
  
  /**
   * Move the internal read pointer to $byteOffset in the stream.
   * @param int $byteOffset
   * @return boolean
   */
  public function setReadPointer($byteOffset)
  {
    if (isset($this->_reader))
    {
      $this->_seekReadStreamToPosition($byteOffset);
    }
    $this->_offset = $byteOffset;
  }
  
  // -- Private methods
  
  /** Just write the bytes to the file */
  protected function _commit($bytes)
  {
    fwrite($this->_getWriteHandle(), $bytes);
    $this->_resetReadHandle();
  }
  
  /** Not used */
  protected function _flush()
  {
  }
  
  /** Get the resource for reading */
  private function _getReadHandle()
  {
    if (!isset($this->_reader))
    {
      if (!$this->_reader = fopen($this->_path, 'rb'))
      {
        throw new Swift_IoException(
          'Unable to open file for reading [' . $this->_path . ']'
        );
      }
      if ($this->_offset <> 0)
      {
        $this->_getReadStreamSeekableStatus();
        $this->_seekReadStreamToPosition($this->_offset);
      }
    }
    return $this->_reader;
  }
  
  /** Get the resource for writing */
  private function _getWriteHandle()
  {
    if (!isset($this->_writer))
    {
      if (!$this->_writer = fopen($this->_path, $this->_mode))
      {
        throw new Swift_IoException(
          'Unable to open file for writing [' . $this->_path . ']'
        );
      }
    }
    return $this->_writer;
  }
  
  /** Force a reload of the resource for reading */
  private function _resetReadHandle()
  {
    if (isset($this->_reader))
    {
      fclose($this->_reader);
      $this->_reader = null;
    }
  }
  
  /** Check if ReadOnly Stream is seekable */
  private function _getReadStreamSeekableStatus()
  {
    $metas = stream_get_meta_data($this->_reader);
    $this->_seekable = $metas['seekable'];
  }
  
  /** Streams in a readOnly stream ensuring copy if needed */
  private function _seekReadStreamToPosition($offset)
  {
    if ($this->_seekable===null)
    {
      $this->_getReadStreamSeekableStatus();
    }
    if ($this->_seekable === false)
    {
      $currentPos = ftell($this->_reader);
      if ($currentPos<$offset)
      {
        $toDiscard = $offset-$currentPos;
        fread($this->_reader, $toDiscard);
        return;
      }
      $this->_copyReadStream();
    }
    fseek($this->_reader, $offset, SEEK_SET);
  }
  
  /** Copy a readOnly Stream to ensure seekability */
  private function _copyReadStream()
  {
    if ($tmpFile = fopen('php://temp/maxmemory:4096', 'w+b'))
    {
      /* We have opened a php:// Stream Should work without problem */
    } 
    elseif (function_exists('sys_get_temp_dir') && is_writable(sys_get_temp_dir()) && ($tmpFile = tmpfile())) 
    {
      /* We have opened a tmpfile */
    } 
    else
    {
      throw new Swift_IoException('Unable to copy the file to make it seekable, sys_temp_dir is not writable, php://memory not available');
    }
    $currentPos = ftell($this->_reader);
    fclose($this->_reader);
    $source = fopen($this->_path, 'rb');
    if (!$source)
    {
      throw new Swift_IoException('Unable to open file for copying [' . $this->_path . ']');
    }
    fseek($tmpFile, 0, SEEK_SET);
    while (!feof($source)) 
    {
      fwrite($tmpFile, fread($source, 4096));
    }
    fseek($tmpFile, $currentPos, SEEK_SET);
    fclose($source);
    $this->_reader = $tmpFile;
  }
}
