<?php
/**
 * This class formats the HTML output of every popular posts listing.
 *
 *
 * @package    WordPressPopularPosts
 * @author     Hector Cabrera <me@cabrerahector.com>
 */

namespace WordPressPopularPosts;

class Output {

    /**
     * Popular posts data.
     *
     * @since   4.0.0
     * @var     string
     */
    private $data;

    /**
     * HTML output.
     *
     * @since   4.0.0
     * @var     string
     */
    private $output;

    /**
     * Widget / shortcode settings.
     *
     * @since   4.0.0
     * @var     array
     */
    private $public_options = [];

    /**
     * Administrative settings.
     *
     * @since   2.3.3
     * @var     array
     */
    private $admin_options = [];

    /**
     * Default excerpt 'more' string.
     *
     * @since   4.2.1
     * @var     string
     */
    private $more;

    /**
     * Image object
     *
     * @since   4.0.2
     * @var     WordPressPopularPosts\Image
     */
    private $thumbnail;

    /**
     * Translate object.
     *
     * @var     \WordPressPopularPosts\Translate    $translate
     * @access  private
     */
    private $translate;

    /**
     * Themer object.
     *
     * @var     \WordPressPopularPosts\Themer       $themer
     * @access  private
     */
    private $themer;

    /**
     * WordPress Date format.
     *
     * @var     string
     * @access  private
     */
    private $wp_date_format;

    /**
     * Constructor.
     *
     * @since   4.0.0
     * @param   array                           $public_options
     * @param   array                           $admin_options
     * @param   WordPressPopularPosts\Image     $thumbnail
     * @param   WordPressPopularPosts\Translate $translate
     * @param   WordPressPopularPosts\Themer    $themer
     */
    public function __construct(array $public_options, array $admin_options, Image $thumbnail, Translate $translate, Themer $themer)
    {
        $this->public_options = $public_options;
        $this->admin_options = $admin_options;
        $this->thumbnail = $thumbnail;
        $this->translate = $translate;
        $this->themer = $themer;

        $this->more = '...';

        $this->wp_date_format = get_option('date_format');

        if ( ! $this->wp_date_format ) {
            $this->wp_date_format = 'F j, Y';
        }
    }

    /**
     * Sets data.
     *
     * @since   5.0.0
     * @param   array
     */
    public function set_data(array $data = [])
    {
        $this->data = $data;
    }

    /**
     * Sets public options.
     *
     * @since   5.0.0
     * @param   array
     */
    public function set_public_options(array $public_options = [])
    {
        $this->public_options = Helper::merge_array_r(
            Settings::get('widget_options'),
            $public_options
        );
    }

