<?php

/**
 * Joomla! Content Management System
 *
 * @copyright  (C) 2012 Open Source Matters, Inc. <https://www.joomla.org>
 * @license    GNU General Public License version 2 or later; see LICENSE.txt
 */

namespace Joomla\CMS\Feed;

use Joomla\CMS\Date\Date;

// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects

/**
 * Class to encapsulate a feed for the Joomla Platform.
 *
 * @property  FeedPerson     $author         Person responsible for feed content.
 * @property  array          $categories     Categories to which the feed belongs.
 * @property  array          $contributors   People who contributed to the feed content.
 * @property  string         $copyright      Information about rights, e.g. copyrights, held in and over the feed.
 * @property  string         $description    A phrase or sentence describing the feed.
 * @property  string         $generator      A string indicating the program used to generate the feed.
 * @property  FeedLink|null  $image          FeedLink object containing feed image properties.
 * @property  Date           $publishedDate  The publication date for the feed content.
 * @property  string         $title          A human readable title for the feed.
 * @property  Date           $updatedDate    The last time the content of the feed changed.
 * @property  string         $uri            Universal, permanent identifier for the feed.
 *
 * @since  3.1.4
 */
class Feed implements \ArrayAccess, \Countable
{
    /**
     * @var    array  The entry properties.
     * @since  3.1.4
     */
    protected $properties = [
        'uri'          => '',
        'title'        => '',
        'updatedDate'  => '',
        'description'  => '',
        'categories'   => [],
        'contributors' => [],
    ];

    /**
     * @var    array  The list of feed entry objects.
     * @since  3.1.4
     */
    protected $entries = [];

    /**
     * Magic method to return values for feed properties.
     *
     * @param   string  $name  The name of the property.
     *
     * @return  mixed
     *
     * @since   3.1.4
     */
    public function __get($name)
    {
        return $this->properties[$name] ?? null;
    }

    /**
     * Magic method to set values for feed properties.
     *
     * @param   string  $name   The name of the property.
     * @param   mixed   $value  The value to set for the property.
     *
     * @return  void
     *
     * @since   3.1.4
     */
    public function __set($name, $value)
    {
        // Ensure that setting a date always sets a Date instance.
        if ((($name === 'updatedDate') || ($name === 'publishedDate')) && !($value instanceof Date)) {
            $value = new Date($value);
        }

        // Validate that any authors that are set are instances of JFeedPerson or null.
        if (($name === 'author') && (!($value instanceof FeedPerson) || ($value === null))) {
            throw new \InvalidArgumentException(
                \sprintf(
                    '%1$s "author" must be an instance of Joomla\\CMS\\Feed\\FeedPerson. %2$s given.',
                    \get_class($this),
                    \is_object($value) ? \get_class($value) : \gettype($value)
                )
            );
        }

        // Disallow setting categories or contributors directly.
        if (\in_array($name, ['categories', 'contributors'])) {
            throw new \InvalidArgumentException(
                \sprintf(
                    'Cannot directly set %1$s property "%2$s".',
                    \get_class($this),
                    $name
                )
            );
        }

        $this->properties[$name] = $value;
    }

    /**
     * Method to add a category to the feed object.
     *
     * @param   string  $name  The name of the category to add.
     * @param   string  $uri   The optional URI for the category to add.
     *
     * @return  Feed
     *
     * @since   3.1.4
     */
    public function addCategory($name, $uri = '')
    {
        $this->properties['categories'][$name] = $uri;

        return $this;
    }

    /**
     * Method to add a contributor to the feed object.
     *
     * @param   string  $name   The full name of the person to add.
     * @param   string  $email  The email address of the person to add.
     * @param   string  $uri    The optional URI for the person to add.
     * @param   string  $type   The optional type of person to add.
     *
     * @return  Feed
     *
     * @since   3.1.4
     */
    public function addContributor($name, $email, $uri = null, $type = null)
    {
        $contributor = new FeedPerson($name, $email, $uri, $type);

        // If the new contributor already exists then there is nothing to do, so just return.
        foreach ($this->properties['contributors'] as $c) {
            if ($c == $contributor) {
                return $this;
            }
        }

        // Add the new contributor.
        $this->properties['contributors'][] = $contributor;

        return $this;
    }

    /**
     * Method to add an entry to the feed object.
     *
     * @param   FeedEntry  $entry  The entry object to add.
     *
     * @return  Feed
     *
     * @since   3.1.4
     */
    public function addEntry(FeedEntry $entry)
    {
        // If the new entry already exists then there is nothing to do, so just return.
        foreach ($this->entries as $e) {
            if ($e == $entry) {
                return $this;
            }
        }

        // Add the new entry.
        $this->entries[] = $entry;

        return $this;
    }

