<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.

/**
 * Renderer factory.
 *
 * @package    mod_forum
 * @copyright  2019 Ryan Wyllie <ryan@moodle.com>
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */

namespace mod_forum\local\factories;

defined('MOODLE_INTERNAL') || die();

use mod_forum\grades\forum_gradeitem;
use mod_forum\local\entities\discussion as discussion_entity;
use mod_forum\local\entities\forum as forum_entity;
use mod_forum\local\factories\vault as vault_factory;
use mod_forum\local\factories\legacy_data_mapper as legacy_data_mapper_factory;
use mod_forum\local\factories\entity as entity_factory;
use mod_forum\local\factories\exporter as exporter_factory;
use mod_forum\local\factories\manager as manager_factory;
use mod_forum\local\factories\builder as builder_factory;
use mod_forum\local\factories\url as url_factory;
use mod_forum\local\renderers\discussion as discussion_renderer;
use mod_forum\local\renderers\discussion_list as discussion_list_renderer;
use mod_forum\local\renderers\posts as posts_renderer;
use moodle_page;
use core\output\notification;

/**
 * Renderer factory.
 *
 * See:
 * https://designpatternsphp.readthedocs.io/en/latest/Creational/SimpleFactory/README.html
 *
 * @copyright  2019 Ryan Wyllie <ryan@moodle.com>
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */
class renderer {
    /** @var legacy_data_mapper_factory $legacydatamapperfactory Legacy data mapper factory */
    private $legacydatamapperfactory;
    /** @var exporter_factory $exporterfactory Exporter factory */
    private $exporterfactory;
    /** @var vault_factory $vaultfactory Vault factory */
    private $vaultfactory;
    /** @var manager_factory $managerfactory Manager factory */
    private $managerfactory;
    /** @var entity_factory $entityfactory Entity factory */
    private $entityfactory;
    /** @var builder_factory $builderfactory Builder factory */
    private $builderfactory;
    /** @var url_factory $urlfactory URL factory */
    private $urlfactory;
    /** @var renderer_base $rendererbase Renderer base */
    private $rendererbase;
    /** @var moodle_page $page Moodle page */
    private $page;

    /**
     * Constructor.
     *
     * @param legacy_data_mapper_factory $legacydatamapperfactory Legacy data mapper factory
     * @param exporter_factory $exporterfactory Exporter factory
     * @param vault_factory $vaultfactory Vault factory
     * @param manager_factory $managerfactory Manager factory
     * @param entity_factory $entityfactory Entity factory
     * @param builder_factory $builderfactory Builder factory
     * @param url_factory $urlfactory URL factory
     * @param moodle_page $page Moodle page
     */
    public function __construct(
        legacy_data_mapper_factory $legacydatamapperfactory,
        exporter_factory $exporterfactory,
        vault_factory $vaultfactory,
        manager_factory $managerfactory,
        entity_factory $entityfactory,
        builder_factory $builderfactory,
        url_factory $urlfactory,
        moodle_page $page
    ) {
        $this->legacydatamapperfactory = $legacydatamapperfactory;
        $this->exporterfactory = $exporterfactory;
        $this->vaultfactory = $vaultfactory;
        $this->managerfactory = $managerfactory;
        $this->entityfactory = $entityfactory;
        $this->builderfactory = $builderfactory;
        $this->urlfactory = $urlfactory;
        $this->page = $page;
        $this->rendererbase = $page->get_renderer('mod_forum');
    }

    /**
     * Create a discussion renderer for the given forum and discussion.
     *
     * @param forum_entity $forum Forum the discussion belongs to
     * @param discussion_entity $discussion Discussion to render
     * @param int $displaymode How should the posts be formatted?
     * @return discussion_renderer
     */
    public function get_discussion_renderer(
        forum_entity $forum,
        discussion_entity $discussion,
        int $displaymode
    ): discussion_renderer {

        $capabilitymanager = $this->managerfactory->get_capability_manager($forum);
        $ratingmanager = $this->managerfactory->get_rating_manager();
        $rendererbase = $this->rendererbase;

        $baseurl = $this->urlfactory->get_discussion_view_url_from_discussion($discussion);
        $notifications = [];

        return new discussion_renderer(
            $forum,
            $discussion,
            $displaymode,
            $rendererbase,
            $this->get_single_discussion_posts_renderer($displaymode, false),
            $this->page,
            $this->legacydatamapperfactory,
            $this->exporterfactory,
            $this->vaultfactory,
            $this->urlfactory,
            $this->entityfactory,
            $capabilitymanager,
            $ratingmanager,
            $this->entityfactory->get_exported_posts_sorter(),
            $baseurl,
            $notifications,
            function($discussion, $user, $forum) {
                $exportbuilder = $this->builderfactory->get_exported_discussion_builder();
                return $exportbuilder->build(
                    $user,
                    $forum,
                    $discussion
                );
            }
        );
    }