    /**
     * Output the HTML.
     *
     * @since   4.0.0
     */
    public function output()
    {
        echo $this->get_output(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- At this point everything has been escaped / sanitized already
    }

    /**
     * Return the HTML.
     *
     * @since   4.0.0
     * @return  string
     */
    public function get_output()
    {
        $this->output = $this->get_clean_output();
        $this->output = ( WP_DEBUG ? "\n" . '<!-- WP Popular Posts v' . WPP_VERSION . ( $this->admin_options['tools']['cache']['active'] ? ' - cached' : '' ) . ' -->' . "\n" : '' ) . $this->output;
        return $this->output;
    }

    /**
     * Returns a "clean" version of the HTML output.
     *
     * @since   7.3.7
     * @return  string
     */
    private function get_clean_output()
    {
        if ( $this->output ) {
            // Attempt to close open tags
            $this->output = force_balance_tags($this->output);

            /**
             * @ToDo
             *
             * Look into \Dom\HTMLDocument (PHP 8.4 apparently) to see
             * if it's a good alternative to the code below.
             */

            if ( extension_loaded('mbstring') && function_exists('mb_encode_numericentity') ) {
                $clean_html = '';
                $html = trim($this->output);

                // Process special characters
                $html = htmlspecialchars_decode(mb_encode_numericentity(htmlentities($html, ENT_QUOTES, 'UTF-8'), [0x80, 0x10FFFF, 0, ~0], 'UTF-8'));

                // Remove empty tags
                $html = '<!DOCTYPE html><html><head><meta charset="UTF-8" /></head><body>' . $html . '</body></html>';

                $dom = new \DOMDocument();
                $dom->loadHTML($html, LIBXML_NOERROR | LIBXML_NOWARNING | LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
                $xpath = new \DOMXPath($dom);

                // Void/empty tags that should never be removed
                $voids = ['br', 'div', 'hr', 'img', 'source', 'span'];
                $voidPred = implode(' or ', array_map(function($n) { return "name() = '" . $n . "'"; }, $voids));

                // Build XPath predicate
                $query = "//*[not(normalize-space()) and not(*) and not({$voidPred})]";

                // Iterate until no more removable nodes found (because removing can create new empty tags)
                do {
                    $removed = false;
                    $nodes = $xpath->query($query);

                    if ( $nodes === null || $nodes->length === 0 ) {
                        break;
                    }

                    // Collect nodes first to avoid live-node issues
                    $toRemove = [];

                    foreach( $nodes as $node ) {
                        $toRemove[] = $node;
                    }

                    foreach( $toRemove as $node ) {
                        if ($node->parentNode) {
                            $node->parentNode->removeChild($node);
                            $removed = true;
                        }
                    }
                } while( $removed );

                $body = $dom->getElementsByTagName('body')->item(0);

                if ( isset($body->childNodes) ) {
                    foreach( $body->childNodes as $node ) {
                        $clean_html .= $dom->saveHTML($node);
                    }
                }

                $this->output = trim($clean_html);
            } else {
                if ( defined('WP_DEBUG') && WP_DEBUG ) {
                    trigger_error('WP Popular Posts - looks like PHP\'s mbstring extension isn\'t enabled on this site. Please enable it for the plugin to be able to properly format your popular post list.', E_USER_WARNING);
                }
            }

            // Remove excess line jumps
            // @TODO
            // Causes encoding artifacts in some languages
            // See: https://wordpress.org/support/topic/encoding-issue-with-titles-and-excerpts-in-wpp-shortcode/
            // Using the /u modifier may fix this, requires further testing
            // $this->output = preg_replace('/\R+/', "\n", $this->output);

            // Sanitize HTML
            $this->output = Helper::sanitize_html($this->output, $this->public_options);
        }

        return $this->output;
    }

    /**
     * Build the HTML output.
     *
     * @since   4.0.0
     */
    public function build_output()
    {
        // Got some posts, format 'em!
        if ( ! empty($this->data) ) {

            $this->output = '';

            // Allow WP themers / coders access to raw data
            // so they can build their own output
            if ( has_filter('wpp_custom_html') ) {
                $this->output .= apply_filters('wpp_custom_html', $this->data, $this->public_options);
                return;
            }

            if (
                isset($this->public_options['theme']['name'])
                && $this->public_options['theme']['name']
            ) {
                $this->output .= '<div class="popular-posts-sr">';

                if ( @file_exists(get_stylesheet_directory() . '/wordpress-popular-posts/themes/' . $this->public_options['theme']['name'] . '/style.css') ) {
                    $theme_stylesheet = get_stylesheet_directory() . '/wordpress-popular-posts/themes/' . $this->public_options['theme']['name'] . '/style.css';
                } else {
                    $theme_stylesheet = $this->themer->get_theme($this->public_options['theme']['name'])['path'] . '/style.css';
                }

                $theme_css_rules = wp_strip_all_tags(file_get_contents($theme_stylesheet), true); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- We're loading a local file
                $additional_styles = '';

                if ( has_filter('wpp_additional_theme_styles') ) {
                    $additional_styles = wp_strip_all_tags(apply_filters('wpp_additional_theme_styles', '', $this->public_options['theme']['name']), true);

                    if ( $additional_styles ) {
                        $additional_styles = ' /* additional rules */ ' . $additional_styles;
                    }
                }

                $this->output .= '<style>' . $theme_css_rules . $additional_styles . '</style>';
            }

            // Opening HTML wrapper
            $this->output .= $this->build_opening_wrapper();

            // Format each post
            $position = 0;

            foreach( $this->data as $post_object ) {
                $position++;
                $this->output .= $this->render_post($post_object, $position);
            }

            // Closing HTML wrapper
            $this->output .= $this->build_closing_wrapper();

            if (
                isset($this->public_options['theme']['name'])
                && $this->public_options['theme']['name']
            ) {
                $this->output .= '</div>';
            }

        }
        // Got nothing to show, give 'em the old "Sorry. No data so far." message!
        else {
            $this->output = apply_filters('wpp_no_data', '<p class="wpp-no-data">' . __('Sorry. No data so far.', 'wordpress-popular-posts') . '</p>');
        }
    }

    /**
     * Builds the opening HTML wrapper for the list.
     *
     * @since   7.3.3
     * @return  string
     */
    public function build_opening_wrapper()
    {
        // Custom opening HTML wrapper
        if (
            isset($this->public_options['markup']['custom_html'])
            && $this->public_options['markup']['custom_html']
            && isset($this->public_options['markup']['wpp-start'])
            && isset($this->public_options['markup']['wpp-end'])
        ){
            return "\n" . htmlspecialchars_decode($this->public_options['markup']['wpp-start'], ENT_QUOTES) . "\n";
        }

        // Default opening HTML wrapper
        $classes = 'wpp-list';

        if ( $this->public_options['thumbnail']['active'] ) {
            $classes .= ' wpp-list-with-thumbnails';
        }

        return "\n<ul class=\"{$classes}\">\n";
    }

    /**
     * Builds the closing HTML wrapper for the list.
     *
     * @since   7.3.3
     * @return  string
     */
    public function build_closing_wrapper()
    {
        // Custom closing HTML wrapper
        if (
            isset($this->public_options['markup']['custom_html'])
            && $this->public_options['markup']['custom_html']
            && isset($this->public_options['markup']['wpp-start'])
            && isset($this->public_options['markup']['wpp-end'])
        ){
            return "\n" . htmlspecialchars_decode($this->public_options['markup']['wpp-end'], ENT_QUOTES) . "\n";
        }

        // Default closing HTML wrapper
        return '</ul>' . "\n";
    }

    /**
     * Build the HTML markup for a single post.
     *
     * @since   4.0.0
     * @access  private
     * @param   object   $post_object
     * @param   integer  $position
     * @return  string
     */
    private function render_post(\stdClass $post_object, int $position = 1)
    {
        $is_single = $this->is_single();
        $post = '';
        $post_id = $post_object->id;
        $trid = $this->translate->get_object_id(
            $post_object->id,
            get_post_type($post_object->id)
        );

        if ( $trid && $post_id != $trid ) {
            $post_id = $trid;
        }

        $is_current_post = ( $is_single && ($is_single == $post_id || $is_single == $post_object->id) ) ? true : false;

        // Permalink
        $permalink = esc_url($this->get_permalink($post_object, $post_id));

        // Post title (and title attribute)
        $post_title_attr = esc_attr(wp_strip_all_tags($this->get_title($post_object, $post_id)));
        $post_title = $this->get_title($post_object, $post_id);

        if ( $this->public_options['shorten_title']['active'] ) {
            $length = ( filter_var($this->public_options['shorten_title']['length'], FILTER_VALIDATE_INT) && $this->public_options['shorten_title']['length'] > 0 )
              ? $this->public_options['shorten_title']['length']
              : 25;

            $more = $this->public_options['shorten_title']['words'] ? ' ' . $this->more : $this->more;
            $more = apply_filters('wpp_title_more', $more);
            $post_title = Helper::truncate($post_title, $length, $this->public_options['shorten_title']['words'], $more);
        }

        // Thumbnail
        $post_thumbnail = $this->get_thumbnail($post_id);

        // Post excerpt
        $post_excerpt = $this->get_excerpt($post_object, $post_id);

        // Post rating
        $post_rating = $this->get_rating($post_object);

        /**
         * Post meta
         */

        // Post date
        $post_date = $this->get_date($post_object);

        // Post taxonomies
        $post_taxonomies = $this->get_taxonomies($post_id);

        // Post author
        $post_author = $this->get_author($post_object, $post_id);

        // Post views count
        $post_views = $this->get_pageviews($post_object);

        // Post comments count
        $post_comments = $this->get_comments($post_object);

        // Post meta
        $meta_arr = $this->get_metadata(
            $post_object,
            $post_id,
            $post_date,
            $post_taxonomies,
            $post_author,
            $post_views,
            $post_comments
        );

        if (
            is_array($meta_arr)
            && ! empty($meta_arr)
            && 'views' == $this->public_options['order_by']
        ) {
            $keys = ['views', 'comments', 'author', 'date', 'taxonomy'];
            $new_meta_arr = [];

            foreach($keys as $key) {
                if ( isset($meta_arr[$key])) {
                    $new_meta_arr[$key] = $meta_arr[$key];
                }
            }

            if ( ! empty($new_meta_arr) ) {
                $meta_arr = $new_meta_arr;
            }
        }

        $post_meta_separator = esc_html(apply_filters('wpp_post_meta_separator', ' | '));
        $post_meta = join($post_meta_separator, $meta_arr);

        $prettify_numbers = apply_filters('wpp_prettify_numbers', true);

        /**
         * @ToDo
         *
         * Remove this filter, it's a typo and the correct one has been around since 6.3.4
         *
         * @since  7.3.7
         */
        if ( has_filter('wpp_pretiffy_numbers') ) {
            $prettify_numbers = apply_filters('wpp_pretiffy_numbers', true);

            if ( defined('WP_DEBUG') && WP_DEBUG ) {
                trigger_error('WP Popular Posts - wpp_pretiffy_numbers has been deprecated. Please use wpp_prettify_numbers instead.', E_USER_WARNING);
            }
        }

        // Build custom HTML output
        if ( $this->public_options['markup']['custom_html'] ) {
            $data = [
                'id' => $post_id,
                'is_current_post' => $is_current_post,
                'title' => '<a href="' . $permalink . '" ' . ($post_title_attr !== $post_title ? 'title="' . $post_title_attr . '" ' : '' ) . 'class="wpp-post-title" target="' . esc_attr($this->admin_options['tools']['link']['target']) . '">' . $post_title . '</a>',
                'title_attr' => $post_title_attr,
                'summary' => $post_excerpt,
                'stats' => $post_meta,
                'img' => ( ! empty($post_thumbnail) ) ? '<a href="' . $permalink . '" ' . ($post_title_attr !== $post_title ? 'title="' . $post_title_attr . '" ' : '' ) . 'target="' . esc_attr($this->admin_options['tools']['link']['target']) . '">' . $post_thumbnail . '</a>' : '',
                'img_no_link' => $post_thumbnail,
                'url' => $permalink,
                'text_title' => $post_title,
                'taxonomy' => $post_taxonomies,
                'taxonomy_copy' => isset($meta_arr['taxonomy']) ? $meta_arr['taxonomy'] : null,
                'author' => ( ! empty($post_author) ) ? '<a href="' . esc_url(get_author_posts_url($post_object->uid != $post_id ? get_post_field('post_author', $post_id) : $post_object->uid )) . '">' . esc_html($post_author) . '</a>' : '',
                'author_copy' => isset($meta_arr['author']) ? $meta_arr['author'] : null,
                'author_name' => esc_html($post_author),
                'author_url' => ( ! empty($post_author) ) ? esc_url(get_author_posts_url($post_object->uid != $post_id ? get_post_field('post_author', $post_id) : $post_object->uid)) : '',
                'views' => ( $this->public_options['order_by'] == 'views' || $this->public_options['order_by'] == 'comments' ) ? ($prettify_numbers ? Helper::prettify_number($post_views) : number_format_i18n($post_views)) : ($prettify_numbers ? Helper::prettify_number($post_views, 2) : number_format_i18n($post_views, 2)),
                'views_copy' => isset($meta_arr['views']) ? $meta_arr['views'] : null,
                'comments' => $prettify_numbers ? Helper::prettify_number($post_comments) : number_format_i18n($post_comments),
                'comments_copy' => isset($meta_arr['comments']) ? $meta_arr['comments'] : null,
                'date' => $post_date,
                'date_copy' => isset($meta_arr['date']) ? $meta_arr['date'] : null,
                'total_items' => count($this->data),
                'item_position' => $position
            ];
            $post = $this->format_content(htmlspecialchars_decode($this->public_options['markup']['post-html'], ENT_QUOTES), $data, $this->public_options['rating']) . "\n";
        } // Use the "stock" HTML output
        else {
            $wpp_post_class = [];

            if ( $is_current_post ) {
                $wpp_post_class[] = 'current';
            }

            // Allow themers / plugin developer
            // to add custom classes to each post
            $wpp_post_class = apply_filters('wpp_post_class', $wpp_post_class, $post_id);

            $post_thumbnail = ( ! empty($post_thumbnail) )
                ? "<a href=\"{$permalink}\" " . ($post_title_attr !== $post_title ? "title=\"{$post_title_attr}\" " : '') . 'target="' . esc_attr($this->admin_options['tools']['link']['target']) . "\">{$post_thumbnail}</a>\n"
                : '';

            $post_excerpt = ( ! empty($post_excerpt) )
                ? " <span class=\"wpp-excerpt\">{$post_excerpt}</span>\n"
                : '';

            $post_meta = ( ! empty($post_meta) )
                ? " <span class=\"wpp-meta post-stats\">{$post_meta}</span>\n"
                : '';

            $post_rating = ( ! empty($post_rating) )
                ? " <span class=\"wpp-rating\">{$post_rating}</span>\n"
                : '';

            $post =
                '<li' . ( ( is_array($wpp_post_class) && ! empty($wpp_post_class) ) ? ' class="' . esc_attr(implode(' ', $wpp_post_class)) . '"' : '') . ">\n"
                . $post_thumbnail
                . "<a href=\"{$permalink}\" " . ($post_title_attr !== $post_title ? "title=\"{$post_title_attr}\" " : '') . 'class="wpp-post-title" target="' . esc_attr($this->admin_options['tools']['link']['target']) . "\">{$post_title}</a>\n"
                . $post_excerpt
                . $post_meta
                . $post_rating
                . "</li>\n";
        }

        return apply_filters('wpp_post', $post, $post_object, $this->public_options);
    }

    /**
     * Return the processed post/page title.
     *
     * @since   3.0.0
     * @access  private
     * @param   object   $post_object
     * @param   integer  $post_id
     * @return  string
     */
    private function get_title(\stdClass $post_object, int $post_id)
    {
        $title = '';

        if ( $post_object->id != $post_id ) {
            $title = get_the_title($post_id);
        } else {
            $title = $post_object->title;
        }

        // Run the_title filter so core/plugin title hooks can
        // be applied to the post title
        $title = apply_filters('the_title', $title, $post_object->id);

        return apply_filters('wpp_the_title', $title, $post_object->id, $post_id);
    }

    /**
     * Return the permalink.
     * 
     * @since   4.0.12
     * @access  private
     * @param   object   $post_object
     * @param   integer  $post_id
     * @return  string
     */
    private function get_permalink(\stdClass $post_object, int $post_id) {
        if ( $post_object->id != $post_id ) {
            return get_permalink($post_id);
        }

        return get_permalink($post_object->id);
    }

    /**
     * Return the processed thumbnail.
     *
     * @since   3.0.0
     * @access  private
     * @param   int     $post_id
     * @return  string
     */
    private function get_thumbnail(int $post_id)
    {
        $thumbnail = '';

        if ( $this->public_options['thumbnail']['active'] ) {
            $thumbnail = $this->thumbnail->get(
                $post_id,
                [
                    $this->public_options['thumbnail']['width'],
                    $this->public_options['thumbnail']['height']
                ],
                $this->admin_options['tools']['thumbnail']['source'],
                $this->public_options['thumbnail']['crop'],
                $this->public_options['thumbnail']['build']
            );
        }

        return $thumbnail;
    }

    /**
     * Return post excerpt.
     *
     * @since   3.0.0
     * @access  private
     * @param   object  $post_object
     * @param   integer $post_id
     * @return  string
     */
    private function get_excerpt(\stdClass $post_object, int $post_id)
    {
        $excerpt = '';

        if ( $this->public_options['post-excerpt']['active'] ) {

            if ( $post_object->id != $post_id ) {
                $the_post = get_post($post_id);

                $excerpt = ( empty($the_post->post_excerpt) )
                  ? $the_post->post_content
                  : $the_post->post_excerpt;
            }
            else {
                $excerpt = ( empty($post_object->post_excerpt) )
                  ? $post_object->post_content
                  : $post_object->post_excerpt;
            }

            // remove caption tags
            $excerpt = preg_replace('/\[caption.*\[\/caption\]/', '', $excerpt);

            // remove Flash objects
            $excerpt = preg_replace("/<object[0-9 a-z_?*=\":\-\/\.#\,\\n\\r\\t]+/smi", '', $excerpt);

            // remove iframes
            $excerpt = preg_replace('/<iframe.*?\/iframe>/i', '', $excerpt);

            // remove WP shortcodes
            $excerpt = strip_shortcodes($excerpt);

            // remove style/script tags
            $excerpt = preg_replace('@<(script|style)[^>]*?>.*?</\\1>@si', '', $excerpt);

            // remove blocks that are not appropriate for the excerpt
            $excerpt = excerpt_remove_blocks($excerpt);

            // remove HTML tags if requested
            if ( $this->public_options['post-excerpt']['keep_format'] ) {
                $excerpt = wp_kses(
                    $excerpt,
                    [
                        'a' => [
                            'href' => [],
                            'title' => []
                        ],
                        'em' => [],
                        'strong' => []
                    ]
                );
            } else {
                $excerpt = wp_kses($excerpt, []);

                // remove URLs, too
                $excerpt = preg_replace('_^(?:(?:https?|ftp)://)(?:\S+(?::\S*)?@)?(?:(?!10(?:\.\d{1,3}){3})(?!127(?:\.\d{1,3}){3})(?!169\.254(?:\.\d{1,3}){2})(?!192\.168(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\x{00a1}-\x{ffff}0-9]+-?)*[a-z\x{00a1}-\x{ffff}0-9]+)(?:\.(?:[a-z\x{00a1}-\x{ffff}0-9]+-?)*[a-z\x{00a1}-\x{ffff}0-9]+)*(?:\.(?:[a-z\x{00a1}-\x{ffff}]{2,})))(?::\d{2,5})?(?:/[^\s]*)?$_iuS', '', $excerpt);
            }

            // remove HTML comments
            $excerpt = preg_replace('/<!--.*?-->/ms', '', $excerpt);

            // remove extra whitespaces
            $excerpt = preg_replace('/\s\s+/', ' ', $excerpt);

            $excerpt = trim($excerpt);

        }

        // Balance tags, if needed
        if ( '' !== $excerpt ) {

            $more = $this->public_options['post-excerpt']['words'] ? ' ' . $this->more : $this->more;
            $more = apply_filters('wpp_excerpt_more', $more);
            $excerpt = Helper::truncate($excerpt, $this->public_options['post-excerpt']['length'], $this->public_options['post-excerpt']['words'], $more);

            if ( $this->public_options['post-excerpt']['keep_format'] ) {
                $excerpt = force_balance_tags($excerpt);
            }
        }

        return $excerpt;
    }

    /**
     * Return post rating.
     *
     * @since   3.0.0
     * @access  private
     * @param   object  $post_object
     * @return  string
     */
    private function get_rating(\stdClass $post_object)
    {
        $rating = '';

        if ( function_exists('the_ratings_results') && $this->public_options['rating'] ) {
            $rating = the_ratings_results($post_object->id);
        }

        return $rating;
    }

    /**
     * Get post date.
     *
     * @since   3.0.0
     * @access  private
     * @param   object  $post_object
     * @return  string
     */
    private function get_date(\stdClass $post_object)
    {
        $date = '';

        if ( $this->public_options['stats_tag']['date']['active'] ) {
            if ( 'relative' == $this->public_options['stats_tag']['date']['format'] ) {
                $date = sprintf(
                    __('%s ago', 'wordpress-popular-posts'),
                    human_time_diff(
                        strtotime($post_object->date),
                        Helper::timestamp()
                    )
                );
            } else {
                $date = date_i18n(
                    ( 'wp_date_format' == $this->public_options['stats_tag']['date']['format'] ? $this->wp_date_format : $this->public_options['stats_tag']['date']['format'] ),
                    strtotime($post_object->date)
                );
            }
        }

        return apply_filters('wpp_the_date', $date, $post_object->id);
    }

    /**
     * Get post taxonomies.
     *
     * @since   3.0.0
     * @access  private
     * @param   integer $post_id
     * @return  string
     */
    private function get_taxonomies(int $post_id)
    {
        $post_tax = '';

        if (
            (isset($this->public_options['stats_tag']['category']) && $this->public_options['stats_tag']['category']) 
            || $this->public_options['stats_tag']['taxonomy']['active']
        ) {

            $taxonomy = 'category';

            if (
                $this->public_options['stats_tag']['taxonomy']['active']
                && ! empty($this->public_options['stats_tag']['taxonomy']['name'])
            ) {
                $taxonomy = $this->public_options['stats_tag']['taxonomy']['name'];
            }

            $terms = wp_get_post_terms($post_id, $taxonomy);

            if ( ! is_wp_error($terms) ) {
                // Usage: https://wordpress.stackexchange.com/a/46824
                if ( has_filter('wpp_post_exclude_terms') ) {
                    $args = apply_filters('wpp_post_exclude_terms', []);
                    $terms = wp_list_filter($terms, $args, 'NOT');
                }

                $terms = apply_filters('wpp_post_terms', $terms);

                if (
                    is_array($terms) 
                    && ! empty($terms)
                ) {
                    $taxonomy_separator = esc_html(apply_filters('wpp_taxonomy_separator', ', '));

                    // We're going to use the taxonomy slug as a CSS class so let's escape it just in case
                    $taxonomy = esc_attr($taxonomy);

                    foreach ($terms as $term) {
                        $term_link = get_term_link($term);

                        if ( is_wp_error($term_link) ) {
                            continue;
                        }

                        $term_link = esc_url($this->translate->url($term_link, $this->translate->get_current_language()));
                        $post_tax .= "<a href=\"{$term_link}\" class=\"wpp-taxonomy {$taxonomy} {$taxonomy}-{$term->term_id}\">" . esc_html($term->name) . '</a>' . $taxonomy_separator;
                    }
                }
            }

            if ( '' != $post_tax ) {
                $post_tax = rtrim($post_tax, $taxonomy_separator);
            }

        }

        return $post_tax;
    }

    /**
     * Get post author.
     *
     * @since   3.0.0
     * @access  private
     * @param   object  $post_object
     * @param   integer $post_id
     * @return  string
     */
    private function get_author(\stdClass $post_object, int $post_id)
    {
        $author = ( $this->public_options['stats_tag']['author'] )
          ? get_the_author_meta('display_name', $post_object->uid != $post_id ? get_post_field('post_author', $post_id) : $post_object->uid)
          : '';

        return $author;
    }

    /**
     * Return post views count.
     *
     * @since   3.0.0
     * @access  private
     * @param   object  $post_object
     * @return  int|float
     */
    private function get_pageviews(\stdClass $post_object)
    {
        $pageviews = 0;

        if (
            (
                $this->public_options['order_by'] == 'views'
                || $this->public_options['order_by'] == 'avg'
                || $this->public_options['stats_tag']['views']
            )
            && ( isset($post_object->pageviews) || isset($post_object->avg_views) )
        ) {
            $pageviews = ( $this->public_options['order_by'] == 'views' || $this->public_options['order_by'] == 'comments' )
            ? $post_object->pageviews
            : $post_object->avg_views;
        }

        return $pageviews;
    }

    /**
     * Return post comment count.
     *
     * @since   3.0.0
     * @access  private
     * @param   object  $post_object
     * @return  int
     */
    private function get_comments(\stdClass $post_object)
    {
        $comments = ( ( $this->public_options['order_by'] == 'comments' || $this->public_options['stats_tag']['comment_count'] ) && isset($post_object->comment_count) )
          ? $post_object->comment_count
          : 0;

        return $comments;
    }

    /**
     * Return post metadata.
     *
     * @since   3.0.0
     * @access  private
     * @param   object  $post_object
     * @param   integer $post_id
     * @return  array
     */
    //private function get_metadata(\stdClass $post_object, $post_id)
    private function get_metadata(\stdClass $post_object, int $post_id, string $date, string $post_tax, string $author, $pageviews, int $comments) /** @TODO: starting PHP 8.0 $pageviews can be declared as mixed $pageviews */
    {
        $stats = [];

        $prettify_numbers = apply_filters('wpp_prettify_numbers', true);

        /**
         * @ToDo
         *
         * Remove this filter, it's a typo and the correct one has been around since 6.3.4
         *
         * @since  7.3.7
         */
        if ( has_filter('wpp_pretiffy_numbers') ) {
            $prettify_numbers = apply_filters('wpp_pretiffy_numbers', true);

            if ( defined('WP_DEBUG') && WP_DEBUG ) {
                trigger_error('WP Popular Posts - wpp_pretiffy_numbers has been deprecated. Please use wpp_prettify_numbers instead..', E_USER_WARNING);
            }
        }

        // comments
        if ( $this->public_options['stats_tag']['comment_count'] ) {
            $comments_text = sprintf(
                _n('%s comment', '%s comments', $comments, 'wordpress-popular-posts'),
                $prettify_numbers ? Helper::prettify_number($comments) : number_format_i18n($comments)
            );

            $stats['comments'] = '<span class="wpp-comments">' . $comments_text . '</span>';
        }

        // views
        if ( $this->public_options['stats_tag']['views'] ) {
            if ( $this->public_options['order_by'] == 'avg' ) {
                $views_text = sprintf(
                    _n('%s view per day', '%s views per day', $pageviews, 'wordpress-popular-posts'),
                    $prettify_numbers ? Helper::prettify_number($pageviews, 2) : number_format_i18n($pageviews, (fmod($pageviews, 1) !== 0.0 ? 2 : 0))
                );
            }
            else {
                $views_text = sprintf(
                    _n('%s view', '%s views', $pageviews, 'wordpress-popular-posts'),
                    $prettify_numbers ? Helper::prettify_number($pageviews) : number_format_i18n($pageviews)
                );
            }

            $stats['views'] = '<span class="wpp-views">' . $views_text . '</span>';
        }

        // author
        if ( $this->public_options['stats_tag']['author'] ) {
            $author_url = get_author_posts_url($post_object->uid != $post_id ? get_post_field('post_author', $post_id) : $post_object->uid);
            $display_name = '<a href="' . esc_url($this->translate->url($author_url, $this->translate->get_current_language())) . '">' . esc_html($author) . '</a>';
            $stats['author'] = '<span class="wpp-author">' . sprintf(__('by %s', 'wordpress-popular-posts'), $display_name) . '</span>';
        }

        // date
        if ( $this->public_options['stats_tag']['date']['active'] ) {
            $stats['date'] = '<span class="wpp-date">' . ( 'relative' == $this->public_options['stats_tag']['date']['format'] ? sprintf(__('posted %s', 'wordpress-popular-posts'), $date) : sprintf(__('posted on %s', 'wordpress-popular-posts'), $date) ) . '</span>';
        }

        // taxonomy
        if ( ($this->public_options['stats_tag']['category'] || $this->public_options['stats_tag']['taxonomy']['active']) && $post_tax != '' ) {
            $stats['taxonomy'] = '<span class="wpp-category">' . sprintf(__('under %s', 'wordpress-popular-posts'), $post_tax) . '</span>';
        }

        return $stats;
    }

    /**
     * Parse content tags.
     *
     * @since   1.4.6
     * @access  private
     * @param   string  HTML string with content tags
     * @param   array   Post data
     * @param   bool    Used to display post rating (if functionality is available)
     * @return  string
     */
    private function format_content(string $string, array $data, bool $rating) {

        if ( empty($string) || ( empty($data) || ! is_array($data) ) ) {
            return false;
        }

        $params = [];
        $pattern = '/\{(pid|current_class|excerpt|summary|meta|stats|title|title_attr|image|thumb|thumb_img|thumb_url|rating|score|url|text_title|author|author_copy|author_name|author_url|taxonomy|taxonomy_copy|category|category_copy|views|views_copy|comments|comments_copy|date|date_copy|total_items|item_position)\}/i';
        preg_match_all($pattern, $string, $matches);

        array_map('strtolower', $matches[0]);

        if ( in_array('{pid}', $matches[0]) ) {
            $string = str_replace('{pid}', $data['id'], $string);
        }

        if ( in_array('{current_class}', $matches[0]) ) {
            $string = str_replace('{current_class}', ( $data['is_current_post'] ? 'current' : '' ), $string);
        }

        if ( in_array('{title}', $matches[0]) ) {
            $string = str_replace('{title}', $data['title'], $string);
        }

        if ( in_array('{title_attr}', $matches[0]) ) {
            $string = str_replace('{title_attr}', $data['title_attr'], $string);
        }

        if ( in_array('{meta}', $matches[0]) || in_array('{stats}', $matches[0]) ) {
            $string = str_replace(['{meta}', '{stats}'], $data['stats'], $string);
        }

        if ( in_array('{excerpt}', $matches[0]) || in_array('{summary}', $matches[0]) ) {
            $string = str_replace(['{excerpt}', '{summary}'], $data['summary'], $string);
        }

        if ( in_array('{image}', $matches[0]) || in_array('{thumb}', $matches[0]) ) {
            $string = str_replace(['{image}', '{thumb}'], $data['img'], $string);
        }

        if ( in_array('{thumb_img}', $matches[0]) ) {
            $string = str_replace('{thumb_img}', $data['img_no_link'], $string);
        }

        if ( in_array('{thumb_url}', $matches[0]) && ! empty($data['img_no_link']) ) {
            $dom = new \DOMDocument();

            if ( $dom->loadHTML($data['img_no_link']) ) {
                $img_tag = $dom->getElementsByTagName('img');

                if ( $img_tag->length ) {
                    foreach( $img_tag as $node ) {
                        if ( $node->hasAttribute('src') ) {
                            $src = $node->getAttribute('src');
                            $string = str_replace('{thumb_url}', $src, $string);
                        }
                    }
                }
            }
        }

        // WP-PostRatings check
        if ( $rating ) {
            if ( function_exists('the_ratings_results') && in_array('{rating}', $matches[0]) ) {
                $string = str_replace('{rating}', the_ratings_results($data['id']), $string);
            }

            if ( function_exists('expand_ratings_template') && in_array('{score}', $matches[0]) ) {
                $string = str_replace('{score}', expand_ratings_template('%RATINGS_SCORE%', $data['id']), $string);
                // removing the redundant plus sign
                $string = str_replace('+', '', $string);
            }
        }

        if ( in_array('{url}', $matches[0]) ) {
            $string = str_replace('{url}', $data['url'], $string);
        }

        if ( in_array('{text_title}', $matches[0]) ) {
            $string = str_replace('{text_title}', $data['text_title'], $string);
        }

        if ( in_array('{author}', $matches[0]) ) {
            $string = str_replace('{author}', $data['author'], $string);
        }

        if ( in_array('{author_copy}', $matches[0]) ) {
            $string = str_replace('{author_copy}', $data['author_copy'], $string);
        }

        if ( in_array('{author_name}', $matches[0]) ) {
            $string = str_replace('{author_name}', $data['author_name'], $string);
        }

        if ( in_array('{author_url}', $matches[0]) ) {
            $string = str_replace('{author_url}', $data['author_url'], $string);
        }

        if ( in_array('{taxonomy}', $matches[0]) || in_array('{category}', $matches[0]) ) {
            $string = str_replace(['{taxonomy}', '{category}'], $data['taxonomy'], $string);
        }

        if ( in_array('{taxonomy_copy}', $matches[0]) || in_array('{category_copy}', $matches[0]) ) {
            $string = str_replace(['{taxonomy_copy}', '{category_copy}'], $data['taxonomy_copy'], $string);
        }

        if ( in_array('{views}', $matches[0]) ) {
            $string = str_replace('{views}', $data['views'], $string);
        }

        if ( in_array('{views_copy}', $matches[0]) ) {
            $string = str_replace('{views_copy}', $data['views_copy'], $string);
        }

        if ( in_array('{comments}', $matches[0]) ) {
            $string = str_replace('{comments}', $data['comments'], $string);
        }

        if ( in_array('{comments_copy}', $matches[0]) ) {
            $string = str_replace('{comments_copy}', $data['comments_copy'], $string);
        }

        if ( in_array('{date}', $matches[0]) ) {
            $string = str_replace('{date}', $data['date'], $string);
        }

        if ( in_array('{date_copy}', $matches[0]) ) {
            $string = str_replace('{date_copy}', $data['date_copy'], $string);
        }

        if ( in_array('{total_items}', $matches[0]) ) {
            $string = str_replace('{total_items}', $data['total_items'], $string);
        }

        if ( in_array('{item_position}', $matches[0]) ) {
            $string = str_replace('{item_position}', $data['item_position'], $string);
        }

        return apply_filters('wpp_parse_custom_content_tags', $string, $data['id']);
    }

    /**
     * Checks whether we're currently seeing a single post/page/CPT.
     *
     * @since   5.0.0
     * @return  int
     */
    public function is_single()
    {
        return apply_filters('wpp_is_single', Helper::is_single());
    }
}
