<?php
/**
 * Forked from "Post Type Switcher" plugin v4.0.1.
 *
 * Original plugin:
 * - Name: Post Type Switcher
 * - Author: Triple J Software, Inc. (John James Jacoby)
 * - Plugin URI: https://wordpress.org/plugins/post-type-switcher/
 *
 * Notes about the ASE fork:
 * - Refactored to avoid global symbol conflicts (supports coexistence with the standalone plugin).
 * - Removes upstream public `pts_*` filters/actions.
 * - Uses `asenha_`-prefixed request params and AJAX action to avoid collisions.
 * - Add compatibility with ASE Pro's Admin Columns Manager, for scenarios where horizontal scroll is enabled. Ensures Quick Edit and Bulk Edit continue showing the Post Type selector
 * - Add alphabetical sort for post types list/selection
 * - Apply force hide for 'Type' admin column
 *
 * @package ASENHA
 */

namespace ASENHA\Post_Type_Switcher;

// Exit if accessed directly.
defined( 'ABSPATH' ) || exit;

/**
 * Post Type Switcher module.
 *
 * Feature parity with upstream Post Type Switcher v4.0.1:
 * - Post editor switcher UI
 * - Quick Edit and Bulk Edit switcher UI
 * - Block Editor integration
 * - WPML compatibility (updates translation group element types)
 */
final class Module {

	/**
	 * Singleton instance.
	 *
	 * @var Module|null
	 */
	private static $instance = null;

	/**
	 * Asset version, for cache busting.
	 *
	 * @var string
	 */
	private $asset_version = '';

	/**
	 * Get the singleton instance.
	 *
	 * @return Module
	 */
	public static function get_instance() {
		if ( null === self::$instance ) {
			self::$instance = new self();
		}

		return self::$instance;
	}

	/**
	 * Constructor.
	 */
	private function __construct() {
		$this->asset_version = defined( 'ASENHA_VERSION' ) ? ASENHA_VERSION : '1.0.0';
		add_action( 'init', array( $this, 'init' ) );
	}

	/**
	 * Initialization.
	 *
	 * @return void
	 */
	public function init() {
		add_action( 'admin_init', array( $this, 'admin_init' ) );
	}

	/**
	 * Admin initialization.
	 *
	 * @return void
	 */
	public function admin_init() {

		// Bail if page not allowed.
		if ( ! self::is_allowed_page() ) {
			return;
		}

		// Add column for quick-edit support.
		$post_type_names = $this->get_post_types( 'names' );

		// Add filters if post-types are switchable.
		if ( ! empty( $post_type_names ) ) {
			foreach ( $post_type_names as $name ) {
				add_filter( "manage_{$name}_posts_columns", array( $this, 'add_column' ) );
				add_action( "manage_{$name}_posts_custom_column", array( $this, 'manage_column' ), 10, 2 );
			}
		}

		// Default to "post_type" column being hidden.
		add_filter( 'default_hidden_columns', array( $this, 'default_hidden_columns' ) );

		// Force-hide the "Type" column for the current user while this module is enabled.
		add_filter( 'hidden_columns', array( $this, 'force_hide_type_column' ), 10, 2 );

		// One-time (lazy per user) update of Screen Options preference.
		add_action( 'current_screen', array( $this, 'maybe_set_hidden_columns_preference' ), 99 );

		// Add UI to "Publish" metabox.
		add_action( 'admin_head', array( $this, 'admin_head' ) );
		add_action( 'post_submitbox_misc_actions', array( $this, 'metabox' ) );
		add_action( 'quick_edit_custom_box', array( $this, 'quick_edit' ) );
		add_action( 'bulk_edit_custom_box', array( $this, 'quick_edit_bulk' ) );
		add_action( 'admin_enqueue_scripts', array( $this, 'quick_edit_script' ) );

		// Add UI to the block editor.
		add_action( 'enqueue_block_editor_assets', array( $this, 'block_editor_assets' ) );

		// AJAX handler.
		add_action( 'wp_ajax_asenha_post_type_switcher', array( $this, 'handle_ajax' ) );

		// Maybe override type on admin-area inserts, when requested.
		add_filter( 'wp_insert_attachment_data', array( $this, 'override_type' ), 10, 2 );
		add_filter( 'wp_insert_post_data', array( $this, 'override_type' ), 10, 2 );

		// Compatibility hooks.
		add_action( 'asenha_post_type_switcher_after_switch', array( $this, 'wpml_sync_type' ), 10, 3 );
	}

