<?php
/**
 * Copyright 2012-2014 Rackspace US, Inc.
 *
 * 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.
 */

namespace OpenCloud\Common;

use OpenCloud\Common\Log\Logger;

/**
 * @deprecated
 * @codeCoverageIgnore
 */
class Collection extends Base
{
    private $service;
    private $itemClass;
    private $itemList = array();
    private $pointer = 0;
    private $sortKey;
    private $nextPageClass;
    private $nextPageCallback;
    private $nextPageUrl;

    /**
     * A Collection is an array of objects
     *
     * Some assumptions:
     * * The `Collection` class assumes that there exists on its service
     *   a factory method with the same name of the class. For example, if
     *   you create a Collection of class `Foobar`, it will attempt to call
     *   the method `parent::Foobar()` to create instances of that class.
     * * It assumes that the factory method can take an array of values, and
     *   it passes that to the method.
     *
     * @param Service $service   - the service associated with the collection
     * @param string  $itemclass - the Class of each item in the collection
     *                           (assumed to be the name of the factory method)
     * @param array   $arr       - the input array
     */
    public function __construct($service, $class, array $array = array())
    {
        $service->getLogger()->warning(Logger::deprecated(__METHOD__, 'OpenCloud\Common\Collection\CollectionBuilder'));

        $this->setService($service);

        $this->setNextPageClass($class);

        // If they've supplied a FQCN, only get the last part
        $class = (false !== ($classNamePos = strrpos($class, '\\')))
            ? substr($class, $classNamePos + 1)
            : $class;

        $this->setItemClass($class);

        // Set data
        $this->setItemList($array);
    }

    /**
     * Set the entire data array.
     *
     * @param array $array
     */
    private function setItemList(array $array)
    {
        $this->itemList = $array;

        return $this;
    }

    /**
     * Retrieve the entire data array.
     *
     * @return array
     */
    public function getItemList()
    {
        return $this->itemList;
    }

    /**
     * Set the service.
     *
     * @param Service|PersistentObject $service
     */
    public function setService($service)
    {
        $this->service = $service;

        return $this;
    }

    /**
     * Retrieves the service associated with the Collection
     *
     * @return Service
     */
    public function getService()
    {
        return $this->service;
    }

    /**
     * Set the resource class name.
     */
    private function setItemClass($itemClass)
    {
        $this->itemClass = $itemClass;

        return $this;
    }

    /**
     * Get item class.
     */
    private function getItemClass()
    {
        return $this->itemClass;
    }

    /**
     * Set the key that will be used for sorting.
     */
    private function setSortKey($sortKey)
    {
        $this->sortKey = $sortKey;

        return $this;
    }

    /**
     * Get the key that will be used for sorting.
     */
    private function getSortKey()
    {
        return $this->sortKey;
    }

    /**
     * Set next page class.
     */
    private function setNextPageClass($nextPageClass)
    {
        $this->nextPageClass = $nextPageClass;

        return $this;
    }

    /**
     * Get next page class.
     */
    private function getNextPageClass()
    {
        return $this->nextPageClass;
    }

    /**
     * for paginated collection, sets the callback function and URL for
     * the next page
     *
     * The callback function should have the signature:
     *
     *      function Whatever($class, $url, $parent)
     *
     * and the `$url` should be the URL of the next page of results
     *
     * @param callable $callback the name of the function (or array of
     *                           object, function name)
     * @param string   $url      the URL of the next page of results
     * @return void
     */
    public function setNextPageCallback($callback, $url)
    {
        $this->nextPageCallback = $callback;
        $this->nextPageUrl = $url;

        return $this;
    }

    /**
     * Get next page callback.
     */
    private function getNextPageCallback()
    {
        return $this->nextPageCallback;
    }

    /**
     * Get next page URL.
     */
    private function getNextPageUrl()
    {
        return $this->nextPageUrl;
    }

    /**
     * Returns the number of items in the collection
     *
     * For most services, this is the total number of items. If the Collection
     * is paginated, however, this only returns the count of items in the
     * current page of data.
     *
     * @return int
     */
    public function count()
    {
        return count($this->getItemList());
    }

    /**
     * Pseudonym for count()
     *
     * @codeCoverageIgnore
     */
    public function size()
    {
        return $this->count();
    }

    /**
     * Resets the pointer to the beginning, but does NOT return the first item
     *
     * @api
     * @return void
     */
    public function reset()
    {
        $this->pointer = 0;
    }