    /**
     * Create a posts renderer to render posts without defined parent/reply relationships.
     *
     * @return posts_renderer
     */
    public function get_posts_renderer(): posts_renderer {
        return new posts_renderer(
            $this->rendererbase,
            $this->builderfactory->get_exported_posts_builder(),
            'mod_forum/forum_discussion_posts'
        );
    }

    /**
     * Create a posts renderer to render a list of posts in a single discussion.
     *
     * @param int|null $displaymode How should the posts be formatted?
     * @param bool $readonly Should the posts include the actions to reply, delete, etc?
     * @return posts_renderer
     */
    public function get_single_discussion_posts_renderer(?int $displaymode = null, bool $readonly = false): posts_renderer {
        $exportedpostssorter = $this->entityfactory->get_exported_posts_sorter();

        switch ($displaymode) {
            case FORUM_MODE_THREADED:
                $template = 'mod_forum/forum_discussion_threaded_posts';
                break;
            case FORUM_MODE_NESTED:
                $template = 'mod_forum/forum_discussion_nested_posts';
                break;
            case FORUM_MODE_NESTED_V2:
                $template = 'mod_forum/forum_discussion_nested_v2_posts';
                break;
            default;
                $template = 'mod_forum/forum_discussion_posts';
                break;
        }

        return new posts_renderer(
            $this->rendererbase,
            $this->builderfactory->get_exported_posts_builder(),
            $template,
            // Post process the exported posts for our template. This function will add the "replies"
            // and "hasreplies" properties to the exported posts. It will also sort them into the
            // reply tree structure if the display mode requires it.
            function($exportedposts, $forums, $discussions) use ($displaymode, $readonly, $exportedpostssorter) {
                $forum = array_shift($forums);
                $seenfirstunread = false;
                $postcount = count($exportedposts);
                $discussionsbyid = array_reduce($discussions, function($carry, $discussion) {
                    $carry[$discussion->get_id()] = $discussion;
                    return $carry;
                }, []);
                $exportedposts = array_map(
                    function($exportedpost) use ($forum, $discussionsbyid, $readonly, $seenfirstunread, $displaymode) {
                        $discussion = $discussionsbyid[$exportedpost->discussionid] ?? null;
                        if ($forum->get_type() == 'single' && !$exportedpost->hasparent) {
                            // Remove the author from any posts that don't have a parent.
                            unset($exportedpost->author);
                            unset($exportedpost->html['authorsubheading']);
                        }

                        $exportedpost->firstpost = false;
                        $exportedpost->readonly = $readonly;
                        $exportedpost->hasreplycount = false;
                        $exportedpost->hasreplies = false;
                        $exportedpost->replies = [];
                        $exportedpost->discussionlocked = $discussion ? $discussion->is_locked() : null;

                        $exportedpost->isfirstunread = false;
                        if (!$seenfirstunread && $exportedpost->unread) {
                            $exportedpost->isfirstunread = true;
                            $seenfirstunread = true;
                        }

                        if ($displaymode === FORUM_MODE_NESTED_V2) {
                            $exportedpost->showactionmenu = $exportedpost->capabilities['view'] ||
                                                            $exportedpost->capabilities['controlreadstatus'] ||
                                                            $exportedpost->capabilities['edit'] ||
                                                            $exportedpost->capabilities['split'] ||
                                                            $exportedpost->capabilities['delete'] ||
                                                            $exportedpost->capabilities['export'] ||
                                                            !empty($exportedpost->urls['viewparent']);
                        }

                        return $exportedpost;
                    },
                    $exportedposts
                );

                if (
                    $displaymode === FORUM_MODE_NESTED ||
                    $displaymode === FORUM_MODE_THREADED ||
                    $displaymode === FORUM_MODE_NESTED_V2
                ) {
                    $sortedposts = $exportedpostssorter->sort_into_children($exportedposts);
                    $sortintoreplies = function($nestedposts) use (&$sortintoreplies) {
                        return array_map(function($postdata) use (&$sortintoreplies) {
                            [$post, $replies] = $postdata;
                            $totalreplycount = 0;

                            if (empty($replies)) {
                                $post->replies = [];
                                $post->hasreplies = false;
                            } else {
                                $sortedreplies = $sortintoreplies($replies);
                                // Set the parent author name on the replies. This is used for screen
                                // readers to help them identify the structure of the discussion.
                                $sortedreplies = array_map(function($reply) use ($post) {
                                    if (isset($post->author)) {
                                        $reply->parentauthorname = $post->author->fullname;
                                    } else {
                                        // The only time the author won't be set is for a single discussion
                                        // forum. See above for where it gets unset.
                                        $reply->parentauthorname = get_string('firstpost', 'mod_forum');
                                    }
                                    return $reply;
                                }, $sortedreplies);

                                $totalreplycount = array_reduce($sortedreplies, function($carry, $reply) {
                                    return $carry + 1 + $reply->totalreplycount;
                                }, $totalreplycount);

                                $post->replies = $sortedreplies;
                                $post->hasreplies = true;
                            }

                            $post->totalreplycount = $totalreplycount;

                            return $post;
                        }, $nestedposts);
                    };
                    // Set the "replies" property on the exported posts.
                    $exportedposts = $sortintoreplies($sortedposts);
                } else if ($displaymode === FORUM_MODE_FLATNEWEST || $displaymode === FORUM_MODE_FLATOLDEST) {
                    $exportedfirstpost = array_shift($exportedposts);
                    $exportedfirstpost->replies = $exportedposts;
                    $exportedfirstpost->hasreplies = true;
                    $exportedposts = [$exportedfirstpost];
                }

                if (!empty($exportedposts)) {
                    // Need to identify the first post so that we can use it in behat tests.
                    $exportedposts[0]->firstpost = true;
                    $exportedposts[0]->hasreplycount = true;
                    $exportedposts[0]->replycount = $postcount - 1;
                }

                return $exportedposts;
            }
        );
    }

