<?php
/**
 * Shortcode registration for VMPlayer.
 *
 * Handles the [vmplayer] shortcode. Accepts an optional `station`
 * attribute (numeric index or ID slug) to force a specific preset
 * on a specific page. Marks the shortcode as used so the public
 * class knows to enqueue frontend assets, then renders the player
 * template.
 *
 * @package VMPlayer
 */

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

/**
 * Class VMPlayer_Shortcode
 */
class VMPlayer_Shortcode {

    /**
     * Shortcode tag.
     */
    const TAG = 'vmplayer';

    /**
     * Alias shortcode for the compact variant. Some page builders
     * (Elementor, WPBakery) run content through texturize filters
     * that convert straight quotes in attributes to curly typographic
     * quotes, which breaks the shortcode attribute parser and drops
     * the variant value silently. Users on those environments can
     * fall back to [vmplayer_compact] as a bulletproof way to force
     * the compact layout with no attribute parsing required.
     */
    const TAG_COMPACT_ALIAS = 'vmplayer_compact';

    /**
     * Register the shortcode with WordPress.
     *
     * @return void
     */
    public function register() {
        add_shortcode( self::TAG, array( $this, 'render' ) );
        add_shortcode( self::TAG_COMPACT_ALIAS, array( $this, 'render_compact' ) );

        // Prevent wptexturize from converting straight quotes in
        // shortcode attributes to curly quotes. Without this filter,
        // page builders that apply texturize between the editor and
        // do_shortcode can turn variant="compact" into a form the
        // shortcode parser cannot read, causing the variant attribute
        // to fall through as an empty string.
        add_filter( 'no_texturize_shortcodes', array( $this, 'protect_from_texturize' ) );
    }

    /**
     * Add VMPlayer shortcodes to the texturize skip list. Wp core
     * calls this filter with a running array; we append and return.
     *
     * @param array $tags Existing shortcode tags to skip.
     * @return array
     */
    public function protect_from_texturize( $tags ) {
        if ( ! is_array( $tags ) ) {
            $tags = array();
        }
        $tags[] = self::TAG;
        $tags[] = self::TAG_COMPACT_ALIAS;
        return $tags;
    }

    /**
     * Alias render entry that always forces the compact variant.
     * Simpler than the main render because there is no variant
     * decision to make: the tag itself is the intent.
     *
     * @param array $atts Shortcode attributes (only station respected).
     * @return string HTML output.
     */
    public function render_compact( $atts ) {
        if ( ! is_array( $atts ) ) {
            $atts = array();
        }
        $atts['variant'] = 'compact';
        return $this->render( $atts );
    }