	/**
	 * Output meta box fields on Edit Post screen.
	 *
	 * @return void
	 */
	public function metabox() {

		// Post types.
		$post_types = $this->get_post_types();
		$post_type  = get_post_type();
		$cpt_object = get_post_type_object( $post_type );

		// Bail if object does not exist or produces an error.
		if ( ! $cpt_object instanceof \WP_Post_Type ) {
			return;
		}

		// Force-add current post type if it's not in the list.
		// Ref: https://wordpress.org/support/topic/dont-show-for-non-public-post-types?replies=4#post-5849287
		if ( ! in_array( $cpt_object, $post_types, true ) ) {
			$post_types[ $post_type ] = $cpt_object;
		}

		// Ensure list is always alphabetical (A-Z) by singular label.
		$this->sort_post_types_by_label( $post_types );

		?>
		<div class="misc-pub-section misc-pub-section-last post-type-switcher asenha-post-type-switcher">
			<label for="asenha_pts_post_type_metabox"><?php esc_html_e( 'Post Type:', 'admin-site-enhancements' ); ?></label>
			<span id="asenha-post-type-display"><?php echo esc_html( $cpt_object->labels->singular_name ); ?></span>

			<?php if ( current_user_can( $cpt_object->cap->publish_posts ) ) : ?>

				<a href="#" id="asenha-edit-post-type-switcher" class="hide-if-no-js"><?php esc_html_e( 'Edit', 'admin-site-enhancements' ); ?></a>
				<div id="asenha-post-type-select">
					<select name="asenha_pts_post_type" id="asenha_pts_post_type_metabox">
						<?php
						foreach ( $post_types as $_post_type => $pt ) :
							if ( ! current_user_can( $pt->cap->publish_posts ) ) {
								continue;
							}
							?>
							<option value="<?php echo esc_attr( $pt->name ); ?>" <?php selected( $post_type, $_post_type ); ?>>
								<?php echo esc_html( $pt->labels->singular_name ); ?> (<?php echo esc_html( $pt->name ); ?>)
							</option>
							<?php
						endforeach;
						?>
					</select>
					<a href="#" id="asenha-save-post-type-switcher" class="hide-if-no-js button"><?php esc_html_e( 'OK', 'admin-site-enhancements' ); ?></a>
					<a href="#" id="asenha-cancel-post-type-switcher" class="hide-if-no-js"><?php esc_html_e( 'Cancel', 'admin-site-enhancements' ); ?></a>
				</div>

				<?php wp_nonce_field( 'asenha-post-type-selector', 'asenha_pts_nonce_select' ); ?>

			<?php endif; ?>

		</div>
		<?php
	}

	/**
	 * Add the post type column.
	 *
	 * @param array $columns Array of registered columns.
	 * @return array
	 */
	public function add_column( $columns = array() ) {
		if ( empty( $columns ) || ! is_array( $columns ) ) {
			$columns = array();
		}

		$new_column = array( 'post_type' => esc_html__( 'Type', 'admin-site-enhancements' ) );

		return array_merge( $columns, $new_column );
	}

	/**
	 * Add "post_type" column to array of hidden columns by default.
	 *
	 * @param array $hidden Array of hidden columns.
	 * @return array
	 */
	public function default_hidden_columns( $hidden = array() ) {
		if ( empty( $hidden ) || ! is_array( $hidden ) ) {
			$hidden = array();
		}

		$hidden[] = 'post_type';

		return $hidden;
	}

