<?php
/**
 * Admin Container
 *
 * @since 1.7.0
 *
 * @package Envira_Gallery
 * @author Envira Gallery Team
 */

namespace Envira\Admin;

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

use Envira\Admin\Settings;
use Envira\Admin\Metaboxes;
use Envira\Admin\Addons;
use Envira\Admin\Importers;
use Envira\Admin\Table;
use Envira\Admin\Posttype;
use Envira\Admin\Editor;
use Envira\Admin\License;
use Envira\Admin\Notices;
use Envira\Admin\Media_View;
use Envira\Admin\Support;
use Envira\Admin\Gutenberg;
use Envira\Admin\Tools;
use Envira\Utils\Copy;
use Envira\Admin\LicenseCheck;
use Envira\Admin\Permissions;
use Envira\Admin\Onboarding_Wizard;
use Envira\Admin\Menu_Nudge;
use Envira\Admin\Envira_Notifications;
// use Envira\Admin\Debug.

/**
 * Admin Container
 *
 * @since 1.7.0
 *
 * @package Envira_Gallery
 * @author  Envira Gallery Team <support@enviragallery.com>
 */
class Admin_Container {


	/**
	 * Holds all dismissed notices.
	 *
	 * @since 1.3.5
	 *
	 * @var array
	 */
	public $notices;

	/**
	 * __construct function.
	 *
	 * @access public
	 * @return void
	 */
	public function __construct() {

		// Actions.
		add_action( 'admin_init', [ $this, 'add_capabilities' ] );
		add_filter( 'wp_handle_upload', [ $this, 'fix_image_orientation' ] );

		// Handle any necessary DB upgrades.
		add_action( 'admin_init', [ $this, 'db_upgrade' ] );

		// Add new section in WordPress media settings page.
		add_action( 'admin_init', [ $this, 'envira_gallery_media_settings_add_new_section' ] );

		// Load admin assets.
		add_action( 'admin_enqueue_scripts', [ $this, 'admin_styles' ], 20 );
		add_action( 'admin_enqueue_scripts', [ $this, 'admin_scripts' ] );
		add_action( 'admin_head', [ $this, 'custom_admin_style' ], 1 );
		add_action( 'admin_menu', [ $this, 'cdn_admin_menu' ], 12 );

		// Delete any gallery association on attachment deletion. Also delete any extra cropped images.
		add_action( 'delete_attachment', [ $this, 'delete_gallery_association' ] );

		// Ensure gallery display is correct when trashing/untrashing galleries.
		add_action( 'wp_trash_post', [ $this, 'trash_gallery' ] );
		add_action( 'untrash_post', [ $this, 'untrash_gallery' ] );

		// Delete attachments, if setting enabled, when a gallery is permanently deleted.
		add_action( 'before_delete_post', [ $this, 'delete_gallery' ] );

		// Prevent plugins from breaking Envira in admin.
		add_action( 'wp_print_scripts', [ $this, 'plugin_humility' ], 1 );
		add_action( 'admin_init', [ $this, 'fix_conflicts' ], 99 );
		if ( is_plugin_active( 'enable-media-replace/enable-media-replace.php' ) ) {
			add_action( 'enable-media-replace-upload-done', [ $this, 'emr_clear_envira_cache' ], 10, 3 );
		}

		// Populate $notices.
		$this->notices = get_option( 'envira_gallery_notices' );

		if ( ! is_array( $this->notices ) ) {
			$this->notices = [];
		}
		$license_check = new LicenseCheck();
		$posttype      = new Posttype();
		$settings      = new Settings();
		$metaboxes     = new Metaboxes();
		$importers     = new Importers();
		$table         = new Table();
		$editor        = new Editor();
		$license       = new License();
		$notices       = new Notices();
		$addons        = new Addons();
		$media_view    = new Media_View();
		$support       = new Support();
		$tools         = new Tools();
		$gutenberg     = new Gutenberg();
		$copy          = new Copy();
		$permissions   = new Permissions();
		$onboarding    = new Onboarding_Wizard();
		$notifications = new Envira_Notifications();
		$notifications->hooks();
		$onboarding->hooks();
		$menu_nudge = new Menu_Nudge();
		$menu_nudge->hooks();

		// Add action to output JS for Envira CDN menu item.
		add_action( 'admin_footer', [ $this, 'admin_cdn_menu_target_blank' ] );
	}

	/**
	 * Add Beta Feedback Menu Item
	 *
	 * @since 1.9.0b
	 *
	 * @return void
	 */
	public function beta_admin_menu() {
		global $submenu;

		$url                                    = 'https://enviragallery.com/account/beta-support/';
		$submenu['edit.php?post_type=envira'][] = [ '<span style="color:#ad0023">Beta Feedback</span>', 'manage_options', $url ];
	}

	/**
	 * Add CDN Menu Item.
	 *
	 * @since 1.11.1-0
	 *
	 * @return void
	 */
	public function cdn_admin_menu() {
		global $submenu;

		$utm = '?utm_source=envira-gallery&utm_medium=pro-plugin&utm_campaign=admin-menu&utm_content=' . ENVIRA_VERSION;

		$url                                    = 'https://enviragallery.com/cdn/' . $utm;
		$cdn_menu                               = [
			'Envira CDN<span class="cdn_new_badge">NEW</span>',
			'manage_options',
			$url,
			'envira-cdn-menu-item',
		];
		$submenu['edit.php?post_type=envira'][] = $cdn_menu;
	}

