<?php
namespace EasyRdf;

/**
 * EasyRdf
 *
 * LICENSE
 *
 * Copyright (c) 2013-2014 Nicholas J Humfrey.  All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 * 3. The name of the author 'Nicholas J Humfrey" may be used to endorse or
 *    promote products derived from this software without specific prior
 *    written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 *
 * @package    EasyRdf
 * @copyright  Copyright (c) 2013-2014 Nicholas J Humfrey
 * @license    https://www.opensource.org/licenses/bsd-license.php
 */

/**
 * Sub-class of EasyRdf\Resource that represents an RDF collection (rdf:List)
 *
 * This class can be used to iterate through a collection of items.
 *
 * Note that items are numbered from 1 (not 0) for consistency with RDF Containers.
 *
 * @package    EasyRdf
 * @link       http://www.w3.org/TR/xmlschema-2/#date
 * @copyright  Copyright (c) 2013-2014 Nicholas J Humfrey
 * @license    https://www.opensource.org/licenses/bsd-license.php
 */
class Collection extends Resource implements \ArrayAccess, \Countable, \SeekableIterator
{
    private $position;
    private $current;

    /** Create a new collection - do not use this directly
     *
     * @ignore
     */
    public function __construct($uri, $graph)
    {
        $this->position = 1;
        $this->current = null;
        parent::__construct($uri, $graph);
    }

    /** Seek to a specific position in the container
     *
     * The first item is postion 1
     *
     * @param  integer  $position     The position in the container to seek to
     *
     * @throws \OutOfBoundsException
     * @throws \InvalidArgumentException
     */
    public function seek($position)
    {
        if (is_int($position) and $position > 0) {
            list($node, $actual) = $this->getCollectionNode($position);
            if ($actual === $position) {
                $this->position = $actual;
                $this->current = $node;
            } else {
                throw new \OutOfBoundsException(
                    "Unable to seek to position $position in the collection"
                );
            }
        } else {
            throw new \InvalidArgumentException(
                "Collection position must be a positive integer"
            );
        }
    }

    /** Rewind the iterator back to the start of the collection
     *
     */
    public function rewind()
    {
        $this->position = 1;
        $this->current = null;
    }

    /** Return the current item in the collection
     *
     * @return mixed The current item
     */
    public function current()
    {
        if ($this->position === 1) {
            return $this->get('rdf:first');
        } elseif ($this->current) {
            return $this->current->get('rdf:first');
        }
    }

    /** Return the key / current position in the collection
     *
     * Note: the first item is number 1
     *
     * @return int The current position
     */
    public function key()
    {
        return $this->position;
    }

    /** Move forward to next item in the collection
     *
     */
    public function next()
    {
        if ($this->position === 1) {
            $this->current = $this->get('rdf:rest');
        } elseif ($this->current) {
            $this->current = $this->current->get('rdf:rest');
        }
        $this->position++;
    }

    /** Checks if current position is valid
     *
     * @return bool True if the current position is valid
     */
    public function valid()
    {
        if ($this->position === 1 and $this->hasProperty('rdf:first')) {
            return true;
        } elseif ($this->current !== null and $this->current->hasProperty('rdf:first')) {
            return true;
        } else {
            return false;
        }
    }

    /** Get a node for a particular offset into the collection
     *
     * This function may not return the item you requested, if
     * it does not exist. Please check the $postion parameter
     * returned.
     *
     * If the offset is null, then the last node in the
     * collection (before rdf:nil) will be returned.
     *
     * @param  integer $offset          The offset into the collection (or null)
     *
     * @return array   $node, $postion  The node object and postion of the node
     */
    public function getCollectionNode($offset)
    {
        $position = 1;
        $node = $this;
        $nil = $this->graph->resource('rdf:nil');
        while (($rest = $node->get('rdf:rest')) and $rest !== $nil and (is_null($offset) or ($position < $offset))) {
            $node = $rest;
            $position++;
        }
        return array($node, $position);
    }