	/**
	 * Force-hide the "Type" column (column key: post_type) on relevant post list tables.
	 *
	 * This overrides the current user's Screen Options choice while the module is enabled.
	 *
	 * @param array     $hidden Array of hidden column slugs.
	 * @param \WP_Screen $screen Current screen.
	 * @return array
	 */
	public function force_hide_type_column( $hidden, $screen ) {
		if ( ! is_array( $hidden ) ) {
			$hidden = array();
		}

		if ( ! $this->is_target_list_table_screen( $screen ) ) {
			return $hidden;
		}

		if ( ! in_array( 'post_type', $hidden, true ) ) {
			$hidden[] = 'post_type';
		}

		return $hidden;
	}

	/**
	 * One-time (lazy per user) update: ensure the current user's Screen Options hide the Type column.
	 *
	 * @param \WP_Screen $screen Current screen.
	 * @return void
	 */
	public function maybe_set_hidden_columns_preference( $screen ) {
		if ( ! $this->is_target_list_table_screen( $screen ) ) {
			return;
		}

		$user_id = get_current_user_id();
		if ( empty( $user_id ) ) {
			return;
		}

		$option_key = 'manage' . $screen->id . 'columnshidden';
		$hidden     = get_user_option( $option_key, $user_id );

		if ( ! is_array( $hidden ) ) {
			$hidden = array();
		}

		if ( in_array( 'post_type', $hidden, true ) ) {
			return;
		}

		$hidden[] = 'post_type';

		// Store as a user option so Screen Options reflect the default hidden state.
		update_user_option( $user_id, $option_key, $hidden, true );
	}

	/**
	 * Whether the current WP_Admin screen is a post list table screen we target.
	 *
	 * @param \WP_Screen|null $screen Current screen.
	 * @return bool
	 */
	private function is_target_list_table_screen( $screen ) {
		if ( ! ( $screen instanceof \WP_Screen ) ) {
			return false;
		}

		// List tables for posts/pages/CPTs.
		if ( 'edit' !== $screen->base ) {
			return false;
		}

		if ( empty( $screen->id ) || 0 !== strpos( $screen->id, 'edit-' ) ) {
			return false;
		}

		$post_type = isset( $screen->post_type ) ? (string) $screen->post_type : '';
		if ( '' === $post_type ) {
			return false;
		}

		$allowed_post_types = $this->get_post_types( 'names' );
		return in_array( $post_type, $allowed_post_types, true );
	}

	/**
	 * Render the post type column.
	 *
	 * @param string $column_name Name of column.
	 * @param int    $post_id     ID of post.
	 * @return void
	 */
	public function manage_column( $column_name = '', $post_id = 0 ) {
		if ( 'post_type' !== $column_name ) {
			return;
		}

		$post_type = get_post_type_object( get_post_type( $post_id ) );
		if ( ! $post_type instanceof \WP_Post_Type ) {
			return;
		}

		?>
		<span data-post-type="<?php echo esc_attr( $post_type->name ); ?>"><?php echo esc_html( $post_type->labels->singular_name ); ?> (<?php echo esc_html( $post_type->name ); ?>)</span>
		<?php
	}

	/**
	 * Render Quick Edit UI.
	 *
	 * @param string $column_name Name of column.
	 * @return void
	 */
	public function quick_edit( $column_name = '' ) {
		if ( 'post_type' !== $column_name ) {
			return;
		}
		?>
		<div id="asenha_pts_quick_edit" class="inline-edit-group wp-clearfix">
			<label class="alignleft">
				<span class="title"><?php esc_html_e( 'Post Type', 'admin-site-enhancements' ); ?></span>
				<?php wp_nonce_field( 'asenha-post-type-selector', 'asenha_pts_nonce_select' ); ?>
				<?php $this->select_box(); ?>
			</label>
		</div>
		<?php
	}