	/**
	 * Attempts to fix conflicts from other plugins.
	 *
	 * @since 1.1.0
	 *
	 * @return void
	 */
	public function fix_conflicts() {
		global $pagenow;
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce is not required here.
		if ( 'post.php' !== $pagenow && isset( $_GET['post'] ) && 'envira' !== get_post_type( sanitize_key( $_GET['post'] ) ) ) {
			return;
		}

		if ( class_exists( 'WRGF' ) ) {
			// phpcs:disable WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
			global $WRGF;
			remove_action( 'admin_print_scripts-post.php', [ $WRGF, 'wrgf_admin_print_scripts' ] );
			remove_action( 'admin_print_scripts-post-new.php', [ $WRGF, 'wrgf_admin_print_scripts' ] );
			// phpcs:enable WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
		}

		/* Accordion Pro By Wpshopmart */
		if ( function_exists( 'wpsm_ac_pro_editor_popup_content' ) ) {
			remove_action( 'admin_footer', 'wpsm_ac_pro_editor_popup_content' );
		}
		/* Tabs Pro wpshopmart */
		if ( function_exists( 'wpsm_tabs_pro_editor_popup_content' ) ) {
			remove_action( 'admin_footer', 'wpsm_tabs_pro_editor_popup_content' );
		}
		/* Service Showcase Pro */
		if ( function_exists( 'wpsm_service_box_pro_editor_popup_content' ) ) {
			remove_action( 'admin_footer', 'wpsm_service_box_pro_editor_popup_content' );
		}
		/* Counter Number Showcase by wpshopmart */
		if ( function_exists( 'wpsm_cns_editor_popup_content' ) ) {
			remove_action( 'admin_footer', 'wpsm_cns_editor_popup_content' );
		}
	}

	/**
	 * Checks if a given notice has been dismissed or not
	 *
	 * @since 1.3.5
	 *
	 * @param string $notice Programmatic Notice Name.
	 * @return bool Notice Dismissed
	 */
	public function is_dismissed( $notice ) {

		if ( ! isset( $this->notices[ $notice ] ) ) {
			return false;
		}

		return true;
	}

	/**
	 * Marks the given notice as dismissed
	 *
	 * @since 1.3.5
	 *
	 * @param string $notice Programmatic Notice Name.
	 */
	public function dismiss( $notice ) {

		$this->notices[ $notice ] = true;
		update_option( 'envira_gallery_notices', $this->notices );
	}


	/**
	 * Marks a notice as not dismissed
	 *
	 * @since 1.3.5
	 *
	 * @param string $notice Programmatic Notice Name.
	 */
	public function undismiss( $notice ) {

		unset( $this->notices[ $notice ] );
		update_option( 'envira_gallery_notices', $this->notices );
	}

	/**
	 * Displays an inline notice with some Envira styling.
	 *
	 * @since 1.3.5
	 *
	 * @param string $notice             Programmatic Notice Name.
	 * @param string $title              Title.
	 * @param string $message            Message.
	 * @param string $type               Message Type (updated|warning|error) - green, yellow/orange and red respectively.
	 * @param string $button_text        Button Text (optional).
	 * @param string $button_url         Button URL (optional).
	 * @param bool   $is_dismissible     User can Dismiss Message (default: true).
	 */
	public function display_inline_notice( $notice, $title, $message, $type = 'success', $button_text = '', $button_url = '', $is_dismissible = true ) {

		// Check if the notice is dismissible, and if so has been dismissed.
		if ( $is_dismissible && $this->is_dismissed( $notice ) ) {
			// Nothing to show here, return!
			return;
		}

		// Display inline notice.
		?>
		<div class="envira-notice <?php echo esc_attr( $type ) . ( $is_dismissible ? ' is-dismissible' : '' ); ?>" data-notice="<?php echo esc_attr( $notice ); ?>">
			<?php
			// Title.
			if ( ! empty( $title ) ) {
				?>
				<p class="envira-intro"><?php echo esc_html( $title ); ?></p>
				<?php
			}

			// Message.
			if ( ! empty( $message ) ) {
				?>
				<p><?php echo esc_html( $message ); ?></p>
				<?php
			}

			// Button.
			if ( ! empty( $button_text ) && ! empty( $button_url ) ) {
				?>
				<a href="<?php echo esc_url( $button_url ); ?>" target="_blank" rel="noopener" class="button button-primary"><?php echo esc_html( $button_text ); ?></a>
				<?php
			}

			// Dismiss Button.
			if ( $is_dismissible ) {
				?>
				<button type="button" class="notice-dismiss">
					<span class="screen-reader-text">
						<?php esc_html_e( 'Dismiss this notice', 'envira-gallery' ); ?>
					</span>
				</button>
				<?php
			}
			?>
		</div>
		<?php
	}
	/**
	 * Registers Envira Gallery capabilities for each Role, if they don't already exist.
	 *
	 * If capabilities don't exist, they're copied from Posts. This ensures users prior to 1.3.7
	 * get like-for-like behaviour in Envira and don't notice the new capabilities.
	 *
	 * @since 1.0.0
	 */
	public function add_capabilities() {

		// Bail out if it is settings page update action.
		// phpcs:ignore WordPress.Security.NonceVerification
		if ( isset( $_POST['envira-permissions-nonce'] ) && sanitize_text_field( wp_unslash( $_POST['envira-permissions-nonce'] ) ) !== null ) {
			return;
		}
		// Todo: Refactor this duplicate method to run only once, and not on every admin_init.

		// Grab the administrator role, and if it already has an Envira capability key defined, bail
		// as we only need to register our capabilities once.
		$administrator = get_role( 'administrator' );
		if ( $administrator->has_cap( 'edit_other_envira_galleries' ) ) {
			return;
		}

		// If here, we need to assign capabilities
		// Define the roles we want to assign capabilities to...
		$roles = [
			'administrator',
			'editor',
			'author',
			'contributor',
			'subscriber',
		];

		// Iterate through roles.
		foreach ( $roles as $role_name ) {
			// Properly get the role as WP_Role object.
			$role = get_role( $role_name );
			if ( ! is_object( $role ) ) {
				continue;
			}

			// Map this Role's Post capabilities to our Envira Gallery capabilities.
			$caps = [
				'edit_envira_gallery'               => $role->has_cap( 'edit_posts' ),
				'read_envira_gallery'               => $role->has_cap( 'read' ),
				'delete_envira_gallery'             => $role->has_cap( 'delete_posts' ),

				'edit_envira_galleries'             => $role->has_cap( 'edit_posts' ),
				'edit_other_envira_galleries'       => $role->has_cap( 'edit_others_posts' ),
				'edit_others_envira_galleries'      => $role->has_cap( 'edit_others_posts' ),
				'publish_envira_galleries'          => $role->has_cap( 'publish_posts' ),
				'read_private_envira_galleries'     => $role->has_cap( 'read_private_posts' ),

				'delete_envira_galleries'           => $role->has_cap( 'delete_posts' ),
				'delete_private_envira_galleries'   => $role->has_cap( 'delete_private_posts' ),
				'delete_published_envira_galleries' => $role->has_cap( 'delete_published_posts' ),
				'delete_others_envira_galleries'    => $role->has_cap( 'delete_others_posts' ),
				'edit_private_envira_galleries'     => $role->has_cap( 'edit_private_posts' ),
				'edit_published_envira_galleries'   => $role->has_cap( 'edit_published_posts' ),
				'create_envira_galleries'           => $role->has_cap( 'edit_posts' ),
			];

			// Add the above Envira capabilities to this Role.
			foreach ( $caps as $envira_cap => $value ) {
				// Don't add if value is false.
				if ( ! $value ) {
					continue;
				}

				$role->add_cap( $envira_cap );
			}
		}
	}

