<?php
/**
 * Trending News block theme.
 *
 * All styling lives in stepfox-looks block attributes (`customId` +
 * `responsiveStyles`) on the blocks themselves, so every visual decision is a
 * control in the Site Editor. This file carries no styling — only theme
 * supports, the image sizes the templates reference by sizeSlug, and pattern
 * registration.
 *
 * @package TN
 * @since 3.0.0
 */

if (!defined('ABSPATH')) {
    exit;
}

function tn_theme_setup() {
    load_theme_textdomain('tn', get_template_directory() . '/languages');

    add_theme_support('automatic-feed-links');
    add_theme_support('title-tag');
    add_theme_support('post-thumbnails');
    add_theme_support('responsive-embeds');
    add_theme_support('align-wide');
    add_theme_support('html5', array('search-form', 'comment-form', 'comment-list', 'gallery', 'caption', 'style', 'script'));
    add_theme_support('custom-logo', array('height' => 75, 'width' => 370, 'flex-height' => true, 'flex-width' => true));
    add_theme_support('post-formats', array('video', 'aside'));

    // Crops the templates reference by sizeSlug. Kept identical to the classic
    // theme so the demo site's existing media renders at the intended sizes.
    add_image_size('slider-img', 802, 499, true);
    add_image_size('solo-slider-img', 636, 395, true);
    add_image_size('slider-thumb', 165, 99, true);
    add_image_size('feat-cat-thumb', 305, 191, true);
    add_image_size('small-thumb', 75, 75, true);
    add_image_size('tiny-thumb', 30, 30, true);
    add_image_size('carousel-thumb', 141, 141, true);
    add_image_size('tv-widget-image', 110, 110, true);
    add_image_size('review-img', 230, 160, true);
}
add_action('after_setup_theme', 'tn_theme_setup');

/**
 * Front-end behaviour only (hero rotation, carousel arrows). No stylesheet —
 * the script toggles inline display/scroll on blocks the templates define.
 */
function tn_front_scripts() {
    $path = get_template_directory() . '/assets/js/tn-front.js';
    // Version off the file's own mtime: the theme version never moves between
    // edits, so a static one leaves returning visitors on a cached script.
    $version = file_exists($path) ? (string) filemtime($path) : wp_get_theme()->get('Version');
    wp_enqueue_script(
        'tn-front',
        get_template_directory_uri() . '/assets/js/tn-front.js',
        array(),
        $version,
        true
    );
}
add_action('wp_enqueue_scripts', 'tn_front_scripts');

/**
 * Register the theme's sections as insertable patterns.
 */
function tn_register_patterns() {
    if (!function_exists('register_block_pattern_category')) {
        return;
    }
    register_block_pattern_category('tn', array('label' => __('Trending News', 'tn')));
}
add_action('init', 'tn_register_patterns');

/**
 * The class tokens on a parsed block, as an array.
 *
 * Every behaviour filter below is opted in from the templates with a marker
 * class (`tn-ago`, `tn-first-term`, …). Matching whole tokens rather than
 * substrings keeps `tn-first-term` from firing on a hypothetical
 * `tn-first-term-wide`.
 *
 * @param array $block Parsed block.
 * @return string[] Class tokens.
 */
function tn_block_classes($block) {
    if (empty($block['attrs']['className'])) {
        return array();
    }
    return preg_split('/\s+/', trim($block['attrs']['className']), -1, PREG_SPLIT_NO_EMPTY);
}

/**
 * True when the block carries any of the given marker classes.
 *
 * @param array    $block   Parsed block.
 * @param string[] $markers Marker class names.
 * @return bool
 */
function tn_block_has_class($block, $markers) {
    return (bool) array_intersect((array) $markers, tn_block_classes($block));
}

/**
 * The post a block is rendering for. Inside a Query Loop the block context is
 * authoritative; outside one the global post is.
 *
 * @param WP_Block|null $instance Block instance, when the filter received one.
 * @return int Post ID, or 0.
 */
function tn_block_post_id($instance = null) {
    if ($instance instanceof WP_Block && isset($instance->context['postId'])) {
        return (int) $instance->context['postId'];
    }
    return (int) get_the_ID();
}