	/**
	 * Render Bulk Edit UI.
	 *
	 * @param string $column_name Name of column.
	 * @return void
	 */
	public function quick_edit_bulk( $column_name = '' ) {
		if ( 'post_type' !== $column_name ) {
			return;
		}
		?>
		<label id="asenha_pts_bulk_edit" class="alignleft">
			<span class="title"><?php esc_html_e( 'Post Type', 'admin-site-enhancements' ); ?></span>
			<?php wp_nonce_field( 'asenha-post-type-selector', 'asenha_pts_nonce_select' ); ?>
			<?php $this->select_box( true ); ?>
		</label>
		<?php
	}

	/**
	 * Enqueue Quick Edit script.
	 *
	 * @param string $hook Current admin page hook.
	 * @return void
	 */
	public function quick_edit_script( $hook = '' ) {
		if ( 'edit.php' !== $hook ) {
			return;
		}

		wp_enqueue_script(
			'asenha-pts-quickedit',
			ASENHA_URL . 'includes/premium/post-type-switcher/assets/js/quickedit.js',
			array( 'jquery' ),
			$this->asset_version,
			true
		);
	}

	/**
	 * Enqueue modifications to the block editor.
	 *
	 * @return void
	 */
	public function block_editor_assets() {

		$current_post_type        = get_post_type();
		$current_post_type_object = get_post_type_object( $current_post_type );

		if ( ! $current_post_type_object instanceof \WP_Post_Type ) {
			return;
		}

		// Bail if current user cannot publish the current post type.
		if ( ! current_user_can( $current_post_type_object->cap->publish_posts ) ) {
			return;
		}

		// Get all switchable types.
		$switchable_types = $this->get_post_types();

		if ( empty( $switchable_types ) ) {
			return;
		}

		$available_post_types = array();

		foreach ( $switchable_types as $post_type ) {
			if ( ! current_user_can( $post_type->cap->publish_posts ) ) {
				continue;
			}

			$available_post_types[] = array(
				'value' => $post_type->name,
				'label' => $post_type->labels->singular_name . ' (' . $post_type->name . ')',
			);
		}

		// Ensure list is always alphabetical (A-Z) by label.
		usort(
			$available_post_types,
			static function( $a, $b ) {
				$a_label = isset( $a['label'] ) ? (string) $a['label'] : '';
				$b_label = isset( $b['label'] ) ? (string) $b['label'] : '';
				return strcasecmp( $a_label, $b_label );
			}
		);

		$change_url = add_query_arg(
			array(
				'action'                => 'asenha_post_type_switcher',
				'asenha_pts_nonce_select' => wp_create_nonce( 'asenha-post-type-selector' ),
				'post_id'               => get_the_ID(),
			),
			admin_url( 'admin-ajax.php' )
		);

		wp_enqueue_script(
			'asenha-pts-blockeditor',
			ASENHA_URL . 'includes/premium/post-type-switcher/assets/js/block.js',
			array( 'wp-components', 'wp-edit-post', 'wp-editor', 'wp-element', 'wp-i18n', 'wp-plugins' ),
			$this->asset_version,
			true
		);

		wp_localize_script(
			'asenha-pts-blockeditor',
			'asenhaPtsBlockEditor',
			array(
				'availablePostTypes'   => $available_post_types,
				'currentPostType'      => $current_post_type,
				'currentPostTypeLabel' => $current_post_type_object->labels->singular_name,
				'changeUrl'            => $change_url,
			)
		);
	}

	/**
	 * Output a post-type dropdown.
	 *
	 * @param bool $bulk Whether this is for Bulk Edit.
	 * @return void
	 */
	public function select_box( $bulk = false ) {
		$post_types = $this->get_post_types();
		$post_type  = get_post_type();
		$selected   = '';

		?>
		<select name="asenha_pts_post_type" class="asenha-pts-post-type-select">
			<?php if ( true === $bulk ) : ?>
				<option value="-1"><?php esc_html_e( '&mdash; No Change &mdash;', 'admin-site-enhancements' ); ?></option>
			<?php endif; ?>

			<?php foreach ( $post_types as $_post_type => $pt ) : ?>
				<?php
				if ( ! current_user_can( $pt->cap->publish_posts ) ) {
					continue;
				}

				if ( false === $bulk ) {
					$selected = selected( $post_type, $_post_type, false );
				}
				?>
				<option value="<?php echo esc_attr( $pt->name ); ?>" <?php echo $selected; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>>
					<?php echo esc_html( $pt->labels->singular_name ); ?> (<?php echo esc_html( $pt->name ); ?>)
				</option>
			<?php endforeach; ?>
		</select>
		<?php
	}

