<?php
// This file is part of mod_datalynx for Moodle - http://moodle.org/
//
// It 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.
//
// It 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/>.

/**
 *
 * @package datalynxfield_rating
 * @subpackage _rating
 * @copyright 2013 onwards edulabs.org and associated programmers
 * @copyright based on the work  by 2011 Itamar Tzadok
 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */

namespace datalynxfield_rating;

use html_table;
use html_table_row;
use html_writer;
use mod_datalynx\datalynx_rating_manager;
use mod_datalynx\local\field\datalynxfield_renderer;
use stdClass;

/**
 * Renderer class for the internal rating field.
 *
 * @package    mod_datalynx
 */
class renderer extends datalynxfield_renderer {
    /**
     * Returns replacement values for rating tags.
     *
     * @param ?array $tags List of tag patterns to replace.
     * @param mixed $entry The entry object.
     * @param ?array $options Rendering options.
     * @return array Associative array of tag => replacement pairs.
     */
    public function replacements(?array $tags = null, $entry = null, ?array $options = null) {
        global $DB;

        $field = $this->field;
        $edit = !empty($options['edit']) ? $options['edit'] : false;

        // If edit mode or rating not active return.
        if ($edit || (!$this->field->df()->data->rating)) {
            if ($tags) {
                $replacements = [];
                foreach ($tags as $tag) {
                    switch (trim($tag, '@')) {
                        case '##ratings:count##':
                        case '##ratings:avg##':
                        case '##ratings:max##':
                        case '##ratings:min##':
                        case '##ratings:sum##':
                            $str = '-';
                            break;
                        default:
                            $str = '';
                    }
                    $replacements[$tag] = $str ? ['html', $str] : '';
                }

                return $replacements;
            } else {
                return null;
            }
        }

        $rm = new datalynx_rating_manager();
        // Get entry rating objects.
        if ($entry->id > 0) {
            $options = new stdClass();
            $options->context = $field->df()->context;
            $options->component = 'mod_datalynx';
            $options->ratingarea = 'entry';
            // Ugly hack to work around the exception in generate_settings.
            $options->aggregate = RATING_AGGREGATE_COUNT;
            // TODO MDL-000000 check when scaleid is empty.
            $options->scaleid = !empty($entry->scaleid) ? $entry->scaleid : $field->df()->data->rating;

            $rec = new stdClass();
            $rec->itemid = $entry->id;
            $rec->context = $field->df()->context;
            $rec->component = 'mod_datalynx';
            $rec->ratingarea = 'entry';
            $rec->settings = $rm->get_rating_settings_object($options);
            $rec->aggregate = array_keys($rm->get_aggregate_types());
            $rec->scaleid = $entry->scaleid;
            $rec->userid = $entry->ratinguserid;
            $rec->id = $entry->ratingid;
            $rec->usersrating = $entry->usersrating;
            $rec->numratings = $entry->numratings;
            $rec->avgratings = $entry->avgratings;
            $rec->sumratings = $entry->sumratings;
            $rec->maxratings = $entry->maxratings;
            $rec->minratings = $entry->minratings;

            $entry->rating = $rm->get_rating_object($entry, $rec);

            $aggravg = round($entry->rating->aggregate[field::AGGREGATE_AVG] ?: 0, 2);
            $aggrmax = round($entry->rating->aggregate[field::AGGREGATE_MAX] ?: 0, 2);
            $aggrmin = round($entry->rating->aggregate[field::AGGREGATE_MIN] ?: 0, 2);
            $aggrsum = round($entry->rating->aggregate[field::AGGREGATE_SUM] ?: 0, 2);

            // Get all ratings for inline view.
            if (in_array('##ratings:viewinline##', $tags)) {
                static $allratings = false;
                static $ratingrecords = null;
                if (!$allratings) {
                    $allratings = true;
                    [$sql, $params] = $rm->get_sql_all($options, false);
                    $ratingrecords = $DB->get_records_sql($sql, $params);
                }
                if ($ratingrecords) {
                    foreach ($ratingrecords as $recordid => $raterecord) {
                        if ($raterecord->itemid < $entry->id) {
                            continue;
                        }
                        // Break if we already found the respective records.
                        if ($raterecord->itemid > $entry->id) {
                            continue;
                        }
                        // Attach the rating record to the entry.
                        if (!isset($entry->rating->records)) {
                            $entry->rating->records = [];
                        }
                        $entry->rating->records[$recordid] = $raterecord;
                    }
                }
            }
        }

        // No edit mode for this field so just return html.
        $replacements = [];
        foreach ($tags as $tag) {
            if ($entry->id > 0 && !empty($entry->rating)) {
                switch (trim($tag, '@')) {
                    case '##ratings:count##':
                        $str = !empty($entry->rating->count) ? $entry->rating->count : '-';
                        break;
                    case '##ratings:avg##':
                        $str = !empty($aggravg) ? $aggravg : '-';
                        break;
                    case '##ratings:max##':
                        $str = !empty($aggrmax) ? $aggrmax : '-';
                        break;
                    case '##ratings:min##':
                        $str = !empty($aggrmin) ? $aggrmin : '-';
                        break;
                    case '##ratings:sum##':
                        $str = !empty($aggrsum) ? $aggrsum : '-';
                        break;
                    case '##ratings:view##':
                    case '##ratings:viewurl##':
                        $str = $this->display_view($entry, $tag);
                        break;
                    case '##ratings:viewinline##':
                        $str = $this->display_view_inline($entry);
                        break;
                    case '##ratings:rate##':
                        $str = $this->render_rating($entry);
                        break;
                    case '##ratings:avg:bar##':
                        $str = $this->display_bar($entry, $aggravg);
                        break;
                    case '##ratings:avg:star##':
                        $str = $this->display_star($entry, $aggravg);
                        break;
                    default:
                        $str = '';
                }
                $replacements[$tag] = ['html', $str];
            }
        }
        return $replacements;
    }