    /**
     * Render the shortcode.
     *
     * @param array $atts Shortcode attributes.
     * @return string HTML output.
     */
    public function render( $atts ) {
        $atts = shortcode_atts(
            array(
                'station' => '',
                // Variant selector. "main" (default) is the full
                // landscape player. "compact" is the smaller two-row
                // card layout for sidebars, footers, and other tight
                // placements. Unknown values fall back to main so a
                // typo does not break the site.
                'variant' => 'main',
            ),
            $atts,
            self::TAG
        );

        $variant = isset( $atts['variant'] ) ? (string) $atts['variant'] : '';

        // Strip curly quotes that some page builders (Elementor,
        // WPBakery) inject when they texturize content before running
        // do_shortcode. Without this cleanup, variant="compact" can
        // arrive here as ["]compact["] with typographic quotes as
        // part of the string value, which fails strict comparison
        // against 'compact' and falls back to the main variant.
        //
        // Two forms of typographic quotes need handling:
        //
        //   1. Raw UTF-8 byte form (E2 80 9C, E2 80 9D, etc). This
        //      is what WordPress's wptexturize outputs by default
        //      and what direct-write page builders inject.
        //
        //   2. HTML entity form (&#8221;, &ldquo;, and friends).
        //      This is what TinyMCE-based editors (Elementor's Text
        //      Editor widget among them) store when the WYSIWYG
        //      layer converts straight quotes to typographic ones
        //      on save. WordPress's shortcode parser does not
        //      decode entities before matching, so the entities
        //      arrive intact in the attribute value.
        //
        // html_entity_decode covers form 2 by turning the entities
        // back into raw bytes, and then str_replace strips both forms
        // in one pass. Straight quotes are also stripped in case a
        // page builder wrapped the value redundantly. Lowercasing and
        // trimming close out the case-insensitive attribute match.
        $variant = html_entity_decode( $variant, ENT_QUOTES | ENT_HTML5, 'UTF-8' );
        $variant = str_replace(
            array( "\xE2\x80\x9C", "\xE2\x80\x9D", "\xE2\x80\x98", "\xE2\x80\x99", '"', "'" ),
            '',
            $variant
        );
        $variant = strtolower( trim( $variant ) );
        if ( 'compact' !== $variant ) {
            $variant = 'main';
        }

        // Resolve the station to render. Falls back to default station
        // configured in admin when no explicit override is given.
        $station_index = $this->resolve_station_index( $atts['station'] );
        $stations      = VMPlayer_Settings::get_stations();

        // Mark the shortcode as used so public::enqueue_assets_if_needed
        // will run the frontend CSS/JS in wp_footer.
        VMPlayer_Public::mark_shortcode_used();

        // No stations configured. Show a helpful notice to admins,
        // stay silent for visitors.
        if ( empty( $stations ) ) {
            return $this->render_no_stations_notice();
        }

        // Requested station not found. Same treatment.
        if ( ! isset( $stations[ $station_index ] ) ) {
            return $this->render_no_stations_notice();
        }

        ob_start();
        $active_station = $stations[ $station_index ];
        $active_index   = $station_index;

        // Fill in the packaged default logo when the admin did not
        // upload one. This keeps the template's logo rendering path
        // simple and gives listeners a proper branded fallback
        // instead of the abstract SVG icon we used to show.
        if ( empty( $active_station['logo_url'] ) ) {
            $active_station['logo_url'] = VMPlayer_Public::get_default_logo_url();
        }
        // Apply the same fill-in to every entry the template exposes
        // to the frontend so the JS station-switch path also lands
        // on a proper logo when the admin left one blank.
        foreach ( $stations as $key => $station_row ) {
            if ( is_array( $station_row ) && empty( $station_row['logo_url'] ) ) {
                $stations[ $key ]['logo_url'] = VMPlayer_Public::get_default_logo_url();
            }
        }

        // Expand social handles into full profile URLs so the template
        // can drop them straight into hrefs. Handles are the storage
        // format; URLs are the render format.
        if ( ! empty( $active_station['social'] ) && is_array( $active_station['social'] ) ) {
            $expanded = array();
            foreach ( $active_station['social'] as $platform => $handle ) {
                $url = VMPlayer_Settings::build_social_url( $platform, $handle );
                if ( '' !== $url ) {
                    $expanded[ $platform ] = $url;
                }
            }
            $active_station['social'] = $expanded;
        }

        $template_file = 'compact' === $variant
            ? 'templates/player-compact.php'
            : 'templates/player.php';

        include VMPLAYER_PLUGIN_DIR . $template_file;
        return (string) ob_get_clean();
    }

    /**
     * Turn a shortcode station attribute into a numeric index.
     * Accepts a numeric string (used as index directly) or a slug
     * (looked up against station IDs). Falls back to the admin
     * default station on empty or unresolved input.
     *
     * @param string $raw Raw attribute value.
     * @return int
     */
    private function resolve_station_index( $raw ) {
        $raw = (string) $raw;

        // Same defensive handling as the variant attribute: decode
        // HTML entities in case the value came through a TinyMCE-style
        // WYSIWYG that stored typographic quotes as entities, then
        // strip both raw curly quote bytes and any straight quotes so
        // the slug or numeric index arrives clean. Lets a shortcode
        // like [vmplayer station="my-slug"] work even when the page
        // builder texturized the attribute quotes.
        $raw = html_entity_decode( $raw, ENT_QUOTES | ENT_HTML5, 'UTF-8' );
        $raw = str_replace(
            array( "\xE2\x80\x9C", "\xE2\x80\x9D", "\xE2\x80\x98", "\xE2\x80\x99", '"', "'" ),
            '',
            $raw
        );
        $raw = trim( $raw );

        if ( '' === $raw ) {
            $settings = VMPlayer_Settings::get_settings();
            return isset( $settings['default_station'] ) ? (int) $settings['default_station'] : 0;
        }

        // Numeric attribute: use as index directly.
        if ( ctype_digit( $raw ) ) {
            return (int) $raw;
        }

        // Slug attribute: look up by ID.
        $stations = VMPlayer_Settings::get_stations();
        foreach ( $stations as $index => $station ) {
            if ( isset( $station['id'] ) && $station['id'] === sanitize_title( $raw ) ) {
                return (int) $index;
            }
        }

        // No match. Fall back to default.
        $settings = VMPlayer_Settings::get_settings();
        return isset( $settings['default_station'] ) ? (int) $settings['default_station'] : 0;
    }

    /**
     * Render a notice explaining no stations are configured.
     * Only shown to users with manage_options capability so visitors
     * see nothing when the shortcode is misused.
     *
     * @return string HTML.
     */
    private function render_no_stations_notice() {
        if ( ! current_user_can( 'manage_options' ) ) {
            return '';
        }

        return sprintf(
            '<div class="vmplayer-notice-admin">%s <a href="%s">%s</a></div>',
            esc_html__( 'VMPlayer: no stations configured yet.', 'vmplayer' ),
            esc_url( admin_url( 'options-general.php?page=vmplayer&tab=stations' ) ),
            esc_html__( 'Add a station', 'vmplayer' )
        );
    }
}