	/**
	 * Handle an admin-ajax request to change post types.
	 *
	 * Note that these use $_GET values specifically, to avoid collisions with
	 * upstream requests.
	 * 
	 * @return void
	 */
	public function handle_ajax() {

		if (
			empty( $_GET['asenha_pts_post_type'] )
			|| empty( $_GET['asenha_pts_nonce_select'] )
			|| empty( $_GET['post_id'] )
		) {
			wp_die( esc_html__( 'Missing data.', 'admin-site-enhancements' ) );
		}

		$post_id          = absint( $_GET['post_id'] );
		$post_type        = sanitize_key( $_GET['asenha_pts_post_type'] );
		$post_type_object = get_post_type_object( $post_type );

		if ( ! $post_type_object instanceof \WP_Post_Type ) {
			wp_die( esc_html__( 'Invalid post type.', 'admin-site-enhancements' ) );
		}

		if (
			! wp_verify_nonce( $_GET['asenha_pts_nonce_select'], 'asenha-post-type-selector' )
			|| ! current_user_can( 'edit_post', $post_id )
			|| ! current_user_can( $post_type_object->cap->publish_posts )
		) {
			wp_die( esc_html__( 'Sorry, you are not allowed to edit this post.', 'admin-site-enhancements' ) );
		}

		$original_post_type = get_post_type( $post_id );

		$this->set_post_type( $post_id, $post_type );

		wp_safe_redirect( get_edit_post_link( $post_id, 'raw' ) );
		exit;
	}

	/**
	 * Override post_type in wp_insert_post().
	 * 
	 * We do a bunch of sanity checks here, to make sure we're only changing the
	 * post type when the user explicitly intends to.
	 *
	 * - Not during autosave
	 * - Check nonce
	 * - Check user capabilities
	 * - Check $_POST input name
	 * - Check if revision or current post-type
	 * - Check new post-type exists
	 * - Check that user can publish posts of new type
	 *
	 * @param array $data    Sanitized post data.
	 * @param array $postarr Raw post data.
	 * @return array
	 */
	public function override_type( $data = array(), $postarr = array() ) {
		
		// Bail if form field is missing
		if ( empty( $_REQUEST['asenha_pts_post_type'] ) || empty( $_REQUEST['asenha_pts_nonce_select'] ) ) {
			return $data;
		}

		// Bail if no specific post ID is being saved
		if ( empty( $postarr['post_ID'] ) ) {
			return $data;
		}

		// Post type information
		$post_id          = absint( $postarr['post_ID'] );
		$post_type        = sanitize_key( $_REQUEST['asenha_pts_post_type'] );
		$post_type_object = get_post_type_object( $post_type );

		// Bail if empty post type
		if ( empty( $post_id ) || empty( $post_type ) || ! $post_type_object instanceof \WP_Post_Type ) {
			return $data;
		}

		// Bail if no change
		if ( $post_type === $data['post_type'] ) {
			return $data;
		}

		// Bail if posted ID to update does not match the post ID being changed.
		// Prevent child/related inserts from also being changed.
		// See: https://github.com/JJJ/post-type-switcher/issues/9
		if ( isset( $postarr['ID'] ) && $post_id !== absint( $postarr['ID'] ) ) {
			return $data;
		}

		// Bail if user cannot 'edit_post' on the current post ID
		if ( ! current_user_can( 'edit_post', $post_id ) ) {
			return $data;
		}

		// Bail if user cannot 'publish_posts' on the new type
		if ( ! current_user_can( $post_type_object->cap->publish_posts ) ) {
			return $data;
		}

		// Bail if nonce is invalid
		if ( ! wp_verify_nonce( $_REQUEST['asenha_pts_nonce_select'], 'asenha-post-type-selector' ) ) {
			return $data;
		}

		// Bail if autosave
		if ( isset( $postarr['ID'] ) && wp_is_post_autosave( $postarr['ID'] ) ) {
			return $data;
		}

		// Bail if revision
		if ( isset( $postarr['ID'] ) && wp_is_post_revision( $postarr['ID'] ) ) {
			return $data;
		}

		// Bail if it's a revision
		if ( isset( $postarr['post_type'] ) && in_array( $postarr['post_type'], array( $post_type, 'revision' ), true ) ) {
			return $data;
		}

		// Update post type
		$data['post_type'] = $post_type;

		return $data;
	}

