<?php

namespace api\utils\feedGenerator;

require_once "FeedItem.php";

/**
 * Universal Feed Writer class
 *
 * Generate RSS 1.0, RSS 2.0, and Atom Feed
 *
 * @package     UniversalFeedWriter
 * @link        http://www.ajaxray.com/projects/rss
 */
class FeedWriter
{
    private array $channels = []; // Collection of channel elements
    private array $items = []; // Collection of items as objects of FeedItem class
    private array $data = []; // Store some other version-wise data
    private array $CDATAEncoding = []; // The tag names that need to be encoded as CDATA

    private string $version;

    /**
     * Constructor
     *
     * @param string $version The version (RSS1, RSS2, ATOM).
     */
    public function __construct(string $version = RSS2)
    {
        $this->version = $version;

        // Setting default values for essential channel elements
        $this->channels['title'] = $version . ' Feed';
        $this->channels['link'] = 'http://www.ajaxray.com/blog';
        $this->channels["feedUrl"] = "http://example.com/feed";

        // Tag names to encode in CDATA
        $this->CDATAEncoding = ['description', 'content:encoded', 'summary'];
    }

    // Public functions

    /**
     * Set a channel element
     *
     * @param string $elementName Name of the channel tag
     * @param string $content Content of the channel tag
     */
    public function setChannelElement(string $elementName, string|array $content): void
    {
        $this->channels[$elementName] = $content;
    }

    /**
     * Generate the actual RSS/Atom file
     */
    public function generateFeed(): void
    {
        $this->printHead();
        $this->printChannels();
        $this->printItems();
        $this->printTail();
    }

    /**
     * Create a new FeedItem.
     *
     * @return FeedItem An instance of FeedItem class
     */
    public function createNewItem(): FeedItem
    {
        $item = new FeedItem($this->version);
        return $item;
    }

    /**
     * Add a FeedItem to the main class
     *
     * @param FeedItem $feedItem An instance of FeedItem class
     */
    public function addItem(FeedItem $feedItem): void
    {
        $this->items[] = $feedItem;
    }

    // Wrapper functions

    /**
     * Set the 'title' channel element
     *
     * @param string $title Value of 'title' channel tag
     */
    public function setTitle(string $title): void
    {
        $this->setChannelElement('title', $title);
    }

    /**
     * Set the 'description' channel element
     *
     * @param string $description Value of 'description' channel tag
     */
    public function setDescription(string $description): void
    {
        $this->setChannelElement('description', $description);
    }

    /**
     * Set the 'link' channel element
     *
     * @param string $link Value of 'link' channel tag
     */
    public function setLink(string $link): void
    {
        $this->setChannelElement('link', $link);
    }

    /**
     * Set the 'image' channel element
     *
     * @param string $title Title of the image
     * @param string $link Link URL of the image
     * @param string $url Path URL of the image
     */
    public function setImage(string $title, string $link, string $url): void
    {
        $this->setChannelElement('image', ['title' => $title, 'link' => $link, 'url' => $url]);
    }

    /**
     * Set the 'about' channel element. Only for RSS 1.0
     *
     * @param string $url Value of 'about' channel tag
     */
    public function setChannelAbout(string $url): void
    {
        $this->data['ChannelAbout'] = $url;
    }

    // Other functions

    /**
     * Generates a UUID
     *
     * @param string $key An optional prefix
     * @param string $prefix A prefix
     * @return string The formatted UUID
     */
    public static function uuid(?string $key = null, string $prefix = ''): string
    {
        $key = $key ?? uniqid((string)rand());
        $chars = md5($key);
        $uuid = substr($chars, 0, 8) . '-';
        $uuid .= substr($chars, 8, 4) . '-';
        $uuid .= substr($chars, 12, 4) . '-';
        $uuid .= substr($chars, 16, 4) . '-';
        $uuid .= substr($chars, 20, 12);

        return $prefix . $uuid;
    }

    // Private functions

    /**
     * Prints the XML and RSS namespace
     */
    private function printHead(): void
    {
        $out = '<?xml version="1.0" encoding="utf-8"?>' . "\n";

        if ($this->version == RSS2)
        {
            $out .= '<rss version="2.0"
                    xmlns:content="http://purl.org/rss/1.0/modules/content/"
                    xmlns:wfw="http://wellformedweb.org/CommentAPI/"
                  >' . PHP_EOL;
        }
        elseif ($this->version == RSS1)
        {
            $out .= '<rdf:RDF
                     xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
                     xmlns="http://purl.org/rss/1.0/"
                     xmlns:dc="http://purl.org/dc/elements/1.1/"
                    >' . PHP_EOL;
        }
        elseif ($this->version == ATOM)
        {
            $out .= '<feed xmlns="http://www.w3.org/2005/Atom">' . PHP_EOL;
        }
        echo $out;
    }

    /**
     * Closes the open tags at the end of the file
     */
    private function printTail(): void
    {
        if ($this->version == RSS2)
        {
            echo '</channel>' . PHP_EOL . '</rss>';
        }
        elseif ($this->version == RSS1)
        {
            echo '</rdf:RDF>';
        }
        elseif ($this->version == ATOM)
        {
            echo '</feed>';
        }
    }

