<?php
namespace FeedWriter;

use \Iterator;
use \Exception;
use \stdClass;
use \ArrayIterator;

class Feed {

	const RSS1 = 'RSS 1.0';
	const RSS2 = 'RSS 2.0';
	const ATOM = 'ATOM';

  protected $_iter  = null;
  protected $_map   = [];
  protected $_views = [];

  /**
   * Generate one or more views of a collection from a given iterator or array.
   *
   * Takes a data source, an array describing a mapping from instances of the
   * collection to items in the view, and a list of views to generate.
   *
   * This all sounds more abstract than it actually is, so let's run through
   * some examples:
   *
   * <code>
   *
   * use \FeedWriter\Feed;
   * use \FeedWriter\View\Atom;
   *
   * // Here's an array of arrays describing some blog posts.  These could also
   * // be objects with properties like $blog_posts->writer.
   *
   * $blog_posts = [
   *   [ 'date_created' => '2013-02-14', 'writer' => 'Brennen Bearnes', 'text' => 'Hate.' ],
   *   [ 'date_created' => '2013-07-04', 'writer' => 'Brennen Bearnes', 'text' => 'Explosions.' ],
   *   [ 'date_created' => '2013-08-31', 'writer' => 'Brennen Bearnes', 'text' => 'A feed thingy.' ],
   * ];
   *
   * // Here's a map that explains how to find the properties we need for our
   * // view:
   * $entry_from_post = [
   *   'date'        => 'date_created',
   *   'author'      => 'writer',
   *   'content'     => 'text'
   * ];
   *
   * $feed = new Feed($blog_posts, $entry_from_post, ['atom' => new Atom]);
   *
   * // If everything went ok, views are now available as properties of the feed.
   * if (! $feed->error())
   *   echo $feed->atom->render();
   * else
   *   echo 'error: ' . $feed->error();
   *
   * </code>
   *
   * @param $source mixed iterator or array
   * @param $map array describing mapping of fields to fields
   * @param $views array one or more views to generate
   */
  public function __construct ($source, array $map, array $views)
  {
    if (is_array($source)) 
      $iter = new ArrayIterator($source);
    else
      $iter = $source;

    $this->setIter($iter);
    $this->setViewsAndMap($views, $map);
    $this->spin();
  }

  /**
   * Explode if anybody tries to set a property directly.
   */
  public function __set ($name, $view)
  {
    throw new Exception("You can't directly set properties on Feed. See documentation for constructor.");
  }

  /**
   * Get a view.
   */
  public function __get ($name)
  {
    return $this->_views[$name];
  }

  /**
   * Is a given view set?
   */
  public function __isset ($name)
  {
    return isset($this->_views[$name]);
  }

  /**
   * Stash views and map, making sure that the map provides for anything
   * the view explicitly requires.
   */
  protected function setViewsAndMap (array $views, array $map)
  {
    if (! count($views))
      throw new Exception('Feed requires that at least one view instance be passed in)');

    if (! is_array($map))
      throw new Exception('$map must be an array');

    if (! count($map))
      throw new Exception('$map must provide values');

    // Validate map for each view:
    foreach ($views as $name => $view) {
      if (! isset($view->require))
        continue;

      foreach ($view->require as $field) {
        if (! isset($map[ $field ])) {
          throw new Exception('$map should provide a mapping for ' . $field);
        }
      }
    }

    $this->_views = $views;
    $this->_map   = $map;
  }

  protected function setIter (Iterator $iter)
  {
    if (! ($iter instanceof Iterator))
      throw new Exception('$iter must be an iterator');
    $this->_iter = $iter;
  }

  /**
   * Do the business of handing off values to views.
   */
  protected function spin ()
  {
    $output_item = (object)[];
    while ($this->_iter->valid()) {
      $input_item = $this->_iter->current();

      if (is_array($input_item))
        $input_item = (object)$input_item;

      foreach ($this->_map as $output_key => &$input_key) {
        if (is_string($input_key))
        {
          // shorthand for "use this field on the object for the value"
          if (isset($input_item->$input_key))
            $output_item->$output_key = $input_item->$input_key;
          else
            throw new Exception("unable to access $input_key on input item: " . print_r($input_item, 1));
        }
        elseif (is_callable($input_key))
        {
          // if we have a function, pass it the item and expect it to
          // return a value for our output key
          $output_item->$output_key = $input_key($input_item);
        }
      }

      foreach ($this->_views as $name => $view) {
        $view->collect($output_item);
      }

      $this->_iter->next();
    }
  }

}