    /** Counts the number of items in the collection
     *
     * Note that this is an slow method - it is more efficient to use
     * the iterator interface, if you can.
     *
     * @return integer The number of items in the collection
     */
    public function count()
    {
        // Find the end of the collection
        list($node, $position) = $this->getCollectionNode(null);
        if (!$node->hasProperty('rdf:first')) {
            return 0;
        } else {
            return $position;
        }
    }

    /** Append an item to the end of the collection
     *
     * @param  mixed $value      The value to append
     *
     * @return integer           The number of values appended (1 or 0)
     */
    public function append($value)
    {
        // Find the end of the collection
        list($node, ) = $this->getCollectionNode(null);
        $rest = $node->get('rdf:rest');

        if ($node === $this and is_null($rest)) {
            $node->set('rdf:first', $value);
            $node->addResource('rdf:rest', 'rdf:nil');
        } else {
            $new = $this->graph->newBnode();
            $node->set('rdf:rest', $new);
            $new->add('rdf:first', $value);
            $new->addResource('rdf:rest', 'rdf:nil');
        }

        return 1;
    }

    /** Array Access: check if a position exists in collection using array syntax
     *
     * Example: isset($list[2])
     */
    public function offsetExists($offset)
    {
        if (is_int($offset) and $offset > 0) {
            list($node, $position) = $this->getCollectionNode($offset);
            return ($node and $position === $offset and $node->hasProperty('rdf:first'));
        } else {
            throw new \InvalidArgumentException(
                "Collection offset must be a positive integer"
            );
        }
    }

    /** Array Access: get an item at a specified position in collection using array syntax
     *
     * Example: $item = $list[2];
     */
    public function offsetGet($offset)
    {
        if (is_int($offset) and $offset > 0) {
            list($node, $position) = $this->getCollectionNode($offset);
            if ($node and $position === $offset) {
                return $node->get('rdf:first');
            }
        } else {
            throw new \InvalidArgumentException(
                "Collection offset must be a positive integer"
            );
        }
    }

    /**
     * Array Access: set an item at a positon in collection using array syntax
     *
     * Example: $list[2] = $item;
     */
    public function offsetSet($offset, $value)
    {
        if (is_null($offset)) {
            // No offset - append to end of collection
            $this->append($value);
        } elseif (is_int($offset) and $offset > 0) {
            list($node, $position) = $this->getCollectionNode($offset);

            // Create nodes, if they are missing
            while ($position < $offset) {
                $new = $this->graph->newBnode();
                $node->set('rdf:rest', $new);
                $new->addResource('rdf:rest', 'rdf:nil');
                $node = $new;
                $position++;
            }

            // Terminate the list
            if (!$node->hasProperty('rdf:rest')) {
                $node->addResource('rdf:rest', 'rdf:nil');
            }

            return $node->set('rdf:first', $value);
        } else {
            throw new \InvalidArgumentException(
                "Collection offset must be a positive integer"
            );
        }
    }

    /**
     * Array Access: delete an item at a specific postion using array syntax
     *
     * Example: unset($seq[2]);
     */
    public function offsetUnset($offset)
    {
        if (is_int($offset) and $offset > 0) {
            list($node, $position) = $this->getCollectionNode($offset);
        } else {
            throw new \InvalidArgumentException(
                "Collection offset must be a positive integer"
            );
        }

        // Does the item exist?
        if ($node and $position === $offset) {
            $nil = $this->graph->resource('rdf:nil');
            if ($position === 1) {
                $rest = $node->get('rdf:rest');
                if ($rest and $rest !== $nil) {
                    // Move second value, so we can keep the head of list
                    $node->set('rdf:first', $rest->get('rdf:first'));
                    $node->set('rdf:rest', $rest->get('rdf:rest'));
                    $rest->delete('rdf:first');
                    $rest->delete('rdf:rest');
                } else {
                    // Just remove the value
                    $node->delete('rdf:first');
                    $node->delete('rdf:rest');
                }
            } else {
                // Remove the value and re-link the list
                $node->delete('rdf:first');
                $rest = $node->get('rdf:rest');
                $previous = $node->get('^rdf:rest');
                if (is_null($rest)) {
                    $rest = $nil;
                }
                if ($previous) {
                    $previous->set('rdf:rest', $rest);
                }
            }
        }
    }
}