    /**
     * Returns a count of the number of entries in the feed.
     *
     * This method is here to implement the Countable interface.
     * You can call it by doing count($feed) rather than $feed->count();
     *
     * @return  integer number of entries in the feed.
     */
    public function count(): int
    {
        return \count($this->entries);
    }

    /**
     * Whether or not an offset exists.  This method is executed when using isset() or empty() on
     * objects implementing ArrayAccess.
     *
     * @param   mixed  $offset  An offset to check for.
     *
     * @return  boolean
     *
     * @see     ArrayAccess::offsetExists()
     * @since   3.1.4
     */
    public function offsetExists($offset): bool
    {
        return isset($this->entries[$offset]);
    }

    /**
     * Returns the value at specified offset.
     *
     * @param   mixed  $offset  The offset to retrieve.
     *
     * @return  mixed  The value at the offset.
     *
     * @see     ArrayAccess::offsetGet()
     * @since   3.1.4
     */
    public function offsetGet($offset): mixed
    {
        return $this->entries[$offset];
    }

    /**
     * Assigns a value to the specified offset.
     *
     * @param   mixed      $offset  The offset to assign the value to.
     * @param   FeedEntry  $value   The JFeedEntry to set.
     *
     * @return  void
     *
     * @see     ArrayAccess::offsetSet()
     * @since   3.1.4
     * @throws  \InvalidArgumentException
     */
    public function offsetSet($offset, $value): void
    {
        if (!($value instanceof FeedEntry)) {
            throw new \InvalidArgumentException(
                \sprintf(
                    '%1$s entries must be an instance of Joomla\\CMS\\Feed\\FeedPerson. %2$s given.',
                    \get_class($this),
                    \is_object($value) ? \get_class($value) : \gettype($value)
                )
            );
        }

        $this->entries[$offset] = $value;
    }

    /**
     * Unsets an offset.
     *
     * @param   mixed  $offset  The offset to unset.
     *
     * @return  void
     *
     * @see     ArrayAccess::offsetUnset()
     * @since   3.1.4
     */
    public function offsetUnset($offset): void
    {
        unset($this->entries[$offset]);
    }

    /**
     * Method to remove a category from the feed object.
     *
     * @param   string  $name  The name of the category to remove.
     *
     * @return  Feed
     *
     * @since   3.1.4
     */
    public function removeCategory($name)
    {
        unset($this->properties['categories'][$name]);

        return $this;
    }

    /**
     * Method to remove a contributor from the feed object.
     *
     * @param   FeedPerson  $contributor  The person object to remove.
     *
     * @return  Feed
     *
     * @since   3.1.4
     */
    public function removeContributor(FeedPerson $contributor)
    {
        // If the contributor exists remove it.
        foreach ($this->properties['contributors'] as $k => $c) {
            if ($c == $contributor) {
                unset($this->properties['contributors'][$k]);
                $this->properties['contributors'] = array_values($this->properties['contributors']);

                return $this;
            }
        }

        return $this;
    }

    /**
     * Method to remove an entry from the feed object.
     *
     * @param   FeedEntry  $entry  The entry object to remove.
     *
     * @return  Feed
     *
     * @since   3.1.4
     */
    public function removeEntry(FeedEntry $entry)
    {
        // If the entry exists remove it.
        foreach ($this->entries as $k => $e) {
            if ($e == $entry) {
                unset($this->entries[$k]);
                $this->entries = array_values($this->entries);

                return $this;
            }
        }

        return $this;
    }

    /**
     * Shortcut method to set the author for the feed object.
     *
     * @param   string  $name   The full name of the person to set.
     * @param   string  $email  The email address of the person to set.
     * @param   string  $uri    The optional URI for the person to set.
     * @param   string  $type   The optional type of person to set.
     *
     * @return  Feed
     *
     * @since   3.1.4
     */
    public function setAuthor($name, $email, $uri = null, $type = null)
    {
        $author = new FeedPerson($name, $email, $uri, $type);

        $this->properties['author'] = $author;

        return $this;
    }

    /**
     * Method to reverse the items if display is set to 'oldest first'
     *
     * @return  Feed
     *
     * @since   3.1.4
     */
    public function reverseItems()
    {
        if (\is_array($this->entries) && !empty($this->entries)) {
            $this->entries = array_reverse($this->entries);
        }

        return $this;
    }
}