    /**
     * Create a posts renderer to render posts in the forum search results.
     *
     * @param string[] $searchterms The search terms to be highlighted in the posts
     * @return posts_renderer
     */
    public function get_posts_search_results_renderer(array $searchterms): posts_renderer {
        $urlfactory = $this->urlfactory;

        return new posts_renderer(
            $this->rendererbase,
            $this->builderfactory->get_exported_posts_builder(),
            'mod_forum/forum_search_results',
            // Post process the exported posts to add the highlighting of the search terms to the post
            // and also the additional context links in the subject.
            function($exportedposts, $forumsbyid, $discussionsbyid) use ($searchterms, $urlfactory) {
                $highlightwords = implode(' ', $searchterms);

                return array_map(
                    function($exportedpost) use (
                        $forumsbyid,
                        $discussionsbyid,
                        $searchterms,
                        $highlightwords,
                        $urlfactory
                    ) {
                        $discussion = $discussionsbyid[$exportedpost->discussionid];
                        $forum = $forumsbyid[$discussion->get_forum_id()];

                        $viewdiscussionurl = $urlfactory->get_discussion_view_url_from_discussion($discussion);
                        $exportedpost->urls['viewforum'] = $urlfactory->get_forum_view_url_from_forum($forum)->out(false);
                        $exportedpost->urls['viewdiscussion'] = $viewdiscussionurl->out(false);
                        $exportedpost->subject = highlight($highlightwords, $exportedpost->subject);
                        $exportedpost->forumname = format_string($forum->get_name(), true);
                        $exportedpost->discussionname = highlight($highlightwords, format_string($discussion->get_name(), true));
                        $exportedpost->showdiscussionname = $forum->get_type() != 'single';

                        // Identify search terms only found in HTML markup, and add a warning about them to
                        // the start of the message text. This logic was copied exactly as is from the previous
                        // implementation.
                        $missingterms = '';
                        $exportedpost->message = highlight(
                            $highlightwords,
                            $exportedpost->message,
                            0,
                            '<fgw9sdpq4>',
                            '</fgw9sdpq4>'
                        );

                        foreach ($searchterms as $searchterm) {
                            if (
                                preg_match("/$searchterm/i", $exportedpost->message) &&
                                !preg_match('/<fgw9sdpq4>' . $searchterm . '<\/fgw9sdpq4>/i', $exportedpost->message)
                            ) {
                                $missingterms .= " $searchterm";
                            }
                        }

                        $exportedpost->message = str_replace('<fgw9sdpq4>', '<span class="highlight">', $exportedpost->message);
                        $exportedpost->message = str_replace('</fgw9sdpq4>', '</span>', $exportedpost->message);

                        if ($missingterms) {
                            $strmissingsearchterms = get_string('missingsearchterms', 'forum');
                            $exportedpost->message = '<p class="highlight2">' . $strmissingsearchterms . ' '
                                . $missingterms . '</p>' . $exportedpost->message;
                        }

                        return $exportedpost;
                    },
                    $exportedposts
                );
            }
        );
    }