    /**
     * Resets the collection pointer back to the first item in the page
     * and returns it
     *
     * This is useful if you're only interested in the first item in the page.
     *
     * @api
     * @return Base the first item in the set
     */
    public function first()
    {
        $this->reset();

        return $this->next();
    }

    /**
     * Return the item at a particular point of the array.
     *
     * @param  mixed $offset
     * @return mixed
     */
    public function getItem($pointer)
    {
        return (isset($this->itemList[$pointer])) ? $this->itemList[$pointer] : false;
    }

    /**
     * Add an item to this collection
     *
     * @param mixed $item
     */
    public function addItem($item)
    {
        $this->itemList[] = $item;
    }

    /**
     * Returns the next item in the page
     *
     * @api
     * @return Base the next item or FALSE if at the end of the page
     */
    public function next()
    {
        if ($this->pointer >= $this->count()) {
            return false;
        }

        $data = $this->getItem($this->pointer++);
        $class = $this->getItemClass();

        // Are there specific methods in the parent/service that can be used to
        // instantiate the resource? Currently supported: getResource(), resource()
        foreach (array($class, 'get' . ucfirst($class)) as $method) {
            if (method_exists($this->service, $method)) {
                return call_user_func(array($this->service, $method), $data);
            }
        }

        // Backup method
        if (method_exists($this->service, 'resource')) {
            return $this->service->resource($class, $data);
        }

        return false;
    }

    /**
     * sorts the collection on a specified key
     *
     * Note: only top-level keys can be used as the sort key. Note that this
     * only sorts the data in the current page of the Collection (for
     * multi-page data).
     *
     * @api
     * @param string $keyname the name of the field to use as the sort key
     * @return void
     */
    public function sort($keyname = 'id')
    {
        $this->setSortKey($keyname);
        usort($this->itemList, array($this, 'sortCompare'));
    }

    /**
     * selects only specified items from the Collection
     *
     * This provides a simple form of filtering on Collections. For each item
     * in the collection, it calls the callback function, passing it the item.
     * If the callback returns `TRUE`, then the item is retained; if it returns
     * `FALSE`, then the item is deleted from the collection.
     *
     * Note that this should not supersede server-side filtering; the
     * `Collection::Select()` method requires that *all* of the data for the
     * Collection be retrieved from the server before the filtering is
     * performed; this can be very inefficient, especially for large data
     * sets. This method is mostly useful on smaller-sized sets.
     *
     * Example:
     * <code>
     * $services = $connection->ServiceList();
     * $services->Select(function ($item) { return $item->region=='ORD';});
     * // now the $services Collection only has items from the ORD region
     * </code>
     *
     * `Select()` is *destructive*; that is, it actually removes entries from
     * the collection. For example, if you use `Select()` to find items with
     * the ID > 10, then use it again to find items that are <= 10, it will
     * return an empty list.
     *
     * @api
     * @param callable $testfunc a callback function that is passed each item
     *                           in turn. Note that `Select()` performs an explicit test for
     *                           `FALSE`, so functions like `strpos()` need to be cast into a
     *                           boolean value (and not just return the integer).
     * @returns void
     * @throws DomainError if callback doesn't return a boolean value
     */
    public function select($testfunc)
    {
        foreach ($this->getItemList() as $index => $item) {
            $test = call_user_func($testfunc, $item);
            if (!is_bool($test)) {
                throw new Exceptions\DomainError(
                    Lang::translate('Callback function for Collection::Select() did not return boolean')
                );
            }
            if ($test === false) {
                unset($this->itemList[$index]);
            }
        }
    }

    /**
     * returns the Collection object for the next page of results, or
     * FALSE if there are no more pages
     *
     * Generally, the structure for a multi-page collection will look like
     * this:
     *
     *      $coll = $obj->Collection();
     *      do {
     *          while ($item = $coll->Next()) {
     *              // do something with the item
     *          }
     *      } while ($coll = $coll->NextPage());
     *
     * @api
     * @return Collection if there are more pages of results, otherwise FALSE
     */
    public function nextPage()
    {
        return ($this->getNextPageUrl() !== null)
            ? call_user_func($this->getNextPageCallback(), $this->getNextPageClass(), $this->getNextPageUrl())
            : false;
    }

    /**
     * Compares two values of sort keys
     */
    private function sortCompare($a, $b)
    {
        $key = $this->getSortKey();

        // Handle strings
        if (is_string($a->$key)) {
            return strcmp($a->$key, $b->$key);
        }

        // Handle others with logical comparisons
        if ($a->$key == $b->$key) {
            return 0;
        } elseif ($a->$key < $b->$key) {
            return -1;
        } else {
            return 1;
        }
    }
}
