<?php
/**
 * A class for manipulating files.
 *
 * This class provides a way to open, read, write, lock, truncate and close
 * files.
 */
class file
{
    /**
     * @var integer The lock options.
     */
    const SHARED    = LOCK_SH;
    const EXCLUSIVE = LOCK_EX;
    const NONE      = LOCK_UN;

    /**
     * @var integer The seek options.
     */
    const BEGINNING = SEEK_SET;
    const CURRENT   = SEEK_CUR;
    const END       = SEEK_END;

    /**
     * @var resource The stream.
     */
    protected $stream;

    ////////////////////////////////////////////////////////////////////////////
    // Construct and destruct functions.

    /**
     * @param string (optional) $file The filename.
     * @param string (optional $mode The open mode.
     */
    public function __construct($file = null, $mode = null)
    {
        if ($file !== null)
        {
            $this->open($file, $mode);
        }
    }

    public function __destruct()
    {
        $this->close();
    }

    ////////////////////////////////////////////////////////////////////////////
    // Overload functions.

    ////////////////////////////////////////////////////////////////////////////
    // Interface functions.

    /**
     * Opens a file.
     * 
     * @param string $file The filename.
     * @param integer (optional) $mode The open mode.
     * @throws logicexception If the file is not readable nor writable.
     * @throws unexpectedvalueexception If the file could not be opened.
     * @return file A fluent interface.
     */
    public function open($file, $mode = null)
    {
        $this->close();
        
        if ($mode === null)
        {
            if (is_file($file))
            {
                $readable = is_readable($file);
                $writable = is_writable($file);
                
                if (!$readable && !$writable)
                {
                    throw new logicexception('File ' . var_export($file, true) . ' is not readable nor writable.');
                }
                
                if ($readable && $writable)
                {
                    $mode = 'r+';
                }
                elseif ($readable)
                {
                    $mode = 'r';
                }
                elseif ($writable)
                {
                    $mode = 'a';
                }
            }
            else
            {
                $mode = 'w+';
            }
        }
        
        $stream = fopen($file, $mode, false);
        
        if (!$stream)
        {
            throw new unexpectedvalueexception('File ' . var_export($file, true) . ' could not be opened.');
        }
        
        $this->stream = $stream;
        return $this;
    }

    /**
     * Closes the file.
     *
     * @throws unexpectedvalueexception If the file could not be closed.
     * @return file A fluent interface.
     */
    public function close()
    {
        if ($this->stream)
        {
            if (!fclose($this->stream))
            {
                throw new unexpectedvalueexception('File could not be closed.');
            }
        }
        
        $this->stream = null;
        return $this;
    }

    /**
     * Reads from the file.
     * 
     * @param integer (optional) $length The number of bytes to read.
     * @throws logicexception If the file is not opened.
     * @throws unexpectedvalueexception If the file could not be read from.
     * @return string The read segment.
     */
    public function read($length = null)
    {
        if (!$this->stream)
        {
            throw new logicexception('File is not opened.');
        }
        
        $data = '';
        
        if ($length === null)
        {
            while (!feof($this->stream))
            {
                $read = fread($this->stream, 4096);
                
                if ($read === false)
                {
                    throw new unexpectedvalueexception('File could not be read from.');
                }
                
                $data .= $read;
            }
        }
        else
        {
            $data = fread($this->stream, $length);
            
            if ($data === false)
            {
                throw new unexpectedvalueexception('File could not be read from.');
            }
        }
        
        return $data;
    }

    /**
     * Writes to the file.
     * 
     * @param string $data The data.
     * @param boolean (optional) $insert Whether the writing is in insert mode
     * or not.
     * @throws logicexception If the file is not opened.
     * @throws unexpectedvalueexception If the cursor position cannot be
     * retrieved.
     * @throws unexpectedvalueexception If the data could not be written to
     * file.
     * @return file A fluent interface.
     */
    public function write($data, $insert = false)
    {
        if (!$this->stream)
        {
            throw new logicexception('File is not opened.');
        }
        
        if ($insert)
        {
            $position = ftell($this->stream);
            
            if ($position === false)
            {
                throw new unexpectedvalueexception('Cursor position could not be retrieved.');
            }
            
            $post_data = $this->read();
            $this->seek($position, self::BEGINNING);
            
            if (!fwrite($this->stream, $data . $post_data))
            {
                throw new unexpectedvalueexception('Data could not be written to file.');
            }
        }
        else
        {
            if (!fwrite($this->stream, $data))
            {
                throw new unexpectedvalueexception('Data could not be written to file.');
            }
        }
        
        return $this;
    }

    /**
     * Locks the file.
     * 
     * @param integer (optional) $mode The lock mode.
     * @throws logicexception If the file is not opened.
     * @throws outofrangeexception If the lock mode does not exist.
     * @throws unexpectedvalueexception If the file could not be locked.
     * @return file A fluent interface.
     */
    public function lock($mode = self::SHARED)
    {
        if (!$this->stream)
        {
            throw new logicexception('File is not opened.');
        }
        
        if ($mode !== self::SHARED && $mode !== self::EXCLUSIVE)
        {
            throw new outofrangeexception('Lock mode ' . $mode . ' does not exist.');
        }
        
        if (!flock($this->stream, $mode))
        {
            throw new unexpectedvalueexception('File could not be locked.');
        }
        
        return $this;
    }

    /**
     * Unlocks the file.
     *
     * @throws logicexception If the file is not opened.
     * @throws unexpectedvalueexception If the file could not be unlocked.
     * @return file A fluent interface.
     */
    public function unlock()
    {
        if (!$this->stream)
        {
            throw new logicexception('File is not opened.');
        }
        
        if (!flock($this->stream, self::NONE))
        {
            throw new unexpectedvalueexception('File could not be unlocked.');
        }
        
        return $this;
    }

    /**
     * Truncates the file.
     * 
     * @param integer (optional) $length The size to truncate to.
     * @throws logicexception If the file is not opened.
     * @throws unexpectedvalueexception If the file could not be truncated.
     * @return file A fluent interface.
     */
    public function truncate($length = 0)
    {
        if (!$this->stream)
        {
            throw new logicexception('File is not opened.');
        }
        
        if (!ftruncate($this->stream, $length))
        {
            throw new unexpectedvalueexception('File could not be truncated.');
        }
        
        return $this;
    }

    /**
     * Sets the cursor to a new position.
     * 
     * @param integer $offset The offset.
     * @param integer (optional) $from The starting point to seek from.
     * @throws logicexception If the file is not opened.
     * @throws outofrangeexception If the starting position does not exist.
     * @throws unexpectedvalueexception If the file could not be seeked.
     * @return file A fluent interface.
     */
    public function seek($offset, $from = self::BEGINNING)
    {
        if (!$this->stream)
        {
            throw new logicexception('File is not opened.');
        }
        
        if ($from !== file::BEGINNING && $from !== file::CURRENT && $from !== file::END)
        {
            throw new outofrangeexception('Starting position ' . $from . ' does not exist.');
        }
        
        if (fseek($this->stream, $offset, $from) !== 0)
        {
            throw new unexpectedvalueexception('File could not be seeked.');
        }
        
        return $this;
    }

    ////////////////////////////////////////////////////////////////////////////
    // Internal functions.
}