	/**
	 * Switch post translations via WPML.
	 *
	 * @param string $post_type          The new post type.
	 * @param string $original_post_type The old post type (unused).
	 * @param int    $post_id            The post ID.
	 * @return void
	 */
	public function wpml_sync_type( $post_type, $original_post_type, $post_id ) {
		global $wpdb, $sitepress;

		if ( ! is_a( $sitepress, '\\SitePress' ) ) {
			return;
		}

		$post_type = sanitize_key( $post_type );

		// Retrieve the translation grouping ID.
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$trid = $wpdb->get_var(
			$wpdb->prepare(
				"SELECT trid FROM {$wpdb->prefix}icl_translations WHERE element_id = %d",
				$post_id
			)
		);

		if ( empty( $trid ) ) {
			return;
		}

		// Update translation grouping element types.
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$wpdb->update(
			$wpdb->prefix . 'icl_translations',
			array( 'element_type' => 'post_' . $post_type ),
			array( 'trid' => $trid )
		);

		// Retrieve other posts that are sibling translations.
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$translation_items = $wpdb->get_col(
			$wpdb->prepare(
				"SELECT element_id FROM {$wpdb->prefix}icl_translations WHERE trid = %d",
				$trid
			)
		);

		// Bail if no translation items
		if ( empty( $translation_items ) || ! is_array( $translation_items ) ) {
			return;
		}

		// Update post type of sibling translations
		foreach ( $translation_items as $_post_id ) {
			$this->set_post_type( absint( $_post_id ), $post_type );
		}
	}

