<?php
/**
 * Storzen Module — Wishlist
 *
 * Cookie/session-based wishlist for guests, user-meta backed for logged-in
 * users. Guest wishlist automatically migrates into user meta on login.
 *
 * Storage:
 *   • Logged-in:  user_meta key `storzen_wishlist` (array of product IDs)
 *   • Guest:      cookie `storzen_wishlist` OR PHP session (configurable)
 *   • Cookie TTL: 30 days
 *
 * Hooks fired (for theme/integration):
 *   storzen_wishlist_added   — when a product is added (passes product_id)
 *   storzen_wishlist_removed — when removed
 *   storzen_wishlist_cleared — when cleared
 *
 * Settings honored:
 *   guest_storage, wishlist_page
 *   pro_shareable_link (fully implemented when Pro)
 *   pro_price_drop_email, pro_back_in_stock, pro_multiple_lists,
 *   pro_analytics, pro_reminder_days  (Pro stubs — toggles save, behavior
 *   pending future release)
 *
 * @package storzen
 * @since   1.5.2
 */

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

if ( ! class_exists( 'Storzen_Module_Wishlist' ) ) {

	class Storzen_Module_Wishlist {

		const META_KEY    = 'storzen_wishlist';
		const COOKIE_KEY  = 'storzen_wishlist';
		const SESSION_KEY = 'storzen_wishlist';
		const SHORTCODE   = 'storzen_wishlist';
		const AJAX_ADD    = 'storzen_wishlist_add';
		const AJAX_REMOVE = 'storzen_wishlist_remove';
		const AJAX_CLEAR  = 'storzen_wishlist_clear';
		const AJAX_COUNT  = 'storzen_wishlist_count';
		const NONCE       = 'storzen_wishlist_nonce';

		/** @var array|null Per-request settings cache */
		private $settings = null;

		public function __construct() {
			if ( ! function_exists( 'WC' ) ) {
				return;
			}

			// Assets.
			add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_assets' ) );

			// Heart button on shop cards + single product.
			// Default WC theme hooks.
			add_action( 'woocommerce_after_shop_loop_item_title', array( $this, 'render_button' ), 15 );
			add_action( 'woocommerce_single_product_summary',     array( $this, 'render_button' ), 36 );
			// Storzen-native hooks (imported templates).
			add_action( 'storzen_shop_card_overlay',          array( $this, 'render_button' ), 16, 1 );
			add_action( 'storzen_product_single_after_price', array( $this, 'render_button' ), 25, 1 );

			// Wishlist page rendering.
			add_shortcode( self::SHORTCODE, array( $this, 'shortcode_render_page' ) );
			add_filter( 'the_content', array( $this, 'maybe_inject_page' ), 9 );

			// AJAX endpoints.
			add_action( 'wp_ajax_'        . self::AJAX_ADD,    array( $this, 'ajax_add' ) );
			add_action( 'wp_ajax_nopriv_' . self::AJAX_ADD,    array( $this, 'ajax_add' ) );
			add_action( 'wp_ajax_'        . self::AJAX_REMOVE, array( $this, 'ajax_remove' ) );
			add_action( 'wp_ajax_nopriv_' . self::AJAX_REMOVE, array( $this, 'ajax_remove' ) );
			add_action( 'wp_ajax_'        . self::AJAX_CLEAR,  array( $this, 'ajax_clear' ) );
			add_action( 'wp_ajax_nopriv_' . self::AJAX_CLEAR,  array( $this, 'ajax_clear' ) );
			add_action( 'wp_ajax_'        . self::AJAX_COUNT,  array( $this, 'ajax_count' ) );
			add_action( 'wp_ajax_nopriv_' . self::AJAX_COUNT,  array( $this, 'ajax_count' ) );

			// Migrate guest wishlist into user meta when a customer logs in.
			add_action( 'wp_login', array( $this, 'migrate_guest_to_user' ), 10, 2 );

			// Counter shortcode — for theme integration in header / menu.
			add_shortcode( 'storzen_wishlist_count', array( $this, 'shortcode_count' ) );
		}

		// ─────────────────────────────────────────────────────────────────
		// Settings
		// ─────────────────────────────────────────────────────────────────

		private function get_settings() {
			if ( null !== $this->settings ) {
				return $this->settings;
			}

			$is_pro = defined( 'STORZEN_IS_PRO' ) && 'yes' === STORZEN_IS_PRO;

			$guest = (string) storzen_module_setting( 'wishlist', 'guest_storage' );
			if ( ! in_array( $guest, array( 'cookie', 'session' ), true ) ) {
				$guest = 'cookie';
			}

			$this->settings = array(
				'guest_storage'        => $guest,
				'wishlist_page'        => (int) storzen_module_setting( 'wishlist', 'wishlist_page' ),

				// Pro features
				'pro_shareable_link'   => $is_pro && (bool) storzen_module_setting( 'wishlist', 'pro_shareable_link' ),

				// Pro stubs — toggles save but no behavior wired up yet.
				'pro_price_drop_email' => $is_pro && (bool) storzen_module_setting( 'wishlist', 'pro_price_drop_email' ),
				'pro_back_in_stock'    => $is_pro && (bool) storzen_module_setting( 'wishlist', 'pro_back_in_stock' ),
				'pro_multiple_lists'   => $is_pro && (bool) storzen_module_setting( 'wishlist', 'pro_multiple_lists' ),
				'pro_analytics'        => $is_pro && (bool) storzen_module_setting( 'wishlist', 'pro_analytics' ),
				'pro_reminder_days'    => $is_pro ? (int) storzen_module_setting( 'wishlist', 'pro_reminder_days' ) : 0,

				'is_pro_active'        => $is_pro,
			);
			return $this->settings;
		}

		// ─────────────────────────────────────────────────────────────────
		// Storage layer — read + write
		// ─────────────────────────────────────────────────────────────────

		/**
		 * Get the current wishlist (for logged-in user OR guest).
		 *
		 * Pro: if 'share' query param is present and shareable links are enabled,
		 * decode it instead of reading the local list — lets users preview a
		 * shared wishlist without adding it to their own.
		 *
		 * @return int[] Product IDs
		 */
		public function get_list() {
			// Pro: shareable URL — preview a shared wishlist.
			$s = $this->get_settings();
			// phpcs:disable WordPress.Security.NonceVerification.Recommended -- Read-only preview of a shared wishlist from a public link; no state change requiring a nonce.
			if ( $s['pro_shareable_link'] && ! empty( $_GET['share'] ) ) {
				$decoded = $this->decode_shareable( sanitize_text_field( wp_unslash( $_GET['share'] ) ) );
				// phpcs:enable WordPress.Security.NonceVerification.Recommended
				if ( ! empty( $decoded ) ) {
					return $decoded;
				}
			}

			if ( is_user_logged_in() ) {
				return $this->get_user_list( get_current_user_id() );
			}
			return $this->get_guest_list();
		}

		/**
		 * Logged-in user's wishlist from user meta.
		 *
		 * @param int $user_id
		 * @return int[]
		 */
		private function get_user_list( $user_id ) {
			$raw = get_user_meta( (int) $user_id, self::META_KEY, true );
			if ( ! is_array( $raw ) ) {
				return array();
			}
			return array_values( array_unique( array_filter( array_map( 'absint', $raw ) ) ) );
		}

		/**
		 * Guest wishlist from cookie or session.
		 *
		 * @return int[]
		 */
		private function get_guest_list() {
			$s = $this->get_settings();

			if ( 'session' === $s['guest_storage'] ) {
				if ( function_exists( 'WC' ) && WC()->session ) {
					$raw = WC()->session->get( self::SESSION_KEY, array() );
					return is_array( $raw ) ? array_values( array_unique( array_filter( array_map( 'absint', $raw ) ) ) ) : array();
				}
				return array();
			}

			// Cookie mode (default).
			$raw = isset( $_COOKIE[ self::COOKIE_KEY ] )
				? sanitize_text_field( wp_unslash( $_COOKIE[ self::COOKIE_KEY ] ) )
				: '';
			if ( '' === $raw ) {
				return array();
			}
			$ids = array_filter( array_map( 'absint', explode( ',', $raw ) ) );
			$ids = array_values( array_unique( $ids ) );
			// Hard cap (defense against malicious cookies).
			return array_slice( $ids, 0, 50 );
		}

		/**
		 * Persist the wishlist (for current user/guest).
		 *
		 * @param int[] $ids
		 */
		private function set_list( array $ids ) {
			$ids = array_filter( array_map( 'absint', $ids ) );
			$ids = array_values( array_unique( $ids ) );
			$ids = array_slice( $ids, 0, 50 );

			if ( is_user_logged_in() ) {
				update_user_meta( get_current_user_id(), self::META_KEY, $ids );
				return;
			}

			$s = $this->get_settings();
			if ( 'session' === $s['guest_storage'] && function_exists( 'WC' ) && WC()->session ) {
				WC()->session->set( self::SESSION_KEY, $ids );
				return;
			}

			// Cookie mode.
			$value = implode( ',', $ids );
			if ( function_exists( 'wc_setcookie' ) ) {
				wc_setcookie( self::COOKIE_KEY, $value, time() + ( 30 * DAY_IN_SECONDS ) );
			} else {
				setcookie( self::COOKIE_KEY, $value, time() + ( 30 * DAY_IN_SECONDS ), defined( 'COOKIEPATH' ) ? COOKIEPATH : '/' );
			}
			$_COOKIE[ self::COOKIE_KEY ] = $value;
		}

		// ─────────────────────────────────────────────────────────────────
		// Guest → User migration on login
		// ─────────────────────────────────────────────────────────────────

		/**
		 * When a user logs in, merge their guest wishlist into their user meta
		 * wishlist (without dropping either side).
		 *
		 * @param string  $user_login
		 * @param WP_User $user
		 */
		public function migrate_guest_to_user( $user_login, $user = null ) {
			if ( ! $user instanceof WP_User ) {
				$user = get_user_by( 'login', $user_login );
				if ( ! $user ) {
					return;
				}
			}

			// Read guest list (before WP marks the user as logged in for this
			// process — depending on hook order, get_guest_list still applies).
			$guest_ids = $this->get_guest_list();
			if ( empty( $guest_ids ) ) {
				return;
			}

			$existing  = $this->get_user_list( $user->ID );
			$merged    = array_values( array_unique( array_merge( $existing, $guest_ids ) ) );
			$merged    = array_slice( $merged, 0, 50 );

			update_user_meta( $user->ID, self::META_KEY, $merged );

			// Clear the guest store.
			$s = $this->get_settings();
			if ( 'session' === $s['guest_storage'] && function_exists( 'WC' ) && WC()->session ) {
				WC()->session->set( self::SESSION_KEY, array() );
			} else {
				// Expire the cookie.
				if ( function_exists( 'wc_setcookie' ) ) {
					wc_setcookie( self::COOKIE_KEY, '', time() - 3600 );
				} else {
					setcookie( self::COOKIE_KEY, '', time() - 3600, defined( 'COOKIEPATH' ) ? COOKIEPATH : '/' );
				}
				unset( $_COOKIE[ self::COOKIE_KEY ] );
			}
		}

		// ─────────────────────────────────────────────────────────────────
		// Pro: shareable wishlist URL encoding
		// ─────────────────────────────────────────────────────────────────

		/**
		 * Encode a list of product IDs into a base64-url-safe string suitable
		 * for use as a `?share=` query parameter.
		 *
		 * Format: base64-encoded comma-separated IDs (URL-safe variant).
		 *
		 * @param int[] $ids
		 * @return string
		 */
		public function encode_shareable( array $ids ) {
			$ids = array_filter( array_map( 'absint', $ids ) );
			if ( empty( $ids ) ) {
				return '';
			}
			$encoded = base64_encode( implode( ',', $ids ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- Not for security; just for URL-safe transport.
			return strtr( $encoded, '+/=', '-_~' );
		}

		/**
		 * Decode a shareable string back to product IDs.
		 *
		 * @param string $share
		 * @return int[]
		 */
		private function decode_shareable( $share ) {
			$share = strtr( $share, '-_~', '+/=' );
			$raw   = base64_decode( $share, true ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode -- See above.
			if ( false === $raw ) {
				return array();
			}
			$ids = array_filter( array_map( 'absint', explode( ',', $raw ) ) );
			$ids = array_values( array_unique( $ids ) );
			return array_slice( $ids, 0, 50 );
		}

		// ─────────────────────────────────────────────────────────────────
		// Asset enqueue
		// ─────────────────────────────────────────────────────────────────

		public function enqueue_assets() {
			if ( is_admin() ) {
				return;
			}

			$s = $this->get_settings();

			wp_enqueue_style(
				'storzen-wishlist',
				STORZEN_ROOT_URL . 'assets/css/modules/wishlist.css',
				array(),
				STORZEN_VERSION
			);

			wp_enqueue_script(
				'storzen-wishlist',
				STORZEN_ROOT_URL . 'assets/js/modules/wishlist.js',
				array( 'jquery' ),
				STORZEN_VERSION,
				true
			);

			$list           = $this->get_list();
			$wishlist_url   = $s['wishlist_page'] ? get_permalink( $s['wishlist_page'] ) : '';
			$shareable_url  = '';
			if ( $s['pro_shareable_link'] && ! empty( $list ) && $wishlist_url ) {
				$shareable_url = add_query_arg( 'share', $this->encode_shareable( $list ), $wishlist_url );
			}

			wp_localize_script(
				'storzen-wishlist',
				'szWishlist',
				array(
					'ajaxUrl'      => admin_url( 'admin-ajax.php' ),
					'nonce'        => wp_create_nonce( self::NONCE ),
					'list'         => array_map( 'strval', $list ),
					'wishlistUrl'  => $wishlist_url,
					'shareableUrl' => $shareable_url,
					'isLoggedIn'   => is_user_logged_in(),
					'i18n'         => array(
						'add'        => __( 'Add to wishlist', 'storzen' ),
						'remove'     => __( 'Remove from wishlist', 'storzen' ),
						'added'      => __( 'Added to wishlist', 'storzen' ),
						'removed'    => __( 'Removed from wishlist', 'storzen' ),
						'empty'      => __( 'Your wishlist is empty.', 'storzen' ),
						'shareCopied'=> __( 'Wishlist link copied!', 'storzen' ),
						'shareError' => __( 'Could not copy link.', 'storzen' ),
						'error'      => __( 'Could not update wishlist. Please try again.', 'storzen' ),
					),
				)
			);
		}

		// ─────────────────────────────────────────────────────────────────
		// Heart button — shop cards + single product
		// ─────────────────────────────────────────────────────────────────

		/**
		 * Render the heart button.
		 *
		 * @param WC_Product|null $product_arg
		 */
		public function render_button( $product_arg = null ) {
			if ( class_exists( 'Elementor\Storzen_Archive_Grid' ) ) {
				$ctx = \Elementor\Storzen_Archive_Grid::is_module_active( 'wishlist' );
				if ( false === $ctx ) {
					return;
				}
			}
			$product = ( $product_arg instanceof WC_Product ) ? $product_arg : ( $GLOBALS['product'] ?? null );
			if ( ! $product instanceof WC_Product ) {
				return;
			}

			$id       = (int) $product->get_id();
			$list     = $this->get_list();
			$is_added = in_array( $id, $list, true );

			$classes = 'sz-wishlist-btn';
			if ( $is_added ) {
				$classes .= ' is-added';
			}

			$label_add    = __( 'Add to wishlist', 'storzen' );
			$label_remove = __( 'Remove from wishlist', 'storzen' );
			$aria_label   = $is_added ? $label_remove : $label_add;
			?>
			<button
				type="button"
				class="<?php echo esc_attr( $classes ); ?>"
				data-product-id="<?php echo esc_attr( (string) $id ); ?>"
				aria-pressed="<?php echo $is_added ? 'true' : 'false'; ?>"
				aria-label="<?php echo esc_attr( $aria_label ); ?>"
				title="<?php echo esc_attr( $aria_label ); ?>"
			>
				<i class="sz-wishlist-btn__icon far fa-heart" aria-hidden="true"></i>
				<i class="sz-wishlist-btn__icon--filled fas fa-heart" aria-hidden="true"></i>
			</button>
			<?php
		}

		// ─────────────────────────────────────────────────────────────────
		// Wishlist page rendering
		// ─────────────────────────────────────────────────────────────────

		public function shortcode_render_page() {
			return $this->render_page();
		}

		/**
		 * Auto-inject wishlist on the assigned page when content is requested.
		 */
		public function maybe_inject_page( $content ) {
			$s = $this->get_settings();
			if ( ! $s['wishlist_page'] ) {
				return $content;
			}
			if ( ! is_singular() || ! is_main_query() || ! in_the_loop() ) {
				return $content;
			}
			if ( (int) get_the_ID() !== (int) $s['wishlist_page'] ) {
				return $content;
			}
			if ( has_shortcode( $content, self::SHORTCODE ) ) {
				return $content; // Author already added it.
			}
			return $content . $this->render_page();
		}

		/**
		 * Render the wishlist page (grid of saved products).
		 *
		 * @return string HTML
		 */
		public function render_page() {
			$s        = $this->get_settings();
			$ids      = $this->get_list();
			// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only flag for rendering a shared wishlist view; no state change requiring a nonce.
			$is_share = $s['pro_shareable_link'] && ! empty( $_GET['share'] );

			if ( empty( $ids ) ) {
				return '<div class="sz-wishlist-empty">' . esc_html__( 'Your wishlist is empty. Browse the shop and tap the heart icon to save products.', 'storzen' ) . '</div>';
			}

			$products = array();
			foreach ( $ids as $id ) {
				$p = wc_get_product( $id );
				if ( $p instanceof WC_Product && $p->is_visible() ) {
					$products[] = $p;
				}
			}
			if ( empty( $products ) ) {
				return '<div class="sz-wishlist-empty">' . esc_html__( 'The items in your wishlist are no longer available.', 'storzen' ) . '</div>';
			}

			ob_start();
			?>
			<div class="sz-wishlist-wrap<?php echo $is_share ? ' sz-wishlist-wrap--shared' : ''; ?>">
				<?php if ( $is_share ) : ?>
					<div class="sz-wishlist-notice">
						<?php esc_html_e( 'You are viewing a shared wishlist.', 'storzen' ); ?>
					</div>
				<?php endif; ?>

				<header class="sz-wishlist-header">
					<h3 class="sz-wishlist-title">
						<?php esc_html_e( 'My Wishlist', 'storzen' ); ?>
						<span class="sz-wishlist-count">(<?php echo count( $products ); ?>)</span>
					</h3>

					<div class="sz-wishlist-actions">
						<?php if ( $s['pro_shareable_link'] && ! $is_share ) : ?>
							<button type="button" class="sz-wishlist-share" data-sz-wishlist-share aria-label="<?php esc_attr_e( 'Share wishlist', 'storzen' ); ?>">
								<?php esc_html_e( 'Share', 'storzen' ); ?>
							</button>
						<?php endif; ?>
						<?php if ( ! $is_share ) : ?>
							<button type="button" class="sz-wishlist-clear" data-sz-wishlist-clear>
								<?php esc_html_e( 'Clear all', 'storzen' ); ?>
							</button>
						<?php endif; ?>
					</div>
				</header>

				<div class="sz-wishlist-grid">
					<?php foreach ( $products as $p ) :
						$thumb_id   = $p->get_image_id();
						$price_html = $p->get_price_html();
						$atc_url    = $p->add_to_cart_url();
						$atc_label  = $p->add_to_cart_text();
						$in_stock   = $p->is_in_stock();
						?>
						<article class="sz-wishlist-item" data-product-id="<?php echo esc_attr( $p->get_id() ); ?>">
							<?php if ( ! $is_share ) : ?>
								<button type="button" class="sz-wishlist-item__remove" data-sz-wishlist-remove="<?php echo esc_attr( $p->get_id() ); ?>" aria-label="<?php esc_attr_e( 'Remove from wishlist', 'storzen' ); ?>">&times;</button>
							<?php endif; ?>

							<a class="sz-wishlist-item__thumb" href="<?php echo esc_url( get_permalink( $p->get_id() ) ); ?>">
								<?php if ( $thumb_id ) {
									echo wp_get_attachment_image(
										$thumb_id,
										'woocommerce_thumbnail',
										false,
										array( 'alt' => esc_attr( $p->get_name() ), 'loading' => 'lazy' )
									);
								} ?>
							</a>

							<div class="sz-wishlist-item__body">
								<a class="sz-wishlist-item__name" href="<?php echo esc_url( get_permalink( $p->get_id() ) ); ?>">
									<?php echo esc_html( $p->get_name() ); ?>
								</a>
								<div class="sz-wishlist-item__price"><?php echo wp_kses_post( $price_html ); ?></div>

								<div class="sz-wishlist-item__stock">
									<?php if ( $in_stock ) : ?>
										<span class="sz-wishlist-stock sz-wishlist-stock--in"><?php esc_html_e( 'In stock', 'storzen' ); ?></span>
									<?php else : ?>
										<span class="sz-wishlist-stock sz-wishlist-stock--out"><?php esc_html_e( 'Out of stock', 'storzen' ); ?></span>
									<?php endif; ?>
								</div>

								<a href="<?php echo esc_url( $atc_url ); ?>" class="sz-wishlist-item__atc button"<?php echo $in_stock ? '' : ' aria-disabled="true"'; ?>>
									<?php echo esc_html( $atc_label ); ?>
								</a>
							</div>
						</article>
					<?php endforeach; ?>
				</div>
			</div>
			<?php
			return ob_get_clean();
		}

		/**
		 * [storzen_wishlist_count] — renders just a counter span. Useful in
		 * theme menus / header. Updates live via JS.
		 *
		 * @return string
		 */
		public function shortcode_count() {
			$count = count( $this->get_list() );
			return sprintf(
				'<span class="sz-wishlist-counter" data-sz-wishlist-count>%d</span>',
				$count
			);
		}

		// ─────────────────────────────────────────────────────────────────
		// AJAX endpoints
		// ─────────────────────────────────────────────────────────────────

		public function ajax_add() {
			$this->verify_nonce();
			// phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified above in verify_nonce().
			$id = isset( $_POST['product_id'] ) ? absint( wp_unslash( $_POST['product_id'] ) ) : 0;
			if ( ! $id || ! wc_get_product( $id ) ) {
				wp_send_json_error( array( 'message' => __( 'Invalid product.', 'storzen' ) ), 400 );
			}

			$list = $this->get_list();
			if ( in_array( $id, $list, true ) ) {
				wp_send_json_success(
					array(
						'list'  => array_map( 'strval', $list ),
						'count' => count( $list ),
						'state' => 'already-in',
					)
				);
			}

			$list[] = $id;
			$this->set_list( $list );
			do_action( 'storzen_wishlist_added', $id );

			wp_send_json_success(
				array(
					'list'  => array_map( 'strval', $list ),
					'count' => count( $list ),
					'state' => 'added',
				)
			);
		}

		public function ajax_remove() {
			$this->verify_nonce();
			// phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified above in verify_nonce().
			$id   = isset( $_POST['product_id'] ) ? absint( wp_unslash( $_POST['product_id'] ) ) : 0;
			$list = $this->get_list();
			$list = array_values( array_diff( $list, array( $id ) ) );
			$this->set_list( $list );

			do_action( 'storzen_wishlist_removed', $id );

			wp_send_json_success(
				array(
					'list'  => array_map( 'strval', $list ),
					'count' => count( $list ),
					'page'  => $this->render_page(),
				)
			);
		}

		public function ajax_clear() {
			$this->verify_nonce();
			$this->set_list( array() );
			do_action( 'storzen_wishlist_cleared' );

			wp_send_json_success(
				array(
					'list'  => array(),
					'count' => 0,
					'page'  => $this->render_page(),
				)
			);
		}

		public function ajax_count() {
			// No nonce — read-only public endpoint, low-risk.
			$list = $this->get_list();
			wp_send_json_success(
				array(
					'count' => count( $list ),
					'list'  => array_map( 'strval', $list ),
				)
			);
		}

		private function verify_nonce() {
			$nonce = isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : '';
			if ( ! wp_verify_nonce( $nonce, self::NONCE ) ) {
				wp_send_json_error( array( 'message' => __( 'Security check failed. Please refresh and try again.', 'storzen' ) ), 403 );
			}
		}

	} // end class

} // end class_exists
