<?php
if ( ! defined( 'ABSPATH' ) ) {
	die( 'You are not allowed to call this page directly.' );
}

class FrmStylesController {

	/**
	 * @var string
	 */
	public static $post_type = 'frm_styles';

	/**
	 * @var string
	 */
	public static $screen = 'formidable_page_formidable-styles';

	/**
	 * @var string|null
	 */
	private static $message;

	/**
	 * Cache of the active style object keyed by form ID.
	 *
	 * @since 6.32
	 *
	 * @var array
	 */
	private static $active_style = array();

	/**
	 * @return void
	 */
	public static function load_pro_hooks() {
		if ( FrmAppHelper::pro_is_installed() ) {
			FrmProStylesController::load_pro_hooks();
		}
	}

	/**
	 * @return void
	 */
	public static function register_post_types() {
		register_post_type(
			self::$post_type,
			array(
				'label'           => __( 'Styles', 'formidable' ),
				'public'          => false,
				'show_ui'         => false,
				'capability_type' => 'page',
				'capabilities'    => array(
					'edit_post'          => 'frm_change_settings',
					'edit_posts'         => 'frm_change_settings',
					'edit_others_posts'  => 'frm_change_settings',
					'publish_posts'      => 'frm_change_settings',
					'delete_post'        => 'frm_change_settings',
					'delete_posts'       => 'frm_change_settings',
					'read_private_posts' => 'read_private_posts',
				),
				'supports'        => array(
					'title',
				),
				'has_archive'     => false,
				'labels'          => array(
					'name'          => __( 'Styles', 'formidable' ),
					'singular_name' => __( 'Style', 'formidable' ),
					'menu_name'     => __( 'Style', 'formidable' ),
					'edit'          => __( 'Edit', 'formidable' ),
					'add_new_item'  => __( 'Create a New Style', 'formidable' ),
					'edit_item'     => __( 'Edit Style', 'formidable' ),
				),
			)
		);
	}

	/**
	 * Add two links for the visual styler.
	 * There's a "Styles" submenu in the Formidable menu.
	 * There's a second alternative "Forms" submenu in the Appearance menu.
	 * This submenu links to a page to edit a form with the default style.
	 *
	 * @return void
	 */
	public static function menu() {
		add_submenu_page( 'formidable', 'Formidable | ' . __( 'Styles', 'formidable' ), __( 'Styles', 'formidable' ), 'frm_change_settings', 'formidable-styles', 'FrmStylesController::route' ); // phpcs:ignore SlevomatCodingStandard.Files.LineLength.LineTooLong
		add_submenu_page( 'themes.php', 'Formidable | ' . __( 'Styles', 'formidable' ), __( 'Forms', 'formidable' ), 'frm_change_settings', 'formidable-styles2', 'FrmStylesController::route' ); // phpcs:ignore SlevomatCodingStandard.Files.LineLength.LineTooLong
	}

	/**
	 * Remove filters for the visual styler preview.
	 * This triggers earlier than self::admin_init which is called too late.
	 *
	 * @return void
	 */
	public static function plugins_loaded() {
		if ( ! FrmAppHelper::is_style_editor_page() ) {
			return;
		}

		self::disable_form_css();
		self::prevent_form_scripts_from_loading();
	}

	/**
	 * Avoid loading CSS the normal way with the preview in the styler.
	 * It gets loaded instead with the frmpro_css action set to the #frm-custom-theme-css element.
	 *
	 * @since 6.0
	 *
	 * @return void
	 */
	private static function disable_form_css() {
		add_filter( 'get_frm_stylesheet', '__return_empty_array' );
	}

	/**
	 * Removing this action prevents front end JavaScript from loading.
	 *
	 * @since 6.0
	 *
	 * @return void
	 */
	private static function prevent_form_scripts_from_loading() {
		remove_action( 'init', 'FrmFormsController::front_head' );
	}

	/**
	 * @return void
	 */
	public static function admin_init() {
		self::maybe_hook_into_global_settings_save();

		if ( ! FrmAppHelper::is_style_editor_page() ) {
			return;
		}

		FrmStyleComponent::register_assets();
		self::load_pro_hooks();

		$version = FrmAppHelper::plugin_version();

		if ( FrmAppHelper::is_style_editor_page( 'edit' ) ) {
			wp_enqueue_style( 'wp-color-picker' );
		}

		wp_enqueue_style( 'frm-custom-theme', admin_url( 'admin-ajax.php?action=frmpro_css&frm_scope_custom_css=1' ), array(), $version );

		$style = apply_filters( 'frm_style_head', false );

		if ( $style ) {
			wp_enqueue_style( 'frm-single-custom-theme', admin_url( 'admin-ajax.php?action=frmpro_load_css&flat=1' ) . '&' . http_build_query( $style->post_content ), array(), $version ); // phpcs:ignore SlevomatCodingStandard.Files.LineLength.LineTooLong
		}
	}