	/**
	 * Check if the EXIF orientation flag matches one of the values we're looking for
	 * http://www.impulseadventure.com/photo/exif-orientation.html
	 *
	 * If it does, this means we need to rotate the image based on the orientation flag and then remove the flag.
	 * This will ensure the image has the correct orientation, regardless of where it's displayed.
	 *
	 * Whilst most browsers and applications will read this flag to perform the rotation on displaying just the image, it's
	 * not possible to do this in some situations e.g. displaying an image within a lightbox, or when the image is
	 * within HTML markup.
	 *
	 * Orientation flags we're looking for:
	 * 8: We need to rotate the image 90 degrees counter-clockwise
	 * 3: We need to rotate the image 180 degrees
	 * 6: We need to rotate the image 90 degrees clockwise (270 degrees counter-clockwise)
	 *
	 * @since 1.3.8.2
	 *
	 * @param array $file   Uploaded File.
	 * @return array            Uploaded File
	 */
	public function fix_image_orientation( $file ) {

		// Check we have a file.
		if ( ! file_exists( $file['file'] ) ) {
			return $file;
		}

		// Check we have a JPEG.
		if ( 'image/jpg' !== $file['type'] && 'image/jpeg' !== $file['type'] ) {
			return $file;
		}

		// Attempt to read EXIF data from the image.
		$exif_data = wp_read_image_metadata( $file['file'] );
		if ( ! $exif_data ) {
			return $file;
		}

		// Check if an orientation flag exists.
		if ( ! isset( $exif_data['orientation'] ) ) {
			return $file;
		}

		// Check if the orientation flag matches one we're looking for.
		$required_orientations = [ 8, 3, 6 ];
		if ( ! in_array( $exif_data['orientation'], $required_orientations, true ) ) {
			return $file;
		}

		// If here, the orientation flag matches one we're looking for
		// Load the WordPress Image Editor class.
		$image = wp_get_image_editor( $file['file'] );
		if ( is_wp_error( $image ) ) {
			// Something went wrong - abort.
			return $file;
		}

		// Store the source image EXIF and IPTC data in a variable, which we'll write
		// back to the image once its orientation has changed
		// This is required because when we save an image, it'll lose its metadata.
		$source_size = getimagesize( $file['file'], $image_info );

		// Depending on the orientation flag, rotate the image.
		switch ( $exif_data['orientation'] ) {

			/**
			* Rotate 90 degrees counter-clockwise
			*/
			case 8:
				$image->rotate( 90 );
				break;

			/**
			* Rotate 180 degrees
			*/
			case 3:
				$image->rotate( 180 );
				break;

			/**
			* Rotate 270 degrees counter-clockwise ($image->rotate always works counter-clockwise)
			*/
			case 6:
				$image->rotate( 270 );
				break;
		}

		// Save the image, overwriting the existing image
		// This will discard the EXIF and IPTC data.
		$image->save( $file['file'] );

		// Drop the EXIF orientation flag, otherwise applications will try to rotate the image
		// before display it, and we don't need that to happen as we've corrected the orientation
		// Write the EXIF and IPTC metadata to the revised image.
		$result = $this->transfer_iptc_exif_to_image( $image_info, $file['file'], $exif_data['orientation'] );
		if ( ! $result ) {
			return $file;
		}

		// Read the image again to see if the EXIF data was preserved.
		$exif_data = wp_read_image_metadata( $file['file'] );

		// Finally, return the data that's expected.
		return $file;
	}