/**
 * Replace the text inside the first tag of $tag in $html, leaving the opening
 * tag — and therefore the `id="block_…"` stepfox-looks already injected on the
 * earlier generic `render_block` filter — completely untouched.
 *
 * @param string $html        Rendered block HTML.
 * @param string $tag         Tag name, e.g. `p`.
 * @param string $replacement Replacement inner HTML (already escaped).
 * @return string
 */
function tn_replace_inner_html($html, $tag, $replacement) {
    return preg_replace_callback(
        '#(<' . $tag . '\b[^>]*>)(.*?)(</' . $tag . '>)#s',
        function ($m) use ($replacement) {
            return $m[1] . $replacement . $m[3];
        },
        $html,
        1
    );
}

/**
 * Card title truncation, as the classic theme did server-side (it printed
 * titles through a character limit so card rows kept a predictable height).
 * Opt in with the class `tn-trunc-NN`, e.g. `tn-trunc-40`. This shortens the
 * text the same way the original did — it does not hide or crop rendered
 * content with CSS.
 */
function tn_truncate_post_title($block_content, $block) {
    if (empty($block['attrs']['className'])
        || !preg_match('/tn-trunc-(\d+)/', $block['attrs']['className'], $m)) {
        return $block_content;
    }
    $limit = (int) $m[1];
    return preg_replace_callback(
        '#(<a\b[^>]*>)(.*?)(</a>)#s',
        function ($mm) use ($limit) {
            $text = trim(wp_strip_all_tags($mm[2]));
            if (mb_strlen($text) <= $limit) {
                return $mm[0];
            }
            return $mm[1] . esc_html(rtrim(mb_substr($text, 0, $limit))) . '…' . $mm[3];
        },
        $block_content,
        1
    );
}
add_filter('render_block_core/post-title', 'tn_truncate_post_title', 10, 2);

/**
 * Relative post dates ("Posted 13 years ago"), as the classic theme printed them
 * in list and sidebar rows. Behaviour only — opt a core/post-date block in by
 * giving it the class `tn-ago`; the styling stays in the block's attributes.
 */
function tn_relative_post_date($block_content, $block) {
    if (empty($block['attrs']['className']) || strpos($block['attrs']['className'], 'tn-ago') === false) {
        return $block_content;
    }
    $post_id = get_the_ID();
    if (!$post_id) {
        return $block_content;
    }
    $ago = human_time_diff(get_post_time('U', true, $post_id), current_time('timestamp', true));
    /* translators: %s: human-readable time difference, e.g. "13 years" */
    $text = sprintf(__('Posted %s ago', 'tn'), $ago);
    return preg_replace('#(<time[^>]*>).*?(</time>)#s', '$1' . esc_html($text) . '$2', $block_content, 1);
}
add_filter('render_block_core/post-date', 'tn_relative_post_date', 10, 2);

/**
 * Only the first term, as the classic printed (`the_category()` echoing
 * `$category[0]`). Opt in with the class `tn-first-term` (or the older
 * `tn-firstcat`). Rebuilding the wrapper's contents from the first link drops
 * the separator spans as a side effect, so no dangling separator can survive.
 */
function tn_first_category_only($block_content, $block) {
    if (!tn_block_has_class($block, array('tn-first-term', 'tn-firstcat'))) {
        return $block_content;
    }
    if (!preg_match('#<a\b[^>]*>.*?</a>#s', $block_content, $m)) {
        return $block_content;
    }
    $first = str_replace('$', '\\$', $m[0]);
    return preg_replace('#(<div\b[^>]*>).*(</div>\s*)$#s', '$1' . $first . '$2', $block_content, 1);
}
add_filter('render_block_core/post-terms', 'tn_first_category_only', 10, 2);

/**
 * The classic printed the `tn_sub_title` post meta as the subtitle, falling back
 * to the excerpt. Opt in with the class `tn-subtitle`.
 */
