<?php

class IPBBridge extends FeedExpander
{
    const NAME = 'IPB Bridge';
    const URI = 'https://www.invisionpower.com';
    const DESCRIPTION = 'Returns feeds for forums powered by IPB';
    const MAINTAINER = 'logmanoriginal';
    const PARAMETERS = [
        [
            'uri' => [
                'name' => 'URI',
                'type' => 'text',
                'required' => true,
                'title' => 'Insert forum, subforum or topic URI',
                'exampleValue' => 'https://invisioncommunity.com/forums/forum/499-feedback-and-ideas/'
            ],
            'limit' => [
                'name' => 'Limit',
                'type' => 'number',
                'required' => false,
                'title' => 'Specifies the number of items to return on each request (-1: all)',
                'defaultValue' => 10
            ]
        ]
    ];
    const CACHE_TIMEOUT = 3600;

    // Constants for internal use
    const FORUM_TYPE_LIST_FILTER = '.cForumTopicTable';
    const FORUM_TYPE_TABLE_FILTER = '#forum_table';

    const TOPIC_TYPE_ARTICLE = 'article';
    const TOPIC_TYPE_DIV = 'div.post_block';

    public function getURI()
    {
        return $this->getInput('uri') ?: parent::getURI();
    }

    public function collectData()
    {
        // The URI cannot be the mainpage (or anything related)
        switch (parse_url($this->getInput('uri'), PHP_URL_PATH)) {
            case null:
            case '/index.php':
                returnClientError('Provided URI is invalid!');
                break;
            default:
                break;
        }

        // Sanitize the URI (because else it won't work)
        $uri = rtrim($this->getInput('uri'), '/'); // No trailing slashes!

        // Forums might provide feeds, though that's optional *facepalm*
        // Let's check if there is a valid feed available
        $headers = get_headers($uri . '.xml');

        if ($headers[0] === 'HTTP/1.1 200 OK') { // Heureka! It's a valid feed!
            return $this->collectExpandableDatas($uri . '.xml');
        }

        // No valid feed, so do it the hard way
        $html = getSimpleHTMLDOM($uri);

        $limit = $this->getInput('limit');

        // Determine if this is a topic or a forum
        switch (true) {
            case $this->isTopic($html):
                $this->collectTopic($html, $limit);
                break;
            case $this->isForum($html):
                $this->collectForum($html);
                break;
            default:
                returnClientError('Unknown type!');
                break;
        }
    }

    private function isForum($html)
    {
        return !is_null($html->find('div[data-controller*=forums.front.forum.forumPage]', 0))
        || !is_null($html->find(static::FORUM_TYPE_TABLE_FILTER, 0));
    }

    private function isTopic($html)
    {
        return !is_null($html->find('div[data-controller*=core.front.core.commentFeed]', 0))
        || !is_null($html->find(static::TOPIC_TYPE_DIV, 0));
    }

    private function collectForum($html)
    {
        // There are multiple forum designs in use (depends on version?)
        // 1 - Uses an ordered list (based on https://invisioncommunity.com/forums)
        // 2 - Uses a table (based on https://onehallyu.com)

        switch (true) {
            case !is_null($html->find(static::FORUM_TYPE_LIST_FILTER, 0)):
                $this->collectForumList($html);
                break;
            case !is_null($html->find(static::FORUM_TYPE_TABLE_FILTER, 0)):
                $this->collectForumTable($html);
                break;
            default:
                returnClientError('Unknown forum format!');
                break;
        }
    }

    private function collectForumList($html)
    {
        foreach ($html->find(static::FORUM_TYPE_LIST_FILTER, 0)->children() as $row) {
            // Columns: Title, Statistics, Last modified
            $item = [];

            $item['uri'] = $row->find('a', 0)->href;
            $item['title'] = $row->find('a', 0)->title;
            $item['author'] = $row->find('a', 1)->innertext;
            $item['timestamp'] = strtotime($row->find('time', 0)->getAttribute('datetime'));

            $this->items[] = $item;
        }
    }

    private function collectForumTable($html)
    {
        foreach ($html->find(static::FORUM_TYPE_TABLE_FILTER, 0)->children() as $row) {
            // Columns: Icon, Content, Preview, Statistics, Last modified
            $item = [];

            // Skip header row
            if (!is_null($row->find('th', 0))) {
                continue;
            }

            $item['uri'] = $row->find('a', 0)->href;
            $item['title'] = $row->find('.title', 0)->plaintext;
            $item['timestamp'] = strtotime($row->find('[itemprop=dateCreated]', 0)->plaintext);

            $this->items[] = $item;
        }
    }

    private function collectTopic($html, $limit)
    {
        // There are multiple topic designs in use (depends on version?)
        // 1 - Uses articles (based on https://invisioncommunity.com/forums)
        // 2 - Uses divs (based on https://onehallyu.com)

        switch (true) {
            case !is_null($html->find(static::TOPIC_TYPE_ARTICLE, 0)):
                $this->collectTopicHistory($html, $limit, 'collectTopicArticle');
                break;
            case !is_null($html->find(static::TOPIC_TYPE_DIV, 0)):
                $this->collectTopicHistory($html, $limit, 'collectTopicDiv');
                break;
            default:
                returnClientError('Unknown topic format!');
                break;
        }
    }