	/**
	 * Transfers IPTC and EXIF data from a source image which contains either/both,
	 * and saves it into a destination image's headers that might not have this IPTC
	 * or EXIF data
	 *
	 * Useful for when you edit an image through PHP and need to preserve IPTC and EXIF
	 * data
	 *
	 * @since 1.3.8.2
	 *
	 * @source http://php.net/iptcembed - ebashkoff at gmail dot com
	 *
	 * @param string $image_info                EXIF and IPTC image information from the source image, using getimagesize().
	 * @param string $destination_image     Path and File of Destination Image, which needs IPTC and EXIF data.
	 * @param int    $original_orientation   The image's original orientation, before we changed it.
	 *                                       Used when we replace this orientation in the EXIF data.
	 * @return bool                         Success
	 */
	private function transfer_iptc_exif_to_image( $image_info, $destination_image, $original_orientation ) {

		// Check destination exists.
		if ( ! file_exists( $destination_image ) ) {
			return false;
		}

		// Get EXIF data from the image info, and create the IPTC segment.
		$exif_data = ( ( is_array( $image_info ) && key_exists( 'APP1', $image_info ) ) ? $image_info['APP1'] : null );
		if ( $exif_data ) {
			// Find the image's original orientation flag, and change it to 1
			// This prevents applications and browsers re-rotating the image, when we've already performed that function.
			$exif_data = str_replace( chr( dechex( $original_orientation ) ), chr( 0x1 ), $exif_data );

			$exif_length = strlen( $exif_data ) + 2;
			if ( $exif_length > 0xFFFF ) {
				return false;
			}

			// Construct EXIF segment.
			$exif_data = chr( 0xFF ) . chr( 0xE1 ) . chr( ( $exif_length >> 8 ) & 0xFF ) . chr( $exif_length & 0xFF ) . $exif_data;
		}

		// Get IPTC data from the source image, and create the IPTC segment.
		$iptc_data = ( ( is_array( $image_info ) && key_exists( 'APP13', $image_info ) ) ? $image_info['APP13'] : null );
		if ( $iptc_data ) {
			$iptc_length = strlen( $iptc_data ) + 2;
			if ( $iptc_length > 0xFFFF ) {
				return false;
			}

			// Construct IPTC segment.
			$iptc_data = chr( 0xFF ) . chr( 0xED ) . chr( ( $iptc_length >> 8 ) & 0xFF ) . chr( $iptc_length & 0xFF ) . $iptc_data;
		}

		// Get the contents of the destination image.
		$remote_path                = wp_remote_get( $destination_image );
		$destination_image_contents = wp_remote_retrieve_body( $remote_path );
		if ( ! $destination_image_contents ) {
			return false;
		}
		if ( strlen( $destination_image_contents ) === 0 ) {
			return false;
		}

		// Build the EXIF and IPTC data headers.
		$destination_image_contents = substr( $destination_image_contents, 2 );
		$portion_to_add             = chr( 0xFF ) . chr( 0xD8 ); // Variable accumulates new & original IPTC application segments.
		$exif_added                 = ! $exif_data;
		$iptc_added                 = ! $iptc_data;

		while ( ( substr( $destination_image_contents, 0, 2 ) & 0xFFF0 ) === 0xFFE0 ) {
			$segment_length      = ( substr( $destination_image_contents, 2, 2 ) & 0xFFFF );
			$iptc_segment_number = ( substr( $destination_image_contents, 1, 1 ) & 0x0F );   // Last 4 bits of second byte is IPTC segment #.
			if ( $segment_length <= 2 ) {
				return false;
			}

			$thisexistingsegment = substr( $destination_image_contents, 0, $segment_length + 2 );
			if ( ( 1 <= $iptc_segment_number ) && ( ! $exif_added ) ) {
				$portion_to_add .= $exif_data;
				$exif_added      = true;
				if ( 1 === $iptc_segment_number ) {
					$thisexistingsegment = '';
				}
			}

			if ( ( 13 <= $iptc_segment_number ) && ( ! $iptc_added ) ) {
				$portion_to_add .= $iptc_data;
				$iptc_added      = true;
				if ( 13 === $iptc_segment_number ) {
					$thisexistingsegment = '';
				}
			}

			$portion_to_add            .= $thisexistingsegment;
			$destination_image_contents = substr( $destination_image_contents, $segment_length + 2 );
		}

		// Write the EXIF and IPTC data to the new file.
		if ( ! $exif_added ) {
			$portion_to_add .= $exif_data;
		}
		if ( ! $iptc_added ) {
			$portion_to_add .= $iptc_data;
		}

		$output_file = fopen( $destination_image, 'w' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen
		if ( $output_file ) {
			return fwrite( $output_file, $portion_to_add . $destination_image_contents ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fwrite
		}

		return false;
	}

	/**
	 * Handles any necessary DB upgrades for Envira.
	 *
	 * @since 1.0.0
	 */
	public function db_upgrade() {

		// Upgrade to allow captions (v1.1.6).
		$captions = get_option( 'envira_gallery_116' );
		if ( ! $captions ) {
			$galleries = ( class_exists( 'Envira_Gallery' ) ? \Envira_Gallery::get_instance()->_get_galleries() : \Envira_Gallery_Lite::get_instance()->_get_galleries() );
			if ( $galleries ) {
				foreach ( $galleries as $gallery ) {
					foreach ( (array) $gallery['gallery'] as $id => $item ) {
						$gallery['gallery'][ $id ]['caption'] = ! empty( $item['title'] ) ? $item['title'] : '';
						update_post_meta( $gallery['id'], '_eg_gallery_data', $gallery );
						envira_flush_gallery_caches( $gallery['id'], $gallery['config']['slug'] );
					}
				}
			}

			update_option( 'envira_gallery_116', true );
		}

		// 1.2.1: Convert all non-Envira Post Type galleries into Envira CPT galleries.
		$cpt_galleries = get_option( 'envira_gallery_121' );
		if ( ! $cpt_galleries ) {
			// Get Post Types, excluding our own
			// We don't use post_status => 'any', as this doesn't include CPTs where exclude_from_search = true.
			$post_types          = get_post_types(
				[
					'public' => true,
				]
			);
			$excluded_post_types = [ 'envira', 'envira_album', 'attachment' ];
			foreach ( $post_types as $key => $post_type ) {
				if ( in_array( $post_type, $excluded_post_types, true ) ) {
					unset( $post_types[ $key ] );
				}
			}

			// Get all Posts that have _eg_gallery_data set.
			$in_post_galleries = new \WP_Query(
				[
					'post_type'      => $post_types,
					'post_status'    => 'any',
					'posts_per_page' => -1,
					'meta_query'     => [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
						[
							'key'     => '_eg_gallery_data',
							'compare' => 'EXISTS',
						],
					],
				]
			);

			// Check if any Posts with galleries exist.
			if ( count( $in_post_galleries->posts ) > 0 ) {
				$migrated_galleries = 0;

				// Iterate through Posts with Galleries.
				foreach ( $in_post_galleries->posts as $post ) {
					// Check if this is an Envira or Envira Album CPT
					// If so, skip it.
					if ( 'envira' === $post->post_type || 'envira_album' === $post->post_type ) {
						continue;
					}

					// Get metadata.
					$data = get_post_meta( $post->ID, '_eg_gallery_data', true );
					$in   = get_post_meta( $post->ID, '_eg_in_gallery', true );

					// Check if there is at least one image in the gallery
					// Some Posts save Envira config data but don't have images - we don't want to migrate those,
					// as we would end up with blank Envira CPT galleries.
					if ( ! isset( $data['gallery'] ) || ! is_array( $data['gallery'] ) ) {
						continue;
					}

					// If here, we need to create a new Envira CPT.
					$cpt_args = [
						'post_title'  => ( ! empty( $data['config']['title'] ) ? $data['config']['title'] : $post->post_title ),
						'post_status' => $post->post_status,
						'post_type'   => 'envira',
						'post_author' => $post->post_author,
					];
					if ( ! empty( $data['config']['slug'] ) ) {
						$cpt_args['post_name'] = $data['config']['slug'];
					}
					$envira_gallery_id = wp_insert_post( $cpt_args );

					// Check gallery creation was successful.
					if ( is_wp_error( $envira_gallery_id ) ) {
						// @TODO how to handle errors?
						continue;
					}

					// Get Envira Gallery Post.
					$envira_post = get_post( $envira_gallery_id );

					// Map the title and slug of the post object to the custom fields if no value exists yet.
					$data['config']['title'] = trim( wp_strip_all_tags( $envira_post->post_title ) );
					$data['config']['slug']  = sanitize_text_field( $envira_post->post_name );

					// Store post metadata.
					update_post_meta( $envira_gallery_id, '_eg_gallery_data', $data );
					update_post_meta( $envira_gallery_id, '_eg_in_gallery', $in );
					update_post_meta( $envira_gallery_id, '_eg_gallery_old', $post->ID );
					if ( ! empty( $data['config']['slug'] ) ) {
						update_post_meta( $envira_gallery_id, '_eg_gallery_old_slug', $data['config']['slug'] );
					}

					// Remove post metadata from the original Post.
					delete_post_meta( $post->ID, '_eg_gallery_data' );
					delete_post_meta( $post->ID, '_eg_in_gallery' );

					// Search for the envira shortcode in the Post content, and change its ID to the new Envira Gallery ID.
					if ( has_shortcode( $post->post_content, 'envira-gallery' ) ) {
						$pattern = get_shortcode_regex();
						if ( preg_match_all( '/' . $pattern . '/s', $post->post_content, $matches ) ) {
							foreach ( $matches[2] as $key => $shortcode ) {
								if ( 'envira-gallery' === $shortcode ) {
									// Found an envira-gallery shortcode
									// Change the ID.
									$original_shortcode    = $matches[0][ $key ];
									$replacement_shortcode = str_replace( 'id="' . $post->ID . '"', 'id="' . $envira_gallery_id . '"', $original_shortcode );
									$post->post_content    = str_replace( $original_shortcode, $replacement_shortcode, $post->post_content );
									wp_update_post( $post );
								}
							}
						}
					}

					// Store a relationship between the gallery and this Post.
					update_post_meta( $post->ID, '_eg_gallery_id', $envira_gallery_id );

					// Increment the counter.
					++$migrated_galleries;
				}

				// Display a one time admin notice so the user knows their in-page galleries were migrated.
				if ( $migrated_galleries > 0 ) {
					add_action( 'admin_notices', [ $this, 'notice_galleries_migrated' ] );
				}
			}

			// Force the tags addon to convert any tags to the new CPT system for any galleries that have been converted to Envira post type.
			delete_option( 'envira_tags_taxonomy_migrated' );

			// Mark upgrade as complete.
			update_option( 'envira_gallery_121', true );
		}
	}

	/**
	 * Displays a notice on screen when a user upgrades from Lite to Pro or Lite to Lite 1.5.x,
	 * telling them that their in-page galleries have been migrated.
	 *
	 * @since 1.5.0
	 */
	public function notice_galleries_migrated() {

		?>
		<div class="notice updated">
			<p><strong><?php esc_html_e( 'Envira Gallery:', 'envira-gallery' ); ?></strong><?php esc_html_e( ' Your existing in-page Galleries can now be found by clicking on Envira Gallery in the WordPress Admin menu.', 'envira-gallery' ); ?></p>
		</div>
		<?php
	}

	/**
	 * Loads styles for all Envira-based Administration Screens.
	 *
	 * @since 1.3.1
	 */
	public function admin_styles() {

		// Get current screen.
		$screen = get_current_screen();

		// if we are on the widget page, load the admin css.
		if ( 'widgets' === $screen->id || 'customize' === $screen->id ) {

			wp_register_style( ENVIRA_SLUG . '-admin-style', plugins_url( 'assets/css/admin.css', ENVIRA_FILE ), [], ENVIRA_VERSION );
			wp_enqueue_style( ENVIRA_SLUG . '-admin-style' );
		} elseif ( 'envira' !== $screen->post_type && 'envira_album' !== $screen->post_type ) {

			// If we're not on the Envira Post Type screen, only load the modal and minimum css then bail.
			wp_register_style( ENVIRA_SLUG . '-admin-min-style', plugins_url( 'assets/css/admin-min.css', ENVIRA_FILE ), [], ENVIRA_VERSION );
			wp_enqueue_style( ENVIRA_SLUG . '-admin-min-style' );
			wp_register_style( ENVIRA_SLUG . '-admin-modal-style', plugins_url( 'assets/css/admin-modal.css', ENVIRA_FILE ), [], ENVIRA_VERSION );
			wp_enqueue_style( ENVIRA_SLUG . '-admin-modal-style' );
		} else {

			// Proceed loading remaining admin CSS necessary admin styles.
			wp_register_style( ENVIRA_SLUG . '-admin-style', plugins_url( 'assets/css/admin.css', ENVIRA_FILE ), [], ENVIRA_VERSION );
			wp_enqueue_style( ENVIRA_SLUG . '-admin-style' );
			wp_register_style( ENVIRA_SLUG . '-admin-modal-style', plugins_url( 'assets/css/admin-modal.css', ENVIRA_FILE ), [], ENVIRA_VERSION );
			wp_enqueue_style( ENVIRA_SLUG . '-admin-modal-style' );
		}

		// Fire a hook to load in custom admin styles.
		do_action( 'envira_gallery_admin_styles' );
	}

	/**
	 * Force special css to load at the top of the page before most anything else
	 *
	 * @since 1.8.1
	 *
	 * @return null
	 */
	public function custom_admin_style() {

		// Add CDN menu item's styles.
		echo '<style>
			ul#adminmenu li.menu-icon-envira ul.wp-submenu span.cdn_new_badge {
				color:#f18200;
				vertical-align:super;
				font-size:9px;
				margin-left:3px;
			}
		</style>';

		// Get current screen.
		$screen = get_current_screen();

		// Bail if we're not on the Envira Post Type screen or in widgets.
		if ( 'envira' !== $screen->post_type && 'envira_album' !== $screen->post_type ) {
			return;
		}

		echo '<style>
			#screen-meta-links {
				display: none;
			}
	  </style>';
	}