    /**
     * Create a posts renderer to render posts in mod/forum/user.php.
     *
     * @param bool $addlinkstocontext Should links to the course, forum, and discussion be included?
     * @return posts_renderer
     */
    public function get_user_forum_posts_report_renderer(bool $addlinkstocontext): posts_renderer {
        $urlfactory = $this->urlfactory;

        return new posts_renderer(
            $this->rendererbase,
            $this->builderfactory->get_exported_posts_builder(),
            'mod_forum/forum_posts_with_context_links',
            function($exportedposts, $forumsbyid, $discussionsbyid) use ($urlfactory, $addlinkstocontext) {

                return array_map(function($exportedpost) use ($forumsbyid, $discussionsbyid, $addlinkstocontext, $urlfactory) {
                    $discussion = $discussionsbyid[$exportedpost->discussionid];
                    $forum = $forumsbyid[$discussion->get_forum_id()];
                    $courserecord = $forum->get_course_record();

                    if ($addlinkstocontext) {
                        $viewdiscussionurl = $urlfactory->get_discussion_view_url_from_discussion($discussion);
                        $exportedpost->urls['viewforum'] = $urlfactory->get_forum_view_url_from_forum($forum)->out(false);
                        $exportedpost->urls['viewdiscussion'] = $viewdiscussionurl->out(false);
                        $exportedpost->urls['viewcourse'] = $urlfactory->get_course_url_from_forum($forum)->out(false);
                    }

                    $exportedpost->forumname = format_string($forum->get_name(), true);
                    $exportedpost->discussionname = format_string($discussion->get_name(), true);
                    $exportedpost->coursename = format_string($courserecord->shortname, true);
                    $exportedpost->showdiscussionname = $forum->get_type() != 'single';

                    return $exportedpost;
                }, $exportedposts);
            }
        );
    }

    /**
     * Create a standard type discussion list renderer.
     *
     * @param forum_entity $forum The forum that the discussions belong to
     * @return discussion_list_renderer
     */
    public function get_discussion_list_renderer(
        forum_entity $forum
    ): discussion_list_renderer {

        $capabilitymanager = $this->managerfactory->get_capability_manager($forum);
        $rendererbase = $this->rendererbase;
        $notifications = [];

        switch ($forum->get_type()) {
            case 'news':
                if (SITEID == $forum->get_course_id()) {
                    $template = 'mod_forum/frontpage_news_discussion_list';
                } else {
                    $template = 'mod_forum/news_discussion_list';
                }
                break;
            case 'qanda':
                $template = 'mod_forum/qanda_discussion_list';
                break;
            default:
                $template = 'mod_forum/discussion_list';
        }

        return new discussion_list_renderer(
            $forum,
            $rendererbase,
            $this->legacydatamapperfactory,
            $this->exporterfactory,
            $this->vaultfactory,
            $this->builderfactory,
            $capabilitymanager,
            $this->urlfactory,
            forum_gradeitem::load_from_forum_entity($forum),
            $template,
            $notifications,
            function($discussions, $user, $forum) {

                $exporteddiscussionsummarybuilder = $this->builderfactory->get_exported_discussion_summaries_builder();
                return $exporteddiscussionsummarybuilder->build(
                    $user,
                    $forum,
                    $discussions
                );
            }
        );
    }