    /**
     * Returns aggregation keys matching the given patterns.
     *
     * @param array $patterns Array of pattern strings to match.
     * @return ?array Array of matching aggregation keys, or null if none match.
     */
    public function get_aggregations($patterns) {
        $aggr = [field::AGGREGATE_AVG => '##ratings:avg##',
                field::AGGREGATE_MAX => '##ratings:max##',
                field::AGGREGATE_MIN => '##ratings:min##',
                field::AGGREGATE_SUM => '##ratings:sum##'];
        if ($aggregations = array_intersect($aggr, $patterns)) {
            return array_keys($aggregations);
        } else {
            return null;
        }
    }

    // phpcs:disable moodle.PHP.ForbiddenGlobalUse.BadGlobal
    /**
     * Displays a link to view all ratings for an entry.
     *
     * @param object $entry The entry object.
     * @param string $tag The tag pattern being rendered.
     * @return string HTML string.
     */
    protected function display_view($entry, $tag) {
        global $OUTPUT;

        if (isset($entry->rating)) {
            $rating = $entry->rating;
            if (
                $rating->settings->permissions->viewall &&
                    $rating->settings->pluginpermissions->viewall
            ) {
                $nonpopuplink = $rating->get_view_ratings_url();
                $popuplink = $rating->get_view_ratings_url(true);
                $popupaction = new popup_action(
                    'click',
                    $popuplink,
                    'ratings',
                    ['height' => 400, 'width' => 600,
                    ]
                );

                if ($tag == '##ratings:view##') {
                    return $OUTPUT->action_link(
                        $nonpopuplink,
                        'view all',
                        $popupaction
                    );
                } else {
                    return $popuplink;
                }
            }
        }
        return '';
    }
    // phpcs:enable moodle.PHP.ForbiddenGlobalUse.BadGlobal