	/**
	 * Loads scripts for all Envira-based Administration Screens.
	 *
	 * @since 1.3.5
	 *
	 * @param string $hook Page Hook.
	 * @return null Return early if not on the proper screen.
	 */
	public function admin_scripts( $hook ) {

		// Get current screen.
		$screen = get_current_screen();

		// Include notice js, because notices might need to be displayed on pages other than Envira's.
		wp_register_script( ENVIRA_SLUG . '-admin-notice-script', plugins_url( 'assets/js/min/notices-min.js', ENVIRA_FILE ), [ 'jquery' ], ENVIRA_VERSION, false );

		wp_enqueue_script( ENVIRA_SLUG . '-admin-notice-script' );
		wp_localize_script(
			ENVIRA_SLUG . '-admin-notice-script',
			'envira_gallery_admin',
			[
				'ajax'                 => admin_url( 'admin-ajax.php' ),
				'dismiss_notice_nonce' => wp_create_nonce( 'envira-gallery-dismiss-notice' ),
			]
		);
		wp_enqueue_style( 'envira-notice-css', plugins_url( 'assets/css/notices.css', ENVIRA_FILE ), false, ENVIRA_VERSION );

		// Bail if we're not on the Envira Post Type screen or in widgets.
		if ( 'envira' !== $screen->post_type && 'envira_album' !== $screen->post_type && 'widgets' !== $screen->base ) {
			return;
		}
		// Load necessary admin scripts.
		wp_register_script( ENVIRA_SLUG . '-admin-script', envira_script( 'assets/js/min/admin-min.js', ENVIRA_FILE ), [ 'jquery' ], ENVIRA_VERSION, [] );
		wp_enqueue_script( ENVIRA_SLUG . '-choice-js', plugins_url( 'assets/js/lib/choices.min.js', ENVIRA_FILE ), [ 'jquery' ], ENVIRA_VERSION, false );

		if ( 'envira_page_envira-gallery-settings' === $hook ) {
			$permissions = new Permissions();
			$permissions->envira_permissions_localize();

			// For permisson modal.
			wp_register_script( ENVIRA_SLUG . '-permissions-modal-js', plugins_url( 'assets/js/lib/a11y-dialog.min.js', ENVIRA_FILE ), [], ENVIRA_VERSION, false );
			wp_enqueue_script( ENVIRA_SLUG . '-permissions-modal-js' );
		}

		wp_enqueue_script( ENVIRA_SLUG . '-admin-script' );
		wp_localize_script(
			ENVIRA_SLUG . '-admin-script',
			'envira_gallery_admin_notifications',
			[
				'dismiss_notification_nonce' => wp_create_nonce( 'envira_dismiss_notification' ),
			]
		);

		if ( 'widgets.php' === $hook || 'post.php' === $hook || 'envira_page_envira-gallery-settings' === $hook || ( 'post-new.php' === $hook && isset( $_GET['post_type'] ) && 'envira' === $_GET['post_type'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended

			wp_enqueue_style( 'envira-choice-css', plugins_url( 'assets/css/choices.css', ENVIRA_FILE ), false, ENVIRA_VERSION );
		}

		// Load gallery convert script.
		wp_register_script( ENVIRA_SLUG . '-convert-gallery-script', envira_script( 'assets/js/min/gallery-convert-min.js', ENVIRA_FILE ), [ 'jquery' ], ENVIRA_VERSION, false );

		wp_enqueue_script( ENVIRA_SLUG . '-convert-gallery-script' );

		wp_localize_script(
			ENVIRA_SLUG . '-convert-gallery-script',
			'envira_gallery_convert',
			[
				'bulk_convert_rest_url'      => rest_url( 'envira-convert/v1/bulk-convert' ),
				'process_item_rest_url'      => rest_url( 'envira-convert/v1/process-gallery' ),
				'gallery_convert_rest_nonce' => wp_create_nonce( 'wp_rest' ),
				'bulk_confirmation_alert'    => __( 'Are you sure you want to convert? This action cannot be reversed.', 'envira-gallery' ),
				'bulk_conversion_started'    => __( 'Gallery conversion in progress... Please do not close this window or refresh the page until the process is complete.', 'envira-gallery' ),
				'found_posts_text'           => __( 'posts found containing WordPress galleries.', 'envira-gallery' ),
				'conversion_completed'       => __( 'Gallery conversion completed.', 'envira-gallery' ),
				'failed_conversion_logs'     => __( 'Failed Conversion Logs:', 'envira-gallery' ),
				'post_id_text'               => __( 'Post ID', 'envira-gallery' ),
				'edit_post_text'             => __( 'Edit Post', 'envira-gallery' ),
			]
		);

		// Fire a hook to load in custom admin scripts.
		do_action( 'envira_gallery_admin_scripts' );
	}

	/**
	 * Deletes the Envira gallery association for the image being deleted.
	 *
	 * @since 1.0.0
	 *
	 * @param int $attach_id The attachment ID being deleted.
	 */
	public function delete_gallery_association( $attach_id ) {

		$has_gallery = get_post_meta( $attach_id, '_eg_has_gallery', true );

		// Only proceed if the image is attached to any Envira galleries.
		if ( ! empty( $has_gallery ) ) {
			foreach ( (array) $has_gallery as $post_id ) {
				// Remove the in_gallery association.
				$in_gallery = get_post_meta( $post_id, '_eg_in_gallery', true );
				if ( ! empty( $in_gallery ) ) {
					$in_key = array_search( $attach_id, (array) $in_gallery, true );
					if ( false !== $in_key && ! empty( $key ) && isset( $in_gallery[ $key ] ) ) {
						unset( $in_gallery[ $key ] );
					}
				}

				update_post_meta( $post_id, '_eg_in_gallery', $in_gallery );

				// Remove the image from the gallery altogether.
				$gallery_data = get_post_meta( $post_id, '_eg_gallery_data', true );
				if ( ! empty( $gallery_data['gallery'] ) ) {
					unset( $gallery_data['gallery'][ $attach_id ] );
				}

				// Update the post meta for the gallery.
				update_post_meta( $post_id, '_eg_gallery_data', $gallery_data );

				// Flush necessary gallery caches.
				envira_flush_gallery_caches( $post_id, ( ! empty( $gallery_data['config']['slug'] ) ? $gallery_data['config']['slug'] : '' ) );
			}
		}
	}

	/**
	 * Trash a gallery when the gallery post type is trashed.
	 *
	 * @since 1.0.0
	 *
	 * @param integer $id   The post ID being trashed.
	 * @return null Return early if no gallery is found.
	 */
	public function trash_gallery( $id ) {
		$gallery = get_post( $id );

		// Return early if not an Envira gallery.
		if ( 'envira' !== $gallery->post_type ) {
			return;
		}

		// Flush necessary gallery caches to ensure trashed galleries are not showing.
		envira_flush_gallery_caches( $id );

		// Check some gallery data exists.
		$gallery_data = get_post_meta( $id, '_eg_gallery_data', true );
		if ( empty( $gallery_data ) ) {
			return;
		}

		// Set the gallery status to inactive.
		$gallery_data['status'] = 'inactive';
		update_post_meta( $id, '_eg_gallery_data', $gallery_data );

		// Allow other addons to run routines when a Gallery is trashed.
		do_action( 'envira_gallery_trash', $id, $gallery_data );
	}

	/**
	 * Untrash a gallery when the gallery post type is untrashed.
	 *
	 * @since 1.0.0
	 *
	 * @param integer $id   The post ID being untrashed.
	 * @return null Return early if no gallery is found.
	 */
	public function untrash_gallery( $id ) {

		$gallery = get_post( $id );

		// Flush necessary gallery caches to ensure untrashed galleries are showing.
		envira_flush_gallery_caches( $id );

		// Return early if not an Envira gallery.
		if ( 'envira' !== $gallery->post_type ) {
			return;
		}

		// Set the gallery status to inactive.
		$gallery_data = get_post_meta( $id, '_eg_gallery_data', true );
		if ( empty( $gallery_data ) ) {
			return;
		}

		if ( isset( $gallery_data['status'] ) ) {
			unset( $gallery_data['status'] );
		}

		update_post_meta( $id, '_eg_gallery_data', $gallery_data );

		// Allow other addons to run routines when a Gallery is untrashed.
		do_action( 'envira_gallery_untrash', $id, $gallery_data );
	}

	/**
	 * Fired when a gallery is about to be permanently deleted from Trash
	 *
	 * Checks if the media_delete setting is enabled, and if so safely deletes
	 * media that isn't being used elsewhere on the site
	 *
	 * @since 1.3.6.1
	 *
	 * @param int $id Post ID.
	 * @return null
	 */
	public function delete_gallery( $id ) {

		// Check if the media_delete setting is enabled.
		$media_delete = envira_get_setting( 'media_delete' );
		if ( '1' !== $media_delete ) {
			return;
		}

		// Get post.
		$gallery = get_post( $id );

		// Flush necessary gallery caches to ensure untrashed galleries are showing.
		envira_flush_gallery_caches( $id );

		// Return early if not an Envira gallery.
		if ( 'envira' !== $gallery->post_type ) {
			return;
		}

		// Determine what images are inside this gallery, thanks to Envira meta-data.
		$in_gallery = get_post_meta( $id, '_eg_in_gallery', true );
		if ( ! is_array( $in_gallery ) ) {
			return;
		}

		// Iterate through media, deleting - making sure to delete only images that aren't in another gallery.
		foreach ( $in_gallery as $attach_id ) {

			$attachment  = get_post( $attach_id );
			$has_gallery = get_post_meta( $attach_id, '_eg_has_gallery', true );

			// If post parent is the Gallery ID, and the image isn't in another gallery, we're OK to delete the image.
			if ( ( $attachment->post_parent === $id || in_array( $attach_id, $in_gallery, true ) ) && ( count( [ $has_gallery ] ) === 1 ) ) { // the "1" should mean only one gallery - the one we are deleting.
				wp_delete_attachment( $attach_id );
			}
		}
	}

	/**
	 * I'm sure some plugins mean well, but they go a bit too far trying to reduce
	 * conflicts without thinking of the consequences.
	 *
	 * 1. Elementor doesn't play nice with instagram activation, settings, and envira things
	 *
	 * @since 1.0.0
	 */
	public function plugin_humility() {

		global $wp_scripts;

		if ( ! is_admin() ) {
			return;
		}

		if ( class_exists( 'Elementor\\Admin' ) ) {
			// Remove Custom Post Type Support For Various Plugins In The Admin.
			remove_post_type_support( 'envira', 'elementor' );
			remove_post_type_support( 'envira_albums', 'elementor' );
			if ( isset( $_GET['post'] ) && ( 'envira' === get_post_type( intval( $_GET['post'] ) ) || 'envira_albums' === get_post_type( intval( $_GET['post'] ) ) ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
				wp_dequeue_script( 'elementor-admin-app' );
			}
		}

		if ( is_plugin_active( 'advanced-rich-text-tools/index.php' ) ) {
			if ( isset( $_GET['post'] ) && ( 'envira' === get_post_type( intval( $_GET['post'] ) ) || 'envira_albums' === get_post_type( intval( $_GET['post'] ) ) ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
				wp_dequeue_script( 'advanced-rich-text-tools' );
			}
		}
	}

	/**
	 * Specific function that clears gallery cache when a photo gets replaced by other third-party plugins.
	 *
	 * @since 1.9.3.1
	 *
	 * @param string $target_url Target URL.
	 * @param string $source_url Source URL.
	 * @param int    $post_id Post ID.
	 */
	public function emr_clear_envira_cache( $target_url = false, $source_url = false, $post_id = false ) {

		$has_gallery = get_post_meta( $post_id, '_eg_has_gallery', true );

		if ( count( (array) $has_gallery ) > 0 ) :

			foreach ( $has_gallery as $gallery_id ) {
				envira_flush_gallery_caches( $gallery_id );
			}

		endif;
	}

	/**
	 * Callback for the Envira Gallery Media Settings section.
	 *
	 * @return void
	 */
	public function envira_gallery_media_settings_add_new_section() {
		// Register a new section in the "Media" settings page.
		add_settings_section(
			'envira_gallery_convert_section',
			__( 'Envira Gallery', 'envira-gallery' ),
			[ $this, 'envira_gallery_convert_section_callback' ],
			'media'
		);
	}

	/**
	 * Callback for the Envira Gallery content section.
	 *
	 * @return void
	 */
	public function envira_gallery_convert_section_callback() {
		?>
		<p><?php esc_html_e( 'Convert all of your existing WP Galleries to Envira Galleries. Take advantage of advanced options like drag and drop features, themes, lightboxes, and much more.', 'envira-gallery' ); ?></p>
		<?php
		$convert_button = __( 'Convert to Envira Gallery', 'envira-gallery' );
		?>
		<p><a href="<?php echo esc_url( admin_url( 'edit.php?post_type=envira&page=envira-gallery-settings#!envira-tab-convert_to_envira' ) ); ?>" title="<?php echo esc_attr( $convert_button ); ?>" class="button button-secondary envira-media-settings-convert-btn"><?php echo esc_attr( $convert_button ); ?></a></p>
		<?php
	}

	/**
	 * Renders the post types dropdown.
	 *
	 * @return void
	 */
	public function envira_render_post_types_dropdown() {
		// Exclusions for keywords, exact names, and labels.
		$exclusions = [
			'keywords'    => [ 'envira', 'ngg', 'nextgen', 'photocrati', 'gallery', 'elementor' ],
			'exact_names' => [ 'e-landing-page' ],
			'labels'      => [ 'NextGEN', 'Gallery' ],
		];

		// Retrieve all custom post types.
		$post_types = get_post_types( [ '_builtin' => false ], 'objects' );

		// Filter post types based on exclusions.
		$filtered_post_types = array_filter(
			$post_types,
			function ( $post_type ) use ( $exclusions ) {
				// Exclude based on keywords in the post type name.
				foreach ( $exclusions['keywords'] as $keyword ) {
					if ( strpos( $post_type->name, $keyword ) !== false ) {
						return false;
					}
				}

				// Exclude based on exact post type names.
				if ( in_array( $post_type->name, $exclusions['exact_names'], true ) ) {
					return false;
				}

				// Exclude based on labels.
				foreach ( $exclusions['labels'] as $label ) {
					if ( strpos( $post_type->label, $label ) !== false ) {
						return false;
					}
				}

				return true; // Include post type if it passes all checks.
			}
		);

		// Allow users to modify the post types list.
		$filtered_post_types = apply_filters( 'envira_convert_post_types', $filtered_post_types );
		?>
		<div class="envira-posttype-dropdown-section">
			<select name="envira_convert_post_types_dropdown" id="envira_convert_post_types_dropdown">
				<option value=""><?php esc_html_e( 'Select Post Type for Conversion', 'envira-gallery' ); ?></option>
				<option value="post"><?php esc_html_e( 'Posts', 'envira-gallery' ); ?></option>
				<option value="page"><?php esc_html_e( 'Pages', 'envira-gallery' ); ?></option>
				<?php
				foreach ( $filtered_post_types as $post_type ) {
					?>
					<option value="<?php echo esc_html( $post_type->name ); ?>">
						<?php echo esc_html( $post_type->label ); ?>
					</option>
					<?php
				}
				?>
			</select>
			<br />
			<span class="envira-posttype-dropdown-error"><?php esc_html_e( 'Please select a post type for conversion.', 'envira-gallery' ); ?></span>
		</div>
		<?php
	}

	/**
	 * Add link target.
	 */
	public function admin_cdn_menu_target_blank() {
		?>
		<script type="text/javascript">
			jQuery(document).ready(function($) {
				$('span.cdn_new_badge').closest('li').find('a').attr('target', '_blank');
			});
		</script>
		<?php
	}
}

?>