function tn_subtitle_from_meta($block_content, $block, $instance = null) {
    if (!tn_block_has_class($block, array('tn-subtitle'))) {
        return $block_content;
    }
    $post_id = tn_block_post_id($instance);
    if (!$post_id) {
        return $block_content;
    }
    $sub = get_post_meta($post_id, 'tn_sub_title', true);
    if ('' === trim((string) $sub)) {
        return $block_content; // classic falls back to the trimmed excerpt
    }
    // A callback, not a replacement string: a subtitle containing `$1` must not
    // be read as a backreference. The classic echoed the meta unescaped.
    return preg_replace_callback(
        '#(<p class="wp-block-post-excerpt__excerpt">).*?(</p>)#s',
        function ($m) use ($sub) {
            return $m[1] . wp_kses_post($sub) . $m[2];
        },
        $block_content,
        1
    );
}
add_filter('render_block_core/post-excerpt', 'tn_subtitle_from_meta', 10, 3);

/**
 * "Related by tags" — restrict a query marked `tn-related` / `tn-related-tags`
 * to posts sharing a tag with the post being viewed, as inc/related.php did.
 *
 * The hook fires on core/post-template, so the marker class has to sit on the
 * post-template block (the templates put it on both it and its parent query).
 * When the post has no tags the classic widget printed nothing at all, so an
 * impossible `post__in` is used rather than silently falling back to "latest".
 */
function tn_related_by_tags($query, $block) {
    $cls = isset($block->parsed_block['attrs']['className']) ? $block->parsed_block['attrs']['className'] : '';
    if (strpos($cls, 'tn-related') === false || !is_singular('post')) {
        return $query;
    }
    $post_id = get_queried_object_id();
    $tags = wp_get_post_terms($post_id, 'post_tag', array('fields' => 'ids'));
    if (empty($tags) || is_wp_error($tags)) {
        $query['post__in'] = array(0);
        return $query;
    }
    $query['tag__in'] = $tags;
    $query['post__not_in'] = array($post_id);
    $query['ignore_sticky_posts'] = 1;
    return $query;
}
add_filter('query_loop_block_query_vars', 'tn_related_by_tags', 10, 2);

/**
 * The live date in the top bar, as header.php printed with `date('F j')`.
 * Opt in with the class `tn-today-date`; the literal in the template is then
 * only an editor placeholder.
 */
function tn_today_date_block($block_content, $block) {
    if (!tn_block_has_class($block, array('tn-today-date'))) {
        return $block_content;
    }
    return tn_replace_inner_html($block_content, 'p', esc_html(date_i18n('F j')));
}
add_filter('render_block_core/paragraph', 'tn_today_date_block', 10, 2);

/**
 * The review score badge feat-category-square.php printed inside `.square-image`
 * from the `tn_review_total` meta. Opt in with the class `tn-review-score`;
 * posts without a score print nothing at all, exactly as the classic did.
 */
function tn_review_score_badge($block_content, $block, $instance = null) {
    if (!tn_block_has_class($block, array('tn-review-score'))) {
        return $block_content;
    }
    $post_id = tn_block_post_id($instance);
    $score = $post_id ? get_post_meta($post_id, 'tn_review_total', true) : '';
    if ('' === trim((string) $score)) {
        return '';
    }
    return tn_replace_inner_html($block_content, 'p', esc_html((string) $score));
}
add_filter('render_block_core/paragraph', 'tn_review_score_badge', 10, 3);

/**
 * The view counter the classic meta rows printed with `get_views()` from the
 * `post_views_count` meta. Opt in with the class `tn-views`.
 */
function tn_views_count($block_content, $block, $instance = null) {
    if (!tn_block_has_class($block, array('tn-views'))) {
        return $block_content;
    }
    $post_id = tn_block_post_id($instance);
    if (!$post_id) {
        return $block_content;
    }
    $count = get_post_meta($post_id, 'post_views_count', true);
    if ('' === trim((string) $count)) {
        $count = '0';
    }
    return tn_replace_inner_html($block_content, 'p', esc_html(number_format_i18n((int) $count)));
}
add_filter('render_block_core/paragraph', 'tn_views_count', 10, 3);

/**
 * The play badge the classic overlaid on video-format thumbnails only
 * (`if ('video' == get_post_format())`). Opt in with the class `tn-video-only`:
 * the block renders on video posts and disappears on every other format.
 */