    // phpcs:disable moodle.PHP.ForbiddenGlobalUse.BadGlobal
    /**
     * Displays an inline table of all ratings for an entry.
     *
     * @param object $entry The entry object.
     * @return string HTML string.
     */
    protected function display_view_inline($entry) {
        global $OUTPUT, $DB;

        if (isset($entry->rating)) {
            $rating = $entry->rating;
            if (
                $rating->settings->permissions->viewall &&
                    $rating->settings->pluginpermissions->viewall && !empty($rating->records)
            ) {
                $scalemenu = make_grades_menu($rating->settings->scale->id);

                $table = new html_table();
                $table->cellpadding = 3;
                $table->cellspacing = 3;
                $table->attributes['class'] = 'generalbox ratingtable';
                $table->colclasses = ['', 'firstname', 'rating', 'time',
                ];
                $table->data = [];

                // If the scale was changed after ratings were submitted some ratings may have av alue above the current maximum.
                // We can't just do count($scalemenu) - 1 as custom scales start at index 1, not 0.
                $maxrating = $rating->settings->scale->max;

                foreach ($rating->records as $raterecord) {
                    // Undo the aliasing of the user id column from user_picture::fields().
                    // We could clone the rating object or preserve the rating id if we needed it again.
                    // But we don't.
                    $raterecord->id = $raterecord->userid;
                    $row = new html_table_row();
                    $row->attributes['class'] = 'ratingitemheader';
                    $row->cells[] = $OUTPUT->user_picture(
                        $raterecord,
                        ['courseid' => $this->field->df()->course->id,
                        ]
                    );
                    $userrecord = $DB->get_record('user', ['id' => $raterecord->userid,
                    ]);
                    $row->cells[] = fullname($userrecord);
                    if ($raterecord->rating > $maxrating) {
                        $raterecord->rating = $maxrating;
                    }
                    $row->cells[] = $scalemenu[$raterecord->rating];
                    $row->cells[] = userdate(
                        $raterecord->timemodified,
                        get_string('strftimedate', 'langconfig')
                    );
                    $table->data[] = $row;
                }
                return html_writer::table($table);
            }
        }
        return '';
    }
    // phpcs:enable moodle.PHP.ForbiddenGlobalUse.BadGlobal

    /**
     * Displays a bar chart representation of the rating value.
     *
     * @param object $entry The entry object.
     * @param mixed $value The rating value to display.
     * @return string HTML string.
     */
    protected function display_bar($entry, $value) {
        if (isset($entry->rating) && $value) {
            $rating = $entry->rating;

            $width = round($value / $rating->settings->scale->max * 100);
            $bar = html_writer::tag(
                'div',
                '.',
                ['style' => "width:$width%;height:100%;background:gold;color:gold",
                ]
            );
            return $bar;
        }
        return '';
    }

    // phpcs:disable moodle.PHP.ForbiddenGlobalUse.BadGlobal
    /**
     * Displays a star rating widget for an entry.
     *
     * @param object $entry The entry object.
     * @param mixed $value The current rating value.
     * @return string HTML string.
     */
    protected function display_star($entry, $value) {

        if (isset($entry->rating)) {
            $rating = $entry->rating;
            $numstars = $rating->settings->scale->max;
            $width = $numstars * 20;

            $innerstyle = 'width:100%;height:19px;position:absolute;top:0;left:0;';
            $bgdiv = html_writer::tag(
                'div',
                '.',
                ['style' => "background:#ccc;color:#ccc;$innerstyle",
                ]
            );
            $bar = html_writer::tag(
                'div',
                $this->display_bar($entry, $value),
                ['style' => "z-index:5;$innerstyle",
                ]
            );
            $stars = implode(
                '',
                array_fill(
                    0,
                    $numstars,
                    html_writer::tag('i', '', ['class' => 'fa-regular fa-star', 'aria-hidden' => 'true',
                            'style' => 'float:left;color:#ccc;',
                            ])
                )
            );
            $starsdiv = html_writer::tag('div', $stars, ['style' => "z-index:10;$innerstyle",
            ]);
            $wrapper = html_writer::tag(
                'div',
                "$bgdiv $bar $starsdiv",
                ['style' => "width:{$width}px;position:relative;",
                ]
            );
            return $wrapper;
        }
        return '';
    }
    // phpcs:enable moodle.PHP.ForbiddenGlobalUse.BadGlobal