	/**
	 * Add needed JS and CSS to admin header.
	 *
	 * @return void
	 */
	public function admin_head() {
		?>
		<script type="text/javascript">
			jQuery( document ).ready( function( $ ) {
				jQuery( '.misc-pub-section.curtime.misc-pub-section-last' ).removeClass( 'misc-pub-section-last' );
				jQuery( '#asenha-edit-post-type-switcher' ).on( 'click', function( e ) {
					jQuery( this ).hide();
					jQuery( '#asenha-post-type-select' ).slideDown();
					e.preventDefault();
				} );
				jQuery( '#asenha-save-post-type-switcher' ).on( 'click', function( e ) {
					jQuery( '#asenha-post-type-select' ).slideUp();
					jQuery( '#asenha-edit-post-type-switcher' ).show();
					jQuery( '#asenha-post-type-display' ).text( jQuery( '#asenha_pts_post_type_metabox :selected' ).text() );
					e.preventDefault();
				} );
				jQuery( '#asenha-cancel-post-type-switcher' ).on( 'click', function( e ) {
					jQuery( '#asenha-post-type-select' ).slideUp();
					jQuery( '#asenha-edit-post-type-switcher' ).show();
					e.preventDefault();
				} );
			} );
		</script>
		<style type="text/css">
			#wpbody-content .inline-edit-row .inline-edit-col-right .alignleft + .alignleft {
				float: right;
			}
			/*
			 * Admin Columns Manager compatibility:
			 * When horizontal scrolling is enabled, ASE wraps list tables in #list-table-wrapper and
			 * applies position: sticky to the first cell. That unintentionally affects the inline edit
			 * row (single <td colspan>) and can cause Quick Edit/Bulk Edit fields (incl. Post Type) to
			 * be shifted/clipped. Scope this override to inline edit rows only.
			 */
			#list-table-wrapper > table > tbody > tr.inline-edit-row > td,
			#list-table-wrapper > table > tbody > tr.inline-edit-row > td:first-of-type {
				position: static !important;
				left: auto !important;
				z-index: auto !important;
			}
			#asenha-post-type-select {
				line-height: 2.5em;
				margin-top: 3px;
				display: none;
			}
			#asenha-post-type-select select#asenha_pts_post_type_metabox {
				margin-right: 2px;
				margin-bottom: 6px;
			}
			#asenha-post-type-select a#asenha-save-post-type-switcher {
				vertical-align: middle;
				margin-right: 2px;
			}
			#asenha-post-type-display {
				font-weight: bold;
			}
			#post-body .asenha-post-type-switcher::before {
				content: '\f227';
				font: 400 20px/1 dashicons;
				speak: never;
				display: inline-block;
				padding: 0 2px 0 0;
				top: 0;
				left: -1px;
				position: relative;
				vertical-align: top;
				-webkit-font-smoothing: antialiased;
				-moz-osx-font-smoothing: grayscale;
				text-decoration: none !important;
				color: #888;
			}
			.wp-list-table .column-post_type {
				width: 10%;
			}
			.edit-post-post-type {
				display: flex;
				align-items: center;
				flex-direction: row;
				gap: 8px;
				justify-content: space-between;
				margin-top: -8px;
				width: 100%;
			}
			.edit-post-post-type span {
				display: inline-block;
				flex-shrink: 0;
				padding: 6px 0;
				width: 45%;
			}
			.components-button.edit-post-post-type__toggle {
				height: auto;
				text-align: left;
				white-space: normal;
				word-break: break-word;
			}
			.editor-post-type__dialog-fieldset {
				margin: 8px;
				min-width: 248px;
			}
			.editor-post-type__dialog-fieldset .editor-post-type__dialog-legend {
				line-height: 1.2;
				margin-top: 0;
				margin-bottom: 16px;
				color: rgb(30, 30, 30);
				font-size: 13px;
				font-weight: 600;
				display: block;
			}
			.editor-post-type__dialog-fieldset .editor-post-type__choice {
				margin: 8px;
				display: block;
			}
			.editor-post-type__dialog-fieldset .editor-post-type__choice:last-child {
				margin-bottom: 0;
			}
			.editor-post-type__dialog-fieldset .editor-post-type__dialog-radio[type=radio] {
				display: inline-block;
			}
			.editor-post-type__dialog-fieldset .editor-post-type__dialog-label {
				margin: -3px 0 0 8px;
				display: inline-block;
			}
		</style>
		<?php
	}

	/**
	 * Whether or not the current request should load the switcher.
	 *
	 * @return bool
	 */
	private static function is_allowed_page() {

		// Admin pages.
		if ( is_blog_admin() ) {
			$pages = array(
				'post.php',
				'post-new.php',
				'edit.php',
			);

			return (bool) in_array( $GLOBALS['pagenow'], $pages, true );
		}

		// AJAX requests (Quick Edit save, Block Editor change request).
		if ( wp_doing_ajax() && ! empty( $_REQUEST['action'] ) ) {
			return (bool) in_array( $_REQUEST['action'], array( 'inline-save', 'asenha_post_type_switcher' ), true );
		}

		return false;
	}

	/**
	 * Set the post_type for a given ID and stash original/previous types in meta.
	 *
	 * @param int    $post_id   Post ID.
	 * @param string $post_type New post type.
	 * @return int Number of updated rows (1 or 0).
	 */
	private function set_post_type( $post_id = 0, $post_type = '' ) {
		$current  = get_post_type( $post_id );
		$original = get_post_meta( $post_id, 'pts_original_type', true );
		$previous = get_post_meta( $post_id, 'pts_previous_type', true );

		// Try to set the post type
		$retval = set_post_type( $post_id, $post_type );

		// Maybe stash the current/previous in meta
		if ( ! empty( $retval ) ) {
			
			// Only add the original type once
			if ( empty( $original ) ) {
				add_post_meta( $post_id, 'pts_original_type', $current );
			
			// Only delete original meta after revert
			} elseif ( $post_type === $original ) {
				delete_post_meta( $post_id, 'pts_original_type' );
			}

			// Update the previous meta key
			if ( $post_type !== $previous ) {
				update_post_meta( $post_id, 'pts_previous_type', $current );
				
			// Only delete previous meta after revert
			} else {
				delete_post_meta( $post_id, 'pts_previous_type' );
			}
		}

		// Return (number of updated rows, 1 or 0)
		return $retval;
	}

	/**
	 * Get switchable post type objects, based on post type arguments.
	 *
	 * @param string $output Optional. 'names' or 'objects'. Default 'objects'.
	 * @return array
	 */
	private function get_post_types( $output = 'objects' ) {
		// Always start from objects so we can sort by labels.
		$types = get_post_types( $this->get_post_type_args(), 'objects' );

		// Attachments are intentionally excluded (upstream behavior as of 3.0.0+).
		if ( isset( $types['attachment'] ) ) {
			unset( $types['attachment'] );
		}

		// Exclude WP core post types
		if ( isset( $types['wp_navigation'] ) ) {
			unset( $types['wp_navigation'] );
		}
		if ( isset( $types['wp_block'] ) ) {
			unset( $types['wp_block'] );
		}

		// Exclude ASE post types
		if ( isset( $types['asenha_code_snippet'] ) ) {
			unset( $types['asenha_code_snippet'] );
		}
		if ( isset( $types['asenha_cpt'] ) ) {
			unset( $types['asenha_cpt'] );
		}
		if ( isset( $types['asenha_ctax'] ) ) {
			unset( $types['asenha_ctax'] );
		}
		if ( isset( $types['asenha_cfgroup'] ) ) {
			unset( $types['asenha_cfgroup'] );
		}
		if ( isset( $types['asenha_options_page'] ) ) {
			unset( $types['asenha_options_page'] );
		}
		if ( isset( $types['options_page_config'] ) ) {
			unset( $types['options_page_config'] );
		}
		if ( isset( $types['asenha_redirect'] ) ) {
			unset( $types['asenha_redirect'] );
		}
		if ( isset( $types['formbuilder-styles'] ) ) {
			unset( $types['formbuilder-styles'] );
		}

		$this->sort_post_types_by_label( $types );

		if ( 'names' === $output ) {
			return array_keys( $types );
		}

		if ( 'labels_names' === $output ) {
			$labels_names = array();
			foreach ( $types as $name => $object ) {
				$labels_names[] = $object->labels->singular_name . ' (' . $name . ')';
			}
			return $labels_names;
		}

		return (array) $types;
	}

	/**
	 * Sort a post types array A-Z by singular label (case-insensitive).
	 *
	 * @param array $post_types Associative array of post type objects keyed by slug.
	 * @return void
	 */
	private function sort_post_types_by_label( &$post_types ) {
		if ( empty( $post_types ) || ! is_array( $post_types ) ) {
			return;
		}

		uasort(
			$post_types,
			static function( $a, $b ) {
				$a_label = ( is_object( $a ) && isset( $a->labels->singular_name ) ) ? (string) $a->labels->singular_name : '';
				$b_label = ( is_object( $b ) && isset( $b->labels->singular_name ) ) ? (string) $b->labels->singular_name : '';
				return strcasecmp( $a_label, $b_label );
			}
		);
	}

	/**
	 * Get the arguments used to narrow down switchable post types.
	 *
	 * Include both public and non-public post types that have admin UI.
	 *
	 * Note: Upstream exposes these via `pts_post_type_filter`. ASE does not.
	 *
	 * @return array
	 */
	private function get_post_type_args() {
		return array(
			'show_ui' => true,
		);
	}
}