function tn_video_only_block($block_content, $block, $instance = null) {
    if (!tn_block_has_class($block, array('tn-video-only'))) {
        return $block_content;
    }
    $post_id = tn_block_post_id($instance);
    if (!$post_id || 'video' !== get_post_format($post_id)) {
        return '';
    }
    return $block_content;
}
add_filter('render_block_core/paragraph', 'tn_video_only_block', 10, 3);

/**
 * Classic comments.php always printed a comment count line, including
 * "No Comment" at zero comments (options tn_comments_no_comment /
 * tn_comments_one_comment / tn_comments_number_comments). core/comments-title
 * renders nothing at zero, so the line is restored here — with the stepfox id
 * carried by hand, because the plugin injects ids on the earlier generic
 * `render_block` filter and cannot see markup created in this one.
 */
function tn_comments_title_classic($block_content, $block) {
    if (!tn_block_has_class($block, array('tn-commentcount'))) {
        return $block_content;
    }
    $count = (int) get_comments_number();
    if (0 === $count) {
        $text = __('No Comment', 'tn');
    } elseif (1 === $count) {
        $text = __('One Comment', 'tn');
    } else {
        /* translators: %s: number of comments. */
        $text = sprintf(__('%s Comments on this post.', 'tn'), number_format_i18n($count));
    }
    if (trim($block_content) !== '') {
        return tn_replace_inner_html($block_content, 'h2', esc_html($text));
    }
    $id = !empty($block['attrs']['customId'])
        ? ' id="block_' . esc_attr($block['attrs']['customId']) . '"'
        : '';
    return '<h2' . $id . ' class="wp-block-comments-title tn-commentcount">' . esc_html($text) . '</h2>';
}
add_filter('render_block_core/comments-title', 'tn_comments_title_classic', 10, 2);

/**
 * The oEmbed player the classic printed for a post. Returns '' when the post
 * has no video link so the caller can fall back to the featured image.
 *
 * @param int    $post_id Post ID.
 * @param int    $width   oEmbed request width, as the classic passed it.
 * @param string $style   Inline style forced onto the iframe, reproducing the
 *                        classic stylesheet's sizing of that same iframe.
 * @return string
 */
function tn_video_embed_html($post_id, $width, $style) {
    $url = $post_id ? get_post_meta($post_id, 'trendingnews_video_link', true) : '';
    if (!$url) {
        return '';
    }
    $embed = wp_oembed_get($url, array('width' => $width));
    if (!$embed) {
        return '';
    }
    return preg_replace_callback(
        '#<iframe\b([^>]*)>#i',
        function ($m) use ($style) {
            $attrs = preg_replace('/\sstyle="[^"]*"/i', '', $m[1]);
            return '<iframe' . $attrs . ' style="' . esc_attr($style) . '">';
        },
        $embed,
        1
    );
}

/**
 * single.php swapped the featured image for the oEmbed player on video-format
 * posts (`elseif ('video' == get_post_format())`). Opt in with the class
 * `tn-media`; the customId the single template ships with is also accepted so
 * the behaviour does not depend on that template carrying the marker.
 */
function tn_video_format_media($block_content, $block, $instance = null) {
    $opted_in = tn_block_has_class($block, array('tn-media'))
        || (isset($block['attrs']['customId']) && 'sg-media' === $block['attrs']['customId']);
    if (!$opted_in) {
        return $block_content;
    }
    $post_id = tn_block_post_id($instance);
    if (!$post_id || 'video' !== get_post_format($post_id)) {
        return $block_content;
    }
    // Classic: wp_oembed_get(..., array('width' => 636)) inside .video-wrapper,
    // which the desktop stylesheet leaves at the oEmbed's own 636x358.
    $embed = tn_video_embed_html($post_id, 636, 'display:block;width:100%;max-width:100%;height:358px;border:0;');
    if (!$embed) {
        return $block_content;
    }
    $replaced = tn_replace_inner_html($block_content, 'figure', $embed);
    if (!$replaced) {
        return $block_content;
    }
    // The classic printed no image box at all on a video post, so the figure
    // must hug the player instead of keeping the 636x395 photo crop core wrote
    // into its inline style — otherwise a 37px gap opens under the video.
    return preg_replace_callback(
        '#<figure\b[^>]*>#i',
        function ($m) {
            $tag = preg_replace('#aspect-ratio\s*:\s*[^;"]*;?#i', 'aspect-ratio:636/358;', $m[0], 1);
            return preg_replace('#(?<![-\w])height\s*:\s*[^;"]*;?#i', 'height:358px;', $tag, 1);
        },
        $replaced,
        1
    );
}
add_filter('render_block_core/post-featured-image', 'tn_video_format_media', 10, 3);