    /**
     * Creates a single node in XML format
     *
     * @param string $tagName Name of the tag
     * @param mixed $tagContent Tag value as a string or an array of nested tags in 'tagName' => 'tagValue' format
     * @param array|null $attributes Attributes (if any) in 'attrName' => 'attrValue' format
     * @return string Formatted XML tag
     */
    private function makeNode(string $tagName, $tagContent, ?array $attributes = null): string
    {
        $nodeText = '';
        $attrText = '';

        if (is_array($attributes))
        {
            foreach ($attributes as $key => $value)
            {
                $attrText .= " $key=\"$value\"";
            }
        }

        if (is_array($tagContent) && $this->version == RSS1)
        {
            $attrText = ' rdf:parseType="Resource"';
        }

        $attrText .= (in_array($tagName, $this->CDATAEncoding) && $this->version == ATOM) ? ' type="html" ' : '';
        $nodeText .= (in_array($tagName, $this->CDATAEncoding)) ? "<{$tagName}{$attrText}><![CDATA[" : "<{$tagName}{$attrText}>";

        if (is_array($tagContent))
        {
            foreach ($tagContent as $key => $value)
            {
                $nodeText .= $this->makeNode($key, $value);
            }
        }
        else
        {
            $nodeText .= (in_array($tagName, $this->CDATAEncoding)) ? $tagContent : htmlentities($tagContent);
        }

        $nodeText .= (in_array($tagName, $this->CDATAEncoding)) ? "]]></$tagName>" : "</$tagName>";

        return $nodeText . PHP_EOL;
    }

    /**
     * Prints channel elements
     */
    private function printChannels(): void
    {
        // Start channel tag
        switch ($this->version)
        {
            case RSS2:
                echo '<channel>' . PHP_EOL;
                break;
            case RSS1:
                echo (isset($this->data['ChannelAbout'])) ? "<channel rdf:about=\"{$this->data['ChannelAbout']}\">" : "<channel rdf:about=\"{$this->channels['link']}\">";
                break;
        }

        // Print items of channel
        foreach ($this->channels as $key => $value)
        {
            if ($this->version == ATOM && $key == 'link')
            {
                // ATOM prints the link element as an href attribute
                echo $this->makeNode($key, '', ['href' => $value]);
                // Add the id for ATOM
                echo $this->makeNode('id', $this->uuid($value, 'urn:uuid:'));
            }
            else if ($this->version == ATOM && $key == 'feedUrl')
            {
                echo $this->makeNode('link', '', ['rel' => 'self', 'href' => $value]);
            }
            else
            {
                echo $this->makeNode($key, $value);
            }
        }

        // RSS 1.0 has a special tag <rdf:Seq> with channel
        if ($this->version == RSS1)
        {
            echo "<items>" . PHP_EOL . "<rdf:Seq>" . PHP_EOL;
            foreach ($this->items as $item)
            {
                $thisItems = $item->getElements();
                echo "<rdf:li resource=\"{$thisItems['link']['content']}\"/>" . PHP_EOL;
            }
            echo "</rdf:Seq>" . PHP_EOL . "</items>" . PHP_EOL . "</channel>" . PHP_EOL;
        }
    }

    /**
     * Prints formatted feed items
     */
    private function printItems(): void
    {
        foreach ($this->items as $item)
        {
            $thisItems = $item->getElements();

            // The argument is printed as rdf:about attribute of item in RSS 1.0
            echo $this->startItem($thisItems['link']['content']);

            foreach ($thisItems as $feedItem)
            {
                echo $this->makeNode($feedItem['name'], $feedItem['content'], $feedItem['attributes']);
            }
            echo $this->endItem();
        }
    }

    /**
     * Makes the starting tag of items
     *
     * @param string|false $about The value of about tag, which is used only for RSS 1.0
     */
    private function startItem($about = false): void
    {
        if ($this->version == RSS2)
        {
            echo '<item>' . PHP_EOL;
        }
        elseif ($this->version == RSS1)
        {
            if ($about)
            {
                echo "<item rdf:about=\"$about\">" . PHP_EOL;
            }
            else
            {
                die("link element is not set.\n It's required for RSS 1.0 to be used as the about attribute of the item");
            }
        }
        elseif ($this->version == ATOM)
        {
            echo "<entry>" . PHP_EOL;
        }
    }

    /**
     * Closes the feed item tag
     */
    private function endItem(): void
    {
        if ($this->version == RSS2 || $this->version == RSS1)
        {
            echo '</item>' . PHP_EOL;
        }
        elseif ($this->version == ATOM)
        {
            echo "</entry>" . PHP_EOL;
        }
    }

    /**
     * Set the Feed URL
     * @param string $string - The URL of the feed
     * @return void
     */
    public function setFeedURL(string $string): void
    {
        $this->setChannelElement("feedUrl", $string);
    }
}


// Define constants for RSS 1.0, RSS 2.0, and Atom
const RSS1 = 'RSS 1.0';
const RSS2 = 'RSS 2.0';
const ATOM = 'ATOM';