<?php
/**
 * Perforce Swarm
 *
 * @copyright   2013 Perforce Software. All rights reserved.
 * @license     Please see LICENSE.txt in top-level folder of this distribution.
 * @version     <release>/<patch>
 */
namespace Record\Cache;

/**
 * Memory efficient iterator for arrays that have been written AND indexed by ArrayWriter
 */
class ArrayReader implements \ArrayAccess, \Iterator
{
    protected $file   = null;
    protected $handle = null;
    protected $index  = null;

    /**
     * Setup a new array reader
     *
     * @param string    $file   the file to read array from
     */
    public function __construct($file)
    {
        $this->file = $file;
    }

    public function openFile()
    {
        $file      = $this->file;
        $indexFile = $file . ArrayWriter::INDEX_SUFFIX;
        if (!is_string($file) || !file_exists($file)) {
            throw new \RuntimeException("Cannot open file '" . $file . "'. File does not exist.");
        }

        $this->handle = @fopen($file, 'r');
        if ($this->handle === false) {
            throw new \RuntimeException("Unable to open file ('" . $file . "') for reading.");
        }

        // if anything goes wrong past this point, make sure we close/unlock our file
        try {
            // wait for a read lock to ensure file is not being actively written to
            $locked = flock($this->handle, LOCK_SH);
            if ($locked === false) {
                throw new \RuntimeException("Unable to lock file ('" . $file . "') for reading.");
            }

            if (!file_exists($indexFile)) {
                throw new \RuntimeException("Cannot open index file '" . $indexFile . "'. File does not exist.");
            }

            // read the entire index into memory (impractical to stream)
            $this->index = unserialize(file_get_contents($indexFile));
            if ($this->index === false) {
                throw new \RuntimeException("Cannot unserialize index file ('" . $indexFile . "').");
            }
        } catch (\Exception $e) {
            $this->closeFile();
            throw $e;
        }

        return $this;
    }

    public function closeFile()
    {
        if (!is_resource($this->handle)) {
            throw new \RuntimeException("Cannot close file. File is not open.");
        }

        flock($this->handle, LOCK_UN);
        fclose($this->handle);

        return $this;
    }

    /**
     * Do a case-insensitive lookup for the given key - first match wins
     *
     * @param   mixed   $key    the array key to look for
     * @return  mixed   the matching key (in original case) or false if no match
     */
    public function noCaseLookup($key)
    {
        // note we make a local-scope (lazy) copy to avoid mucking the cursor
        $index = $this->index;
        foreach ($index as $candidate => $value) {
            if (strcasecmp($key, $candidate) === 0) {
                return $candidate;
            }
        }

        return false;
    }

    public function offsetExists($key)
    {
        // attempt to match PHP's key casting behavior
        // http://php.net/manual/en/language.types.array.php
        if (is_object($key) || is_array($key)) {
            return false;
        }
        if (is_null($key)) {
            $key = "";
        }
        if (!is_string($key) && !is_int($key)) {
            $key = (int) $key;
        }
        return array_key_exists($key, $this->index);
    }

    public function offsetGet($key)
    {
        if (!$this->offsetExists($key)) {
            return null;
        }

        $offset = $this->index[$key][0];
        $length = $this->index[$key][1];
        fseek($this->handle, $offset);

        // need to wrap serialized key/value in array format 'a:1{...}'
        // so that it will unserialize correctly into key/value
        $entry = unserialize('a:1:{' . fread($this->handle, $length) . '}');
        return $entry ? current($entry) : false;
    }

    public function offsetSet($key, $value)
    {
        throw new \BadMethodCallException("Cannot set element. Array is read-only.");
    }

    public function offsetUnset($offset)
    {
        throw new \BadMethodCallException("Cannot unset element. Array is read-only.");
    }

    public function current()
    {
        return $this->offsetGet(key($this->index));
    }

    public function key()
    {
        return key($this->index);
    }

    public function next()
    {
        next($this->index);
    }

    public function rewind()
    {
        reset($this->index);
    }

    public function valid()
    {
        return key($this->index) !== null;
    }
}