/**
 * The Theme TV page player: taxonomy-post_format-post-format-video.php printed
 * `wp_oembed_get($url, array('width' => 968))` and TN/style.css:573 forced that
 * iframe to `height:597px`. Opt in with the class `tn-video-embed`; the TV
 * template's own customId is accepted too.
 */
function tn_video_embed_block($block_content, $block, $instance = null) {
    $opted_in = tn_block_has_class($block, array('tn-video-embed'))
        || (isset($block['attrs']['customId']) && 'tv-player' === $block['attrs']['customId']);
    if (!$opted_in) {
        return $block_content;
    }
    $post_id = tn_block_post_id($instance);
    $embed = tn_video_embed_html($post_id, 968, 'display:block;width:100%;max-width:100%;height:597px;border:0;');
    if (!$embed) {
        return $block_content;
    }
    // Keep the outer <div> — it carries the id stepfox-looks already injected.
    $replaced = preg_replace_callback(
        '#(<div\b[^>]*>).*(</div>)\s*$#s',
        function ($m) use ($embed) {
            return $m[1] . $embed . $m[2];
        },
        $block_content,
        1
    );
    return $replaced ? $replaced : $block_content;
}
add_filter('render_block_core/group', 'tn_video_embed_block', 10, 3);

/**
 * Make the Theme TV share row actually share, as the classic `.share-tv` links
 * did. Opt in per icon with `tn-share-<service>`; the service in the class is
 * what selects the endpoint, so the block's own `url` attribute is irrelevant.
 */
function tn_tv_share_links($block_content, $block, $instance = null) {
    $classes = tn_block_classes($block);
    $service = '';
    foreach ($classes as $class) {
        if (strpos($class, 'tn-share-') === 0) {
            $service = substr($class, strlen('tn-share-'));
            break;
        }
    }
    if ('' === $service) {
        return $block_content;
    }
    $post_id = tn_block_post_id($instance);
    if (!$post_id) {
        return $block_content;
    }
    $url = rawurlencode(get_permalink($post_id));
    $title = rawurlencode(get_the_title($post_id));
    $image = wp_get_attachment_image_src(get_post_thumbnail_id($post_id), 'slider-img');
    $media = $image ? rawurlencode($image[0]) : '';

    switch ($service) {
        case 'facebook':
            $href = 'https://www.facebook.com/sharer.php?u=' . $url;
            break;
        case 'twitter':
            $href = 'https://twitter.com/intent/tweet?url=' . $url . '&text=' . $title;
            break;
        case 'google':
            $href = 'https://plus.google.com/share?url=' . $url;
            break;
        case 'pinterest':
            $href = 'https://pinterest.com/pin/create/bookmarklet/?media=' . $media . '&url=' . $url . '&is_video=false&description=' . $title;
            break;
        case 'stumbleupon':
            $href = 'https://www.stumbleupon.com/submit?url=' . $url . '&title=' . $title;
            break;
        case 'reddit':
            $href = 'https://www.reddit.com/submit?url=' . $url . '&title=' . $title;
            break;
        default:
            return $block_content;
    }

    return preg_replace_callback(
        '#<a\b([^>]*)>#i',
        function ($m) use ($href) {
            $attrs = preg_replace('/\s(?:href|target|rel)="[^"]*"/i', '', $m[1]);
            return '<a' . $attrs . ' href="' . esc_url($href) . '" target="_blank" rel="noopener noreferrer">';
        },
        $block_content,
        1
    );
}
add_filter('render_block_core/social-link', 'tn_tv_share_links', 10, 3);