    private function collectTopicHistory($html, $limit, $callback)
    {
        // Make sure the callback is valid!
        if (!method_exists($this, $callback)) {
            returnServerError('Unknown function (\'' . $callback . '\')!');
        }

        $next = null; // Holds the URI of the next page

        while (true) {
            $next = $this->$callback($html, is_null($next));

            if (is_null($next) || ($limit > 0 && count($this->items) >= $limit)) {
                break;
            }

            $html = getSimpleHTMLDOMCached($next);
        }

        // We might have more items than specified, remove excess
        $this->items = array_slice($this->items, 0, $limit);
    }

    private function collectTopicArticle($html, $firstrun = true)
    {
        $title = $html->find('h1.ipsType_pageTitle', 0)->plaintext;

        // Are we on last page?
        if ($firstrun && !is_null($html->find('.ipsPagination', 0))) {
            $last = $html->find('.ipsPagination_last a', 0)->{'data-page'};
            $active = $html->find('.ipsPagination_active a', 0)->{'data-page'};

            if ($active !== $last) {
                // Load last page into memory (cached)
                $html = getSimpleHTMLDOMCached($html->find('.ipsPagination_last a', 0)->href);
            }
        }

        foreach (array_reverse($html->find(static::TOPIC_TYPE_ARTICLE)) as $article) {
            $item = [];

            $item['uri'] = $article->find('time', 0)->parent()->href;
            $item['author'] = $article->find('aside a', 0)->plaintext;
            $item['title'] = $item['author'] . ' - ' . $title;
            $item['timestamp'] = strtotime($article->find('time', 0)->getAttribute('datetime'));

            $content = $article->find('[data-role=commentContent]', 0);
            $content = $this->scaleImages($content);
            $item['content'] = $this->fixContent($content);
            $item['enclosures'] = $this->findImages($article->find('[data-role=commentContent]', 0)) ?: null;

            $this->items[] = $item;
        }

        // Return whatever page comes next (previous, as we add in inverse order)
        // Do we have a previous page? (inactive means no)
        if (!is_null($html->find('li[class=ipsPagination_prev ipsPagination_inactive]', 0))) {
            return null; // No, or no more
        } elseif (!is_null($html->find('li[class=ipsPagination_prev]', 0))) {
            return $html->find('.ipsPagination_prev a', 0)->href;
        }

        return null;
    }

    private function collectTopicDiv($html, $firstrun = true)
    {
        $title = $html->find('h1.ipsType_pagetitle', 0)->plaintext;

        // Are we on last page?
        if ($firstrun && !is_null($html->find('.pagination', 0))) {
            $active = $html->find('li[class=page active]', 0)->plaintext;

            // There are two ways the 'last' page is displayed:
            // - With a distict 'last' button (only if there are enough pages)
            // - With a button for each page (use last button)
            if (!is_null($html->find('li.last', 0))) {
                $last = $html->find('li.last a', 0);
            } else {
                $last = $html->find('li[class=page] a', -1);
            }

            if ($active !== $last->plaintext) {
                // Load last page into memory (cached)
                $html = getSimpleHTMLDOMCached($last->href);
            }
        }

        foreach (array_reverse($html->find(static::TOPIC_TYPE_DIV)) as $article) {
            $item = [];

            $item['uri'] = $article->find('a[rel=bookmark]', 0)->href;
            $item['author'] = $article->find('.author', 0)->plaintext;
            $item['title'] = $item['author'] . ' - ' . $title;
            $item['timestamp'] = strtotime($article->find('.published', 0)->getAttribute('title'));

            $content = $article->find('[itemprop=commentText]', 0);
            $content = $this->scaleImages($content);
            $item['content'] = $this->fixContent($content);

            $item['enclosures'] = $this->findImages($article->find('.post_body', 0)) ?: null;

            $this->items[] = $item;
        }

        // Return whatever page comes next (previous, as we add in inverse order)
        // Do we have a previous page?
        if (!is_null($html->find('li.prev', 0))) {
            return $html->find('li.prev a', 0)->href;
        }

        return null;
    }

    /** Returns all images from the provide HTML DOM */
    private function findImages($html)
    {
        $images = [];

        foreach ($html->find('img') as $img) {
            $images[] = $img->src;
        }

        return $images;
    }

    /** Sets the maximum width and height for all images */
    private function scaleImages($html, $width = 400, $height = 400)
    {
        foreach ($html->find('img') as $img) {
            $img->style = "max-width: {$width}px; max-height: {$height}px;";
        }

        return $html;
    }

    /** Removes all unnecessary tags and adds formatting */
    private function fixContent($html)
    {
        // Restore quote highlighting
        foreach ($html->find('blockquote') as $quote) {
            $quote->style = <<<EOD
padding: 0px 15px;
border-width: 1px 1px 1px 2px;
border-style: solid;
border-color: #ededed #e8e8e8 #dbdbdb #666666;
background: #fbfbfb;
EOD;
        }

        // Remove unnecessary tags
        $content = strip_tags(
            $html->innertext,
            '<p><a><img><ol><ul><li><table><tr><th><td><strong><blockquote><br><hr><h>'
        );

        return $content;
    }
}