    /**
     * Create a discussion list renderer which shows more information about the first post.
     *
     * @param forum_entity $forum The forum that the discussions belong to
     * @param string $template The template to use
     * @return discussion_list_renderer
     */
    private function get_detailed_discussion_list_renderer(
        forum_entity $forum,
        string $template
    ): discussion_list_renderer {

        $capabilitymanager = $this->managerfactory->get_capability_manager($forum);
        $rendererbase = $this->rendererbase;
        $notifications = [];

        return new discussion_list_renderer(
            $forum,
            $rendererbase,
            $this->legacydatamapperfactory,
            $this->exporterfactory,
            $this->vaultfactory,
            $this->builderfactory,
            $capabilitymanager,
            $this->urlfactory,
            forum_gradeitem::load_from_forum_entity($forum),
            $template,
            $notifications,
            function($discussions, $user, $forum) use ($capabilitymanager) {
                $exportedpostsbuilder = $this->builderfactory->get_exported_posts_builder();
                $discussionentries = [];
                $postentries = [];
                foreach ($discussions as $discussion) {
                    $discussionentries[] = $discussion->get_discussion();
                    $discussionentriesids[] = $discussion->get_discussion()->get_id();
                    $postentries[] = $discussion->get_first_post();
                }

                $exportedposts['posts'] = $exportedpostsbuilder->build(
                    $user,
                    [$forum],
                    $discussionentries,
                    $postentries
                );

                $postvault = $this->vaultfactory->get_post_vault();
                $canseeanyprivatereply = $capabilitymanager->can_view_any_private_reply($user);
                $discussionrepliescount = $postvault->get_reply_count_for_discussion_ids(
                        $user,
                        $discussionentriesids,
                        $canseeanyprivatereply
                    );
                $forumdatamapper = $this->legacydatamapperfactory->get_forum_data_mapper();
                $forumrecord = $forumdatamapper->to_legacy_object($forum);
                if (forum_tp_is_tracked($forumrecord, $user)) {
                    $discussionunreadscount = $postvault->get_unread_count_for_discussion_ids(
                            $user,
                            $discussionentriesids,
                            $canseeanyprivatereply
                    );
                } else {
                    $discussionunreadscount = [];
                }

                array_walk($exportedposts['posts'], function($post) use ($discussionrepliescount, $discussionunreadscount) {
                    $post->discussionrepliescount = $discussionrepliescount[$post->discussionid] ?? 0;
                    $post->discussionunreadscount = $discussionunreadscount[$post->discussionid] ?? 0;
                    // TODO: Find a better solution due to language differences when defining the singular and plural form.
                    $post->isreplyplural = $post->discussionrepliescount != 1 ? true : false;
                    $post->isunreadplural = $post->discussionunreadscount != 1 ? true : false;
                });

                $exportedposts['state']['hasdiscussions'] = $exportedposts['posts'] ? true : false;

                return $exportedposts;
            }
        );
    }

    /**
     * Create a blog type discussion list renderer.
     *
     * @param forum_entity $forum The forum that the discussions belong to
     * @return discussion_list_renderer
     */
    public function get_blog_discussion_list_renderer(
        forum_entity $forum
    ): discussion_list_renderer {
        return $this->get_detailed_discussion_list_renderer($forum, 'mod_forum/blog_discussion_list');
    }

    /**
     * Create a discussion list renderer for the social course format.
     *
     * @param forum_entity $forum The forum that the discussions belong to
     * @return discussion_list_renderer
     */
    public function get_social_discussion_list_renderer(
        forum_entity $forum
    ): discussion_list_renderer {
        return $this->get_detailed_discussion_list_renderer($forum, 'mod_forum/social_discussion_list');
    }

    /**
     * Create a discussion list renderer for the social course format.
     *
     * @param forum_entity $forum The forum that the discussions belong to
     * @return discussion_list_renderer
     */
    public function get_frontpage_news_discussion_list_renderer(
        forum_entity $forum
    ): discussion_list_renderer {
        return $this->get_detailed_discussion_list_renderer($forum, 'mod_forum/frontpage_social_discussion_list');
    }

    /**
     * Create a single type discussion list renderer.
     *
     * @param forum_entity $forum Forum the discussion belongs to
     * @param discussion_entity $discussion The discussion entity
     * @param bool $hasmultiplediscussions Whether the forum has multiple discussions (more than one)
     * @param int $displaymode How should the posts be formatted?
     * @return discussion_renderer
     */
    public function get_single_discussion_list_renderer(
        forum_entity $forum,
        discussion_entity $discussion,
        bool $hasmultiplediscussions,
        int $displaymode
    ): discussion_renderer {

        $capabilitymanager = $this->managerfactory->get_capability_manager($forum);
        $ratingmanager = $this->managerfactory->get_rating_manager();
        $rendererbase = $this->rendererbase;

        $cmid = $forum->get_course_module_record()->id;
        $baseurl = $this->urlfactory->get_forum_view_url_from_course_module_id($cmid);
        $notifications = array();

        if ($hasmultiplediscussions) {
            $notifications[] = (new notification(get_string('warnformorepost', 'forum')))
                ->set_show_closebutton(true);
        }

        return new discussion_renderer(
            $forum,
            $discussion,
            $displaymode,
            $rendererbase,
            $this->get_single_discussion_posts_renderer($displaymode, false),
            $this->page,
            $this->legacydatamapperfactory,
            $this->exporterfactory,
            $this->vaultfactory,
            $this->urlfactory,
            $this->entityfactory,
            $capabilitymanager,
            $ratingmanager,
            $this->entityfactory->get_exported_posts_sorter(),
            $baseurl,
            $notifications
        );
    }
}