	/**
	 * @since 6.0
	 *
	 * @return void
	 */
	private static function maybe_hook_into_global_settings_save() {
		if ( empty( $_POST ) || ! isset( $_POST['style'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
			// Avoid changing any style data if the style array is not sent in the request.
			return;
		}

		add_action(
			'frm_update_settings',
			/**
			 * Update the form data on the "Manage Styles" tab after global settings are saved.
			 */
			function () {
				self::manage_styles();
			}
		);
	}

	/**
	 * @param string $register Either 'enqueue' or 'register'.
	 * @param bool   $force True to enqueue/register the style if a form has not been loaded.
	 *
	 * @return void
	 */
	public static function enqueue_css( $register = 'enqueue', $force = false ) {
		global $frm_vars;

		$register_css = $register === 'register';
		$should_load  = $force || ( ( $frm_vars['load_css'] || $register_css ) && ! FrmAppHelper::is_admin() );

		if ( ! $should_load ) {
			return;
		}

		$frm_settings = FrmAppHelper::get_settings();

		if ( $frm_settings->load_style === 'none' ) {
			return;
		}

		$css = apply_filters( 'get_frm_stylesheet', self::custom_stylesheet() );

		if ( $css ) {
			$css     = (array) $css;
			$version = FrmAppHelper::plugin_version();

			foreach ( $css as $css_key => $file ) {
				if ( $register_css ) {
					$this_version = self::get_css_version( $css_key, $version );
					wp_register_style( $css_key, $file, array(), $this_version );
				}

				$load_on_all = ! FrmAppHelper::is_admin() && 'all' === $frm_settings->load_style;

				if ( $load_on_all || $register !== 'register' ) {
					wp_enqueue_style( $css_key );
				}
				unset( $css_key, $file );
			}

			if ( $frm_settings->load_style === 'all' ) {
				$frm_vars['css_loaded'] = true;
			}
		}//end if
		unset( $css );

		add_filter( 'style_loader_tag', 'FrmStylesController::add_tags_to_css', 10, 2 );
	}

	/**
	 * @return array<string,string>
	 */
	public static function custom_stylesheet() {
		global $frm_vars;
		$stylesheet_urls = array();

		if ( empty( $frm_vars['css_loaded'] ) ) {
			// Include css in head.
			self::get_url_to_custom_style( $stylesheet_urls );
		}

		return $stylesheet_urls;
	}

	/**
	 * @param array $stylesheet_urls
	 *
	 * @return void
	 */
	private static function get_url_to_custom_style( &$stylesheet_urls ) {
		$add_css_to_uploads_dir = FrmStyle::add_css_to_uploads_dir();
		$file_path              = FrmStyle::get_generated_css_file_path( $add_css_to_uploads_dir ) . '/' . self::get_file_name();

		if ( ! is_readable( $file_path ) ) {
			$stylesheet_urls['formidable'] = admin_url( 'admin-ajax.php?action=frmpro_css' );
			return;
		}

		$base_url                      = $add_css_to_uploads_dir ? wp_upload_dir()['baseurl'] . '/formidable/css/' : FrmAppHelper::plugin_url() . '/css/';
		$stylesheet_urls['formidable'] = $base_url . self::get_file_name();
	}

	/**
	 * Use a different stylesheet per site in a multisite install
	 *
	 * @since 3.0.03
	 *
	 * @return string
	 */
	public static function get_file_name() {
		if ( is_multisite() ) {
			return 'formidableforms' . absint( get_current_blog_id() ) . '.css';
		}

		return 'formidableforms.css';
	}

	/**
	 * @param string $css_key
	 * @param string $version
	 *
	 * @return string
	 */
	private static function get_css_version( $css_key, $version ) {
		if ( 'formidable' !== $css_key ) {
			return $version;
		}

		$this_version = get_option( 'frm_last_style_update' );

		return $this_version ? $this_version : $version;
	}

	/**
	 * @param string $tag
	 * @param string $handle
	 *
	 * @return string
	 */
	public static function add_tags_to_css( $tag, $handle ) {
		if ( ( 'formidable' === $handle || 'jquery-theme' === $handle ) && ! str_contains( $tag, ' property=' ) ) {
			return str_replace( ' type="', ' property="stylesheet" type="', $tag );
		}

		return $tag;
	}

	/**
	 * Route the edit route to the style function.
	 *
	 * @since 6.0 this function no longer has any parameter values.
	 *
	 * @return void
	 */
	public static function edit() {
		self::load_styler();
	}

	/**
	 * When a new style route is hit, show a new style with the defaults. There is no ID yet, it is created after the first save event.
	 *
	 * @return void
	 */
	public static function new_style() {
		self::load_styler();
	}

	/**
	 * When the duplicate route is hit, it just shows the visual styler with a copy of the target style. There is no ID yet, it is created after the first save event.
	 *
	 * @return void
	 */
	public static function duplicate() {
		self::load_styler();
	}

	/**
	 * Render the style page for a form for assigning a style to a form, and for updating a target style.
	 *
	 * @since 6.0 this function no longer takes parameters.
	 *
	 * @return void
	 */
	public static function load_styler() {
		if ( 'assign_style' === FrmAppHelper::get_post_param( 'frm_action' ) ) {
			self::save_form_style();
		}

		self::setup_styles_and_scripts_for_styler();

		$style_id = self::get_style_id_for_styler();

		if ( ! $style_id ) {
			$error_args = array(
				'title'      => __( 'No styles', 'formidable' ),
				'body'       => __( 'You must have a style to use the Visual Styler.', 'formidable' ),
				'cancel_url' => admin_url( 'admin.php?page=formidable' ),
			);
			FrmAppController::show_error_modal( $error_args );
			return;
		}

		$form_id = FrmAppHelper::simple_get( 'form', 'absint', 0 );

		if ( ! $form_id ) {
			$form_id = self::get_form_id_for_style( $style_id );
		}

		$form = FrmForm::getOne( $form_id );

		if ( ! is_object( $form ) ) {
			$error_args = array(
				'title'      => __( 'No forms', 'formidable' ),
				'body'       => __( 'You must have a form to use the Visual Styler.', 'formidable' ),
				'cancel_url' => admin_url( 'admin.php?page=formidable' ),
			);
			FrmAppController::show_error_modal( $error_args );
			return;
		}

		$frm_style    = new FrmStyle( $style_id );
		$active_style = $frm_style->get_one();

		self::disable_admin_page_styling_on_submit_buttons();

		/**
		 * @since 6.0
		 *
		 * @param array{form:\stdClass} $data
		 */
		do_action( 'frm_before_render_style_page', compact( 'form' ) );

		self::render_style_page( $active_style, $form, self::get_default_style() );
	}

	/**
	 * @since 6.0
	 *
	 * @return int
	 */
	private static function get_style_id_for_styler() {
		$action = FrmAppHelper::simple_get( 'frm_action' );

		if ( 'duplicate' === $action ) {
			// The duplicate action uses style_id instead of id for better backward compatibility.
			return FrmAppHelper::simple_get( 'style_id', 'absint', 0 );
		}

		$style_id = FrmAppHelper::simple_get( 'id', 'absint', 0 );

		if ( $style_id ) {
			// Always use the style ID from the URL if one is specified.
			return $style_id;
		}

		$request_form_id = FrmAppHelper::simple_get( 'form', 'absint', 0 );

		if ( $request_form_id && is_callable( 'FrmProStylesController::get_active_style_for_form' ) ) {
			return FrmProStylesController::get_active_style_for_form( $request_form_id )->ID;
		}

		return self::get_default_style()->ID;
	}

	/**
	 * If a form ID is not being passed in the URL, try to get the best match.
	 *
	 * @since 6.0
	 *
	 * @param int $style_id
	 *
	 * @return int
	 */
	private static function get_form_id_for_style( $style_id ) {
		$check   = serialize( array( 'custom_style' => (string) $style_id ) );
		$check   = substr( $check, 5, -1 );
		$form_id = FrmDb::get_var(
			'frm_forms',
			array(
				'options LIKE' => $check,
				'status'       => 'published',
			)
		);

		if ( ! $form_id ) {
			// Fallback to any form.
			$where   = array(
				'status'         => 'published',
				// Make sure it's not a repeater.
				'parent_form_id' => array( null, 0 ),
			);
			$form_id = FrmDb::get_var( 'frm_forms', $where, 'id' );
		}

		return $form_id;
	}

	/**
	 * Add a frm_no_style_button class to all buttons to avoid some style rules like border-radius: 30px.
	 *
	 * @return void
	 */
	private static function disable_admin_page_styling_on_submit_buttons() {
		add_filter(
			'frm_submit_button_class',
			function ( $classes ) {
				$classes[] = 'frm_no_style_button';
				return $classes;
			}
		);
	}

	/**
	 * @since 6.0
	 *
	 * @return WP_Post
	 */
	private static function get_default_style() {
		$frm_style = new FrmStyle( 'default' );
		return $frm_style->get_one();
	}

	/**
	 * Save style for form (from Styler list page) via a POST action.
	 *
	 * @since 6.0
	 *
	 * @return void
	 */
	private static function save_form_style() {
		$permission_error = FrmAppHelper::permission_nonce_error( 'frm_edit_forms', 'frm_save_form_style', 'frm_save_form_style_nonce' );

		if ( $permission_error !== false ) {
			wp_die( 'Unable to save form', '', 403 );
		}

		$style_id = FrmAppHelper::get_post_param( 'style_id', 0, 'absint' );

		if ( $style_id && ! self::confirm_style_exists_before_setting( $style_id ) ) {
			wp_die( esc_html__( 'Invalid target style', 'formidable' ), esc_html__( 'Invalid target style', 'formidable' ), 400 );
			return;
		}

		/**
		 * Hook into the saved style ID so Pro can import a style template by its key and return a new style ID.
		 *
		 * @since 6.0
		 *
		 * @param int $style_id
		 */
		$style_id = apply_filters( 'frm_saved_form_style_id', $style_id );

		if ( ! $style_id && '0' !== FrmAppHelper::get_post_param( 'style_id', '', 'sanitize_text_field' ) ) {
			// "0" is a special value used for the enable/disable toggle.
			wp_die( esc_html__( 'Invalid style value', 'formidable' ), esc_html__( 'Invalid style value', 'formidable' ), 400 );
			return;
		}

		$form_id = FrmAppHelper::get_post_param( 'form_id', 0, 'absint' );

		if ( ! $form_id ) {
			wp_die( esc_html__( 'No form specified', 'formidable' ), esc_html__( 'No form specified', 'formidable' ), 400 );
			return;
		}

		$form = FrmForm::getOne( $form_id );

		if ( ! $form ) {
			wp_die( esc_html__( 'Form does not exist', 'formidable' ), esc_html__( 'Form does not exist', 'formidable' ), 400 );
			return;
		}

		$previous_style_id = $form->options['custom_style'];

		// If the default style is selected, use the "Always use default" legacy option instead of the default style.
		// There's also a check here for conversational forms.
		// Without the check it isn't possible to select "Default" because "Always use default" will convert to "Lines" dynamically.

		if ( $style_id === self::get_default_style()->ID && empty( $form->options['chat'] ) ) {
			$style_id = 1;
		}

		// We want to save a string for consistency. FrmStylesHelper::get_form_count_for_style expects the custom style ID is a string.
		$form->options['custom_style'] = (string) $style_id;

		if ( $previous_style_id === $form->options['custom_style'] ) {
			// Exit early before updating DB if nothing actually changed.
			self::$message = __( 'Successfully updated style.', 'formidable' );
			return;
		}

		global $wpdb;
		$wpdb->update( $wpdb->prefix . 'frm_forms', array( 'options' => maybe_serialize( $form->options ) ), array( 'id' => $form->id ) );

		FrmForm::clear_form_cache();

		/**
		 * @since 6.25.1
		 *
		 * @param int $form_id
		 * @param int $style_id
		 */
		do_action( 'frm_after_changed_form_style', absint( $form_id ), absint( $style_id ) );

		self::$message = __( 'Successfully updated style.', 'formidable' );
	}

	/**
	 * Validate that we're assigning a form to a style that actually exists before assigning it to a form.
	 *
	 * @param int $style_id
	 *
	 * @return bool True if the style actually exists.
	 */
	private static function confirm_style_exists_before_setting( $style_id ) {
		global $wpdb;
		$post_type = FrmDb::get_var( $wpdb->posts, array( 'ID' => $style_id ), 'post_type' );
		return self::$post_type === $post_type;
	}

	/**
	 * Register and enqueue styles and scripts for the style tab page.
	 *
	 * @since 6.0
	 *
	 * @return void
	 */
	private static function setup_styles_and_scripts_for_styler() {
		$plugin_url      = FrmAppHelper::plugin_url();
		$version         = FrmAppHelper::plugin_version();
		$js_dependencies = array( 'wp-i18n', 'wp-hooks', 'formidable_dom' );

		if ( FrmAppHelper::pro_is_installed() && is_callable( 'FrmProAppHelper::use_jquery_datepicker' ) && FrmProAppHelper::use_jquery_datepicker() ) {
			$js_dependencies[] = 'jquery-ui-datepicker';
		}

		wp_register_script( 'formidable_style', $plugin_url . '/js/admin/style.js', $js_dependencies, $version );
		wp_register_style( 'formidable_style', $plugin_url . '/css/admin/style.css', array(), $version );
		wp_print_styles( 'formidable_style' );

		wp_print_styles( 'formidable' );
		wp_enqueue_script( 'formidable_style' );
		wp_set_script_translations( 'formidable_style', 'formidable' );
	}

	/**
	 * Render the style page (with a more limited and typed scope than calling it from self::style directly).
	 *
	 * @since 6.0
	 *
	 * @param stdClass|WP_Post $active_style
	 * @param stdClass         $form
	 * @param WP_Post          $default_style
	 *
	 * @return void
	 */
	private static function render_style_page( $active_style, $form, $default_style ) {
		$style_views_path = self::get_views_path();
		// Edit, list (default), new_style.
		$view      = FrmAppHelper::simple_get( 'frm_action', 'sanitize_text_field', 'list' );
		$frm_style = new FrmStyle( $active_style->ID );

		if ( 'new_style' !== $view && ! FrmAppHelper::simple_get( 'form' ) && ! FrmAppHelper::simple_get( 'style_id' ) ) {
			// Have the Appearance > Forms link fallback to the edit view. Otherwise we want to use 'list' as the default.
			$view = 'edit';
		}

		if ( in_array( $view, array( 'edit', 'new_style', 'duplicate' ), true ) ) {
			self::add_meta_boxes();
		}

		switch ( $view ) {
			case 'edit':
				$style = $active_style;
				break;

			case 'duplicate':
				$style     = clone $active_style;
				$new_style = $frm_style->get_new();
				$new_name  = self::get_new_style_post_name( $new_style->post_name );

				// The single style custom CSS is nested under the old style's scope. Re-scope it to the
				// new style's scope so it unnests correctly for display and re-nests correctly on save.
				if ( ! empty( $style->post_content['single_style_custom_css'] ) ) {
					$css_scope_helper                               = new FrmCssScopeHelper();
					$unnested_css                                   = $css_scope_helper->unnest( $style->post_content['single_style_custom_css'], 'frm_style_' . $style->post_name ); // phpcs:ignore SlevomatCodingStandard.Files.LineLength.LineTooLong
					$style->post_content['single_style_custom_css'] = $css_scope_helper->nest( $unnested_css, 'frm_style_' . $new_name );
				}

				$style->ID        = $new_style->ID;
				$style->post_name = $new_name;
				unset( $new_style, $new_name );
				break;

			case 'new_style':
				$style            = $frm_style->get_new();
				$style->post_name = self::get_new_style_post_name( $style->post_name );
				break;
		}//end switch

		if ( in_array( $view, array( 'duplicate', 'new_style' ), true ) ) {
			// A new or duplicated style has no ID yet, so the Rename modal updates the post_title input
			// instead of calling an endpoint. Prefer that posted title over $_GET['style_name'] when present.
			$posted_title = '';

			// phpcs:ignore WordPress.Security.NonceVerification.Missing
			if ( isset( $_POST['frm_style_setting']['post_title'] ) ) {
				// The nonce is verified in FrmStylesController::save_style before this renders.
				// phpcs:ignore WordPress.Security.NonceVerification.Missing
				$posted_title = sanitize_text_field( wp_unslash( $_POST['frm_style_setting']['post_title'] ) );
			}

			$style->post_title = '' !== $posted_title ? $posted_title : FrmAppHelper::simple_get( 'style_name' );
			$style->menu_order = 0;
		}

		if ( ! isset( $style ) ) {
			$style = $active_style;
		}

		self::force_form_style( $style );

		if ( isset( self::$message ) ) {
			$message = self::$message;
		}

		$preview_helper = new FrmStylesPreviewHelper( $form->id );
		$preview_helper->adjust_form_for_preview();

		// Get form HTML before displaying warnings and notes so we can check global $frm_vars data without adding extra database calls.
		$target_form_preview_html = $preview_helper->get_html_for_form_preview();
		$warnings                 = $preview_helper->get_warnings_for_styler_preview( $style, $default_style, $view );
		$notes                    = $preview_helper->get_notes_for_styler_preview();

		include $style_views_path . 'show.php';
	}

	/**
	 * Derive the temporary post_name (CSS scope slug) for a new or duplicated style from the chosen
	 * style name so the displayed scope matches the slug WordPress will store on save.
	 *
	 * Falls back to the auto-generated key when no style name was provided so the scope is never empty.
	 *
	 * @since 6.32
	 *
	 * @param string $fallback The auto-generated post_name to use when no style name is chosen.
	 *
	 * @return string
	 */
	private static function get_new_style_post_name( $fallback ) {
		$style_name = FrmAppHelper::simple_get( 'style_name' );
		$slug       = $style_name ? sanitize_title( $style_name ) : '';

		return $slug ? $slug : $fallback;
	}

	/**
	 * @since 6.0
	 *
	 * @return string
	 */
	private static function get_views_path() {
		return FrmAppHelper::plugin_path() . '/classes/views/styles/';
	}

	/**
	 * Filter form classes so the form uses the preview style, not the form's active style.
	 *
	 * @since 6.0
	 *
	 * @param stdClass|WP_Post $style A new style is not a WP_Post object.
	 *
	 * @return void
	 */
	private static function force_form_style( $style ) {
		add_filter(
			'frm_add_form_style_class',
			function ( $class ) use ( $style ) {
				$split   = array_filter(
					explode( ' ', $class ),
					/**
					 * @param string $class
					 */
					function ( $class ) {
						return $class && ! str_starts_with( $class, 'frm_style_' );
					}
				);
				$split[] = 'frm_style_' . $style->post_name;
				return implode( ' ', $split );
			}
		);
	}

	/**
	 * Save style post object via a POST request submitted from the Visual styler "edit" page.
	 *
	 * @since 6.0
	 *
	 * @return void
	 */
	public static function save_style() {
		FrmAppHelper::permission_check( 'frm_change_settings' );

		$frm_style   = new FrmStyle();
		$post_id     = FrmAppHelper::get_post_param( 'ID', false, 'sanitize_title' );
		$style_nonce = FrmAppHelper::get_post_param( 'frm_style', '', 'sanitize_text_field' );

		if ( $post_id === false || ! wp_verify_nonce( $style_nonce, 'frm_style_nonce' ) ) {
			// Exit early if the request isn't valid.
			// Since we're not dying, it should just reload the visual styler without any message.
			return;
		}

		$id = $frm_style->update( $post_id );

		if ( ! $post_id && $id ) {
			self::maybe_redirect_after_save( $id );
			// Set the post id to the new style so it will be loaded for editing.
			$post_id = reset( $id );
		}

		/**
		 * @since 6.25.1
		 *
		 * @param int $post_id
		 */
		do_action( 'frm_after_saved_style', absint( $post_id ) );

		self::$message = __( 'Your styling settings have been saved.', 'formidable' );
	}

	/**
	 * Show the edit view after saving.
	 * The save event is triggered earlier, on admin init where self::save_style is called.
	 * This happens earlier because there is a possible redirect (to adjust the URL for a new or duplicated style).
	 *
	 * @return void
	 */
	public static function save() {
		self::edit();
	}

	/**
	 * Force a redirect after duplicating or creating a new style to avoid an old stale URL that could result in more styles than intended.
	 *
	 * @since 6.0
	 *
	 * @param array $ids
	 *
	 * @return void
	 */
	private static function maybe_redirect_after_save( $ids ) {
		$referer             = FrmAppHelper::get_server_value( 'HTTP_REFERER' );
		$parsed              = parse_url( $referer );
		$query               = $parsed['query'];
		$current_action      = false;
		$actions_to_redirect = array( 'duplicate', 'new_style' );

		foreach ( $actions_to_redirect as $action ) {
			if ( str_contains( $query, 'frm_action=' . $action ) ) {
				$current_action = $action;
				break;
			}
		}

		if ( false === $current_action ) {
			// Do not redirect as the referer URL did not match $actions_to_redirect.
			return;
		}

		parse_str( $query, $parsed_query );
		$form_id   = ! empty( $parsed_query['form'] ) ? absint( $parsed_query['form'] ) : 0;
		$style     = new stdClass();
		$style->ID = end( $ids );
		wp_safe_redirect( esc_url_raw( FrmStylesHelper::get_edit_url( $style, $form_id ) ) );
		die();
	}

	/**
	 * @since 6.0
	 *
	 * @param string       $message
	 * @param array|object $forms
	 *
	 * @return void
	 */
	public static function manage( $message = '', $forms = array() ) {
		$frm_style     = new FrmStyle();
		$styles        = $frm_style->get_all();
		$default_style = $frm_style->get_default_style( $styles );
		$frm_settings  = FrmAppHelper::get_settings();

		if ( ! $forms ) {
			$forms = FrmForm::get_published_forms();
		}

		include FrmAppHelper::plugin_path() . '/classes/views/styles/manage.php';
	}

	/**
	 * Handle saving for the page rendered in self::manage which is included in Global Settings in the "Manage Styles" tab.
	 * This gets called from the frm_update_settings hook which is called after saving Global settings.
	 *
	 * @return void
	 */
	private static function manage_styles() {
		global $wpdb;

		$forms = FrmForm::get_published_forms();

		foreach ( $forms as $form ) {
			if ( ! isset( $_POST['style'] ) || ! isset( $_POST['style'][ $form->id ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
				continue;
			}

			$new_style = sanitize_text_field( wp_unslash( $_POST['style'][ $form->id ] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing

			$form->options['custom_style'] = $new_style;
			$wpdb->update( $wpdb->prefix . 'frm_forms', array( 'options' => maybe_serialize( $form->options ) ), array( 'id' => $form->id ) );
			unset( $form );
		}
	}

	/**
	 * Echo content for the Custom CSS page.
	 *
	 * @param string $message
	 * @param array  $extra_args An array of extra arguments.
	 *
	 * @return void
	 */
	public static function custom_css( $message = '', $extra_args = array() ) {
		$id              = $extra_args['id'] ?? 'frm_codemirror_box';
		$settings        = self::enqueue_codemirror( $id, $extra_args['placeholder'] ?? '' );
		$id              = $settings ? $id : 'frm_custom_css_box';
		$show_errors     = $extra_args['show_errors'] ?? true;
		$custom_css      = $extra_args['custom_css'] ?? self::get_custom_css();
		$heading         = $extra_args['heading'] ?? __( 'You can add custom css here or in your theme style.css. Any CSS added here will be used anywhere the Formidable CSS is loaded.', 'formidable' ); // phpcs:ignore SlevomatCodingStandard.Files.LineLength.LineTooLong
		$textarea_params = ! empty( $extra_args['textarea_params'] ) ? $extra_args['textarea_params'] : array(
			'name' => 'frm_custom_css',
			'id'   => $id,
		);

		if ( $settings ) {
			$textarea_params['class'] = 'hide-if-js';
		}

		include FrmAppHelper::plugin_path() . '/classes/views/styles/custom_css.php';
	}

	/**
	 * Get custom CSS code entered in the Custom CSS page.
	 *
	 * @since 6.0
	 *
	 * @param array|null $single_style_settings The single style settings.
	 *
	 * @return string
	 */
	public static function get_custom_css( $single_style_settings = null ) {
		if ( $single_style_settings ) {
			// If the single style settings are passed, return the custom CSS from the single style settings.
			if ( ! empty( $single_style_settings['single_style_custom_css'] ) && ! empty( $single_style_settings['enable_style_custom_css'] ) ) {
				return $single_style_settings['single_style_custom_css'];
			}

			return '';
		}

		$settings = FrmAppHelper::get_settings();

		if ( is_string( $settings->custom_css ) ) {
			return $settings->custom_css;
		}

		// If it does not exist, check the default style as a fallback.
		$frm_style = new FrmStyle();
		$style     = $frm_style->get_default_style();

		return $style->post_content['custom_css'];
	}

	/**
	 * Enqueue assets for codemirror, built into WordPress since 4.9.
	 * The Custom CSS page uses codemirror.
	 *
	 * @since 6.0 Previously this code was embedded in self::custom_css.
	 *
	 * @param string $id The ID of the codemirror box.
	 * @param string $placeholder The placeholder text for the codemirror box.
	 *
	 * @return array|false
	 */
	private static function enqueue_codemirror( $id = 'frm_codemirror_box', $placeholder = '' ) {
		if ( ! function_exists( 'wp_enqueue_code_editor' ) ) {
			// The WordPress version is likely older than 4.9.
			return false;
		}

		$settings = wp_enqueue_code_editor(
			array(
				'type'       => 'text/css',
				'codemirror' => array(
					'indentUnit'  => 2,
					'tabSize'     => 2,
					// As the codemirror box only appears once you click into the Custom CSS tab, we need to auto-refresh.
					// Otherwise the line numbers all end up with a 1px width causing overlap issues with the text in the content.
					'autoRefresh' => true,
					'placeholder' => $placeholder,
				),
			)
		);

		if ( $settings ) {
			wp_add_inline_script(
				'code-editor',
				sprintf(
					'jQuery( function() { window.%s_wp_editor = wp.codeEditor.initialize( \'%s\', %s ); } );',
					$id,
					$id,
					wp_json_encode( $settings )
				)
			);
		}

		return $settings;
	}

	/**
	 * Handling routing for the visual styler.
	 *
	 * @return void
	 */
	public static function route() {
		$action = FrmAppHelper::get_param( 'frm_action', '', 'get', 'sanitize_title' );
		FrmAppHelper::include_svg();

		switch ( $action ) {
			case 'edit':
			case 'save':
				self::$action();
				return;
			default:
				do_action( 'frm_style_action_route', $action );

				if ( apply_filters( 'frm_style_stop_action_route', false, $action ) ) {
					return;
				}

				if ( in_array( $action, array( 'new_style', 'duplicate' ), true ) ) {
					self::$action();
					return;
				}

				self::edit();
				return;
		}
	}

	/**
	 * Handle AJAX routing for frm_settings_reset for resetting styles to the default settings.
	 * From the edit view, it will return default styles and not actually update the style.
	 * On the list view, it does update the style immediately, and returns the default card style attributes so the style card can be reset as well.
	 *
	 * @since 6.0 When a style_id is passed to this action, the style will actually be reset.
	 *
	 * @return void
	 */
	public static function reset_styling() {
		FrmAppHelper::permission_check( 'frm_change_settings' );
		check_ajax_referer( 'frm_ajax', 'nonce' );

		$style_id = FrmAppHelper::get_post_param( 'style_id', '', 'absint' );

		if ( ! $style_id ) {
			// A style ID is not sent when resetting on the edit page.
			// Instead of resetting the style, send the defaults back so the inputs can be updated with JavaScript.
			$frm_style = new FrmStyle();
			echo json_encode( $frm_style->get_defaults() );
			wp_die();
		}

		$frm_style              = new FrmStyle();
		$default_template_style = $frm_style->get_default_template_style( $style_id );
		$where                  = array(
			'ID'        => $style_id,
			'post_type' => self::$post_type,
		);

		$style_object               = $frm_style->get_new();
		$style_object->post_content = json_decode( $default_template_style, true );

		global $wpdb;
		$wpdb->update( $wpdb->posts, array( 'post_content' => $default_template_style ), $where );

		// Save the settings after resetting to default or the old style will still appear.
		$frm_style->save_settings();

		$data = array(
			'style' => FrmStylesCardHelper::get_style_param_for_card( $style_object ),
		);
		wp_send_json_success( $data );
		wp_die();
	}

	/**
	 * Handle routing for the frm_change_styling AJAX action.
	 * This doesn't actually change styling. It just handles the events when someone changes a style.
	 * It responds with the new CSS required for the updated styler preview in the edit page.
	 *
	 * @return void
	 */
	public static function change_styling() {
		FrmAppHelper::permission_check( 'frm_change_settings' );
		check_ajax_referer( 'frm_ajax', 'nonce' );

		$is_loaded_via_ajax = true;
		$frm_style          = new FrmStyle();

		// Intentionally avoid defaults here so nothing gets removed from our style.
		$defaults = array();
		$style    = '';

		echo '<style type="text/css">';
		include FrmAppHelper::plugin_path() . '/css/_single_theme.css.php';
		echo '</style>';
		wp_die();
	}

	/**
	 * @return void
	 */
	public static function add_meta_boxes() {
		// Setup meta boxes
		$meta_boxes = array(
			'general'                => __( 'General', 'formidable' ),
			'form-title'             => __( 'Form Title', 'formidable' ),
			'form-description'       => __( 'Form Description', 'formidable' ),
			'field-labels'           => __( 'Field Labels', 'formidable' ),
			'field-description'      => __( 'Field Description', 'formidable' ),
			'field-colors'           => __( 'Field Colors', 'formidable' ),
			'field-sizes'            => __( 'Field Settings', 'formidable' ),
			'check-box-radio-fields' => __( 'Check Box & Radio Fields', 'formidable' ),
			'buttons'                => __( 'Buttons', 'formidable' ),
			'form-messages'          => __( 'Form Messages', 'formidable' ),
		);

		/**
		 * Add custom boxes to the styling settings
		 *
		 * @since 2.3
		 *
		 * @param array $meta_boxes
		 */
		$meta_boxes = apply_filters( 'frm_style_boxes', $meta_boxes );

		foreach ( $meta_boxes as $nicename => $name ) {
			add_meta_box( $nicename . '-style', $name, 'FrmStylesController::include_style_section', self::$screen, 'side', 'default', $nicename );
			unset( $nicename, $name );
		}
	}

	/**
	 * @param array $atts
	 * @param array $sec
	 *
	 * @return void
	 */
	public static function include_style_section( $atts, $sec ) {
		extract( $atts ); // phpcs:ignore WordPress.PHP.DontExtract
		$style = $atts['style'];
		FrmStylesHelper::prepare_color_output( $style->post_content, false );

		$current_tab = FrmAppHelper::simple_get( 'page-tab', 'sanitize_title', 'default' );
		$file_name   = FrmAppHelper::plugin_path() . '/classes/views/styles/_' . $sec['args'] . '.php';

		/**
		 * Set the location of custom styling settings right before
		 * loading onto the page. If your style box was named "progress",
		 * this hook name will be frm_style_settings_progress.
		 *
		 * @since 2.3
		 */
		$file_name = apply_filters( 'frm_style_settings_' . $sec['args'], $file_name );

		echo '<div class="frm_grid_container">';
		include $file_name;
		echo '</div>';
	}

	public static function load_css() {
		header( 'Content-type: text/css' );

		$frm_style = new FrmStyle();
		$defaults  = $frm_style->get_defaults();
		$style     = '';

		include FrmAppHelper::plugin_path() . '/css/_single_theme.css.php';
		wp_die();
	}

	/**
	 * @return void
	 */
	public static function load_saved_css() {
		$css = get_transient( 'frmpro_css' );

		ob_start();
		include FrmAppHelper::plugin_path() . '/css/custom_theme.css.php';
		$output = ob_get_clean();
		$output = self::replace_relative_url( $output );

		/**
		 * The API needs to load font icons through a custom URL.
		 *
		 * @since 5.2
		 *
		 * @param string $output
		 */
		$output = apply_filters( 'frm_saved_css', $output );

		echo $output; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped

		self::maybe_hide_sample_form_error_message();

		wp_die();
	}

	/**
	 * Add an extra style rule to hide a broken style warning.
	 * To avoid cluttering the front end with any unnecessary styles this is only added when the referer URL matches the styler.
	 *
	 * @since 6.2.3
	 *
	 * @return void
	 */
	public static function maybe_hide_sample_form_error_message() {
		$referer = FrmAppHelper::get_server_value( 'HTTP_REFERER' );

		if ( str_contains( $referer, 'admin.php?page=formidable-styles' ) ) {
			echo '#frm_broken_styles_warning { display: none; }';
		}
	}

	/**
	 * Replaces relative URL with absolute URL.
	 *
	 * @since 4.11.03
	 *
	 * @param string $css CSS content.
	 *
	 * @return string
	 */
	public static function replace_relative_url( $css ) {
		$plugin_url = trailingslashit( FrmAppHelper::plugin_url() );
		return str_replace(
			array(
				'url(../',
				"url('../",
				'url("../',
			),
			array(
				'url(' . $plugin_url,
				"url('" . $plugin_url,
				'url("' . $plugin_url,
			),
			$css
		);
	}

	/**
	 * Check if the Formidable styling should be loaded,
	 * then enqueue it for the footer
	 *
	 * @since 2.0
	 *
	 * @return void
	 */
	public static function enqueue_style() {
		global $frm_vars;

		if ( ! empty( $frm_vars['css_loaded'] ) ) {
			// The CSS has already been loaded.
			return;
		}

		$frm_settings = FrmAppHelper::get_settings();

		if ( $frm_settings->load_style === 'none' ) {
			return;
		}

		wp_enqueue_style( 'formidable' );
		$frm_vars['css_loaded'] = true;
	}

	/**
	 * Get the stylesheets for the form settings page
	 *
	 * @return array<WP_Post>
	 */
	public static function get_style_opts() {
		$frm_style = new FrmStyle();
		return $frm_style->get_all();
	}

	/**
	 * Get the style post object for a target form.
	 *
	 * @param bool|object|string $form
	 *
	 * @return WP_Post|null
	 */
	public static function get_form_style( $form = 'default' ) {
		$style = FrmFormsHelper::get_form_style( $form );

		// phpcs:ignore Universal.Operators.StrictComparisons
		if ( ! $style || 1 == $style ) {
			$style = 'default';
		}

		$frm_style = new FrmStyle( $style );
		return $frm_style->get_one();
	}

	/**
	 * Get the active style object for a field's form.
	 *
	 * @since 6.32
	 *
	 * @param array|int $field The 'field' array.
	 *
	 * @return object
	 */
	public static function get_active_style( $field ) {
		if ( ! is_array( $field ) ) {
			return new stdClass();
		}

		$form_id = $field['parent_form_id'] ?? $field['form_id'];

		if ( isset( self::$active_style[ $form_id ] ) ) {
			return self::$active_style[ $form_id ];
		}

		$active_style = self::get_form_style( $form_id );

		if ( ! is_object( $active_style ) ) {
			$active_style = new stdClass();
		}

		self::$active_style[ $form_id ] = $active_style;

		return self::$active_style[ $form_id ];
	}

	/**
	 * Get the style setting key that stores the alignment for a field type.
	 *
	 * @since 6.32
	 *
	 * @param string $field_type
	 *
	 * @return string
	 */
	public static function get_align_key_for_style_settings( $field_type ) {
		return 'checkbox' === $field_type ? 'check_align' : 'radio_align';
	}

	/**
	 * @param string $class
	 * @param string $style
	 *
	 * @return string
	 */
	public static function get_form_style_class( $class, $style ) {
		// phpcs:ignore Universal.Operators.StrictComparisons
		if ( 1 == $style ) {
			$style = 'default';
		}

		$frm_style = new FrmStyle( $style );
		$style     = $frm_style->get_one();

		if ( $style ) {
			$class .= ' frm_style_' . $style->post_name;
			self::maybe_add_rtl_class( $style, $class );
		}

		return $class;
	}

	/**
	 * @since 3.0
	 *
	 * @param object $style
	 * @param string $class
	 *
	 * @return void
	 */
	private static function maybe_add_rtl_class( $style, &$class ) {
		$is_rtl = isset( $style->post_content['direction'] ) && 'rtl' === $style->post_content['direction'];

		if ( $is_rtl ) {
			$class .= ' frm_rtl';
		}
	}

	/**
	 * @param string     $val  Style setting key.
	 * @param int|string $form Form ID or 'default'.
	 *
	 * @return mixed
	 */
	public static function get_style_val( $val, $form = 'default' ) {
		$style = self::get_form_style( $form );

		if ( $style && isset( $style->post_content[ $val ] ) ) {
			return $style->post_content[ $val ];
		}

		return null;
	}

	/**
	 * @param array $default_styles
	 *
	 * @return array
	 */
	public static function show_entry_styles( $default_styles ) {
		$frm_style = new FrmStyle( 'default' );
		$style     = $frm_style->get_one();

		if ( ! $style ) {
			return $default_styles;
		}

		foreach ( $default_styles as $name => $val ) {
			$setting = $name;

			if ( 'border_width' === $name ) {
				$setting = 'field_border_width';
			} elseif ( 'alt_bg_color' === $name ) {
				$setting = 'bg_color_active';
			}

			$default_styles[ $name ] = $style->post_content[ $setting ];
			unset( $name, $val );
		}

		return $default_styles;
	}

	/**
	 * @param string $important
	 * @param array  $field
	 *
	 * @return string
	 */
	public static function important_style( $important, $field ) {
		return self::get_style_val( 'important_style', $field['form_id'] );
	}

	/**
	 * Duplicate of WordPress do_accordion_section function, it adds an additional svg icon support.
	 *
	 * @since 6.8.3
	 *
	 * @param string|WP_Screen $screen      Screen ID or screen object.
	 * @param string           $context     Meta box context.
	 * @param mixed            $data_object Data object passed to callbacks.
	 *
	 * @return int
	 */
	public static function do_accordion_sections( $screen, $context, $data_object ) { // phpcs:ignore SlevomatCodingStandard.Complexity.Cognitive.ComplexityTooHigh
		global $wp_meta_boxes;

		// The symbol id from icons.svg
		$icon_ids = array(
			'ranking-fields-style' => 'frm_chart_bar_icon',
			'section-fields-style' => 'frm-form-title-style',
		);

		wp_enqueue_script( 'accordion' );

		if ( ! $screen ) {
			$screen = get_current_screen();
		} elseif ( is_string( $screen ) ) {
			$screen = convert_to_screen( $screen );
		}

		$page = $screen->id;

		// phpcs:disable Generic.WhiteSpace.ScopeIndent
		?>
		<div id="side-sortables" class="accordion-container">
			<ul class="outer-border">
		<?php
		$i          = 0;
		$first_open = false;

		if ( isset( $wp_meta_boxes[ $page ][ $context ] ) ) {
			foreach ( array( 'high', 'core', 'default', 'low' ) as $priority ) {
				if ( ! isset( $wp_meta_boxes[ $page ][ $context ][ $priority ] ) ) {
					continue;
				}

				foreach ( $wp_meta_boxes[ $page ][ $context ][ $priority ] as $box ) {
					if ( false === $box || ! $box['title'] ) {
						continue;
					}

					++$i;
					$icon_id    = array_key_exists( $box['id'], $icon_ids ) ? $icon_ids[ $box['id'] ] : 'frm-' . $box['id'];
					$open_class = '';

					if ( ! $first_open ) {
						$first_open = true;
						$open_class = 'open';
					}

					$accordion_content_id = 'frm_style_section_' . $box['id'];
					?>
					<li class="control-section accordion-section <?php echo esc_attr( $open_class ); ?> <?php echo esc_attr( $box['id'] ); ?>" id="<?php echo esc_attr( $box['id'] ); ?>"><?php // phpcs:ignore SlevomatCodingStandard.Files.LineLength.LineTooLong ?>
						<h3 class="accordion-section-title hndle">
							<?php
							FrmAppHelper::icon_by_class( 'frmfont ' . $icon_id . ' frm_svg24' );
							echo esc_html( $box['title'] );
							?>
							<button type="button" aria-expanded="<?php echo esc_attr( 'open' === $open_class ? 'true' : 'false' ); ?>" aria-controls="<?php echo esc_attr( $accordion_content_id ); ?>" aria-label="<?php echo esc_attr( $box['title'] ); ?>"><?php // phpcs:ignore SlevomatCodingStandard.Files.LineLength.LineTooLong ?>
								<?php FrmAppHelper::icon_by_class( 'frmfont frm_arrowdown8_icon' ); ?>
							</button>
						</h3>
						<div class="accordion-section-content <?php postbox_classes( $box['id'], $page ); ?>" id="<?php echo esc_attr( $accordion_content_id ); ?>">
							<div class="inside">
								<?php call_user_func( $box['callback'], $data_object, $box ); ?>
							</div><!-- .inside -->
						</div><!-- .accordion-section-content -->
					</li><!-- .accordion-section -->
					<?php
				}//end foreach
			}//end foreach
		}//end if
		?>
			</ul><!-- .outer-border -->
		</div><!-- .accordion-container -->
		<?php
		// phpcs:enable Generic.WhiteSpace.ScopeIndent
		return $i;
	}

	/**
	 * Rename a style via an AJAX action.
	 *
	 * @since 6.0
	 *
	 * @return void
	 */
	public static function rename_style() {
		$permission_error = FrmAppHelper::permission_nonce_error( 'frm_edit_forms', 'nonce', 'frm_ajax' );

		if ( $permission_error !== false ) {
			$data = array(
				'message' => __( 'Unable to rename style', 'formidable' ),
			);
			wp_send_json_error( $data, 403 );
			die();
		}

		$style_id   = FrmAppHelper::get_post_param( 'style_id', 0, 'absint' );
		$style_name = FrmAppHelper::get_post_param( 'style_name', '', 'sanitize_text_field' );

		if ( ! $style_id || ! $style_name ) {
			$data = array(
				'message' => __( 'Invalid route', 'formidable' ),
			);
			wp_send_json_error( $data, 400 );
			die();
		}

		$post = get_post( $style_id );

		if ( ! $post || $post->post_type !== self::$post_type ) {
			$data = array(
				'message' => __( 'The style you are renaming either does not exist or it is not a style', 'formidable' ),
			);
			wp_send_json_error( $data, 404 );
			die();
		}

		global $wpdb;
		$wpdb->update( $wpdb->posts, array( 'post_title' => $style_name ), array( 'ID' => $post->ID ) );

		$data = array();
		wp_send_json_success( $data );
	}

	/**
	 * Prevent the WordPress edit.css file from loading on the visual styler page.
	 * This way .form-field elements do not have border styles applied to them.
	 *
	 * @since 6.0
	 *
	 * @param WP_Styles $styles
	 *
	 * @return void
	 */
	public static function disable_conflicting_wp_admin_css( $styles ) {
		if ( ! FrmAppHelper::is_style_editor_page() ) {
			return;
		}

		FrmStylesPreviewHelper::disable_conflicting_wp_admin_css( $styles );
	}

	/**
	 * @deprecated 6.1 Saving custom CSS has been moved into Global Settings.
	 *
	 * @return void
	 */
	public static function save_css() {
		_deprecated_function( __METHOD__, '6.1' );
	}
}