    // phpcs:disable moodle.PHP.ForbiddenGlobalUse.BadGlobal
    /**
     * Renders the interactive rating form for an entry.
     *
     * @param object $entry The entry object.
     * @return string HTML string.
     */
    public function render_rating($entry) {
        global $OUTPUT, $PAGE;

        $ratinghtml = '';

        if (isset($entry->rating)) {
            $rating = $entry->rating;

            $rm = new datalynx_rating_manager();
            // Initialise the JavaScript so ratings can be done by AJAX.
            $rm->initialise_rating_javascript($PAGE);

            $strrate = get_string("rate", "rating");
            $ratinghtml = ''; // The string we'll return.

            // Hack to work around the js updating imposed text.
            $ratinghtml .= html_writer::tag(
                'span',
                '',
                ['id' => "ratingaggregate{$rating->itemid}", 'style' => 'display:none;',
                ]
            );
            $ratinghtml .= html_writer::tag(
                'span',
                '',
                ['id' => "ratingcount{$rating->itemid}", 'style' => 'display:none;',
                ]
            );

            $formstart = null;
            // If the item doesn't belong to the current user, the user has permission to rate.
            // And we're within the assessable period.
            if (
                $rating->user_can_rate() ||
                    has_capability('mod/datalynx:manageratings', $this->field->df()->context)
            ) {
                $rateurl = $rating->get_rate_url();
                $inputs = $rateurl->params();

                // Start the rating form.
                $formattrs = ['id' => "postrating{$rating->itemid}",
                        'class' => 'postratingform', 'method' => 'post',
                        'action' => $rateurl->out_omit_querystring(),
                ];
                $formstart = html_writer::start_tag('form', $formattrs);
                $formstart .= html_writer::start_tag('div', ['class' => 'ratingform',
                ]);

                // Add the hidden inputs.
                foreach ($inputs as $name => $value) {
                    $attributes = ['type' => 'hidden', 'class' => 'ratinginput',
                            'name' => $name, 'value' => $value,
                    ];
                    $formstart .= html_writer::empty_tag('input', $attributes);
                }

                $ratinghtml = $formstart . $ratinghtml;

                $scalearray = [RATING_UNSET_RATING => $strrate . '...',
                        ] + $rating->settings->scale->scaleitems;
                $scaleattrs = ['class' => 'postratingmenu ratinginput',
                        'id' => 'menurating' . $rating->itemid,
                ];
                $ratinghtml .= html_writer::select(
                    $scalearray,
                    'rating',
                    $rating->rating,
                    false,
                    $scaleattrs
                );

                // Output submit button.
                $ratinghtml .= html_writer::start_tag('span', ['class' => "ratingsubmit",
                ]);

                $attributes = ['type' => 'submit', 'class' => 'postratingmenusubmit',
                        'id' => 'postratingsubmit' . $rating->itemid,
                        'value' => s(get_string('rate', 'rating')),
                ];
                $ratinghtml .= html_writer::empty_tag('input', $attributes);

                if (!$rating->settings->scale->isnumeric) {
                    $ratinghtml .= $OUTPUT->help_icon_scale(
                        $rating->settings->scale->courseid,
                        $rating->settings->scale
                    );
                }
                $ratinghtml .= html_writer::end_tag('span');
                $ratinghtml .= html_writer::end_tag('div');
                $ratinghtml .= html_writer::end_tag('form');
            }
        }

        return $ratinghtml;
    }
    // phpcs:enable moodle.PHP.ForbiddenGlobalUse.BadGlobal

    /**
     * Array of patterns this field supports
     */
    protected function patterns() {
        $fieldinternalname = $this->field->get('internalname');
        $cat = get_string('ratings', 'datalynx');

        $patterns = [];
        switch ($fieldinternalname) {
            case 'ratings':
                $patterns['##ratings:rate##'] = [true, $cat,
                ];
                $patterns['##ratings:view##'] = [true, $cat,
                ];
                $patterns['##ratings:viewurl##'] = [false,
                ];
                $patterns['##ratings:viewinline##'] = [true, $cat,
                ];
                break;
            case 'avgratings':
                $patterns['##ratings:avg##'] = [true, $cat,
                ];
                $patterns['##ratings:avg:bar##'] = [false,
                ];
                $patterns['##ratings:avg:star##'] = [false,
                ];
                break;
            case 'countratings':
                $patterns['##ratings:count##'] = [true, $cat,
                ];
                break;
            case 'maxratings':
                $patterns['##ratings:max##'] = [true, $cat,
                ];
                break;
            case 'minratings':
                $patterns['##ratings:min##'] = [true, $cat,
                ];
                break;
            case 'sumratings':
                $patterns['##ratings:sum##'] = [true, $cat,
                ];
                break;
        }

        return $patterns;
    }
}
