<?php
namespace Jet_Engine\Elementor_Views\Components;

// If this file is called directly, abort.
if ( ! defined( 'WPINC' ) ) {
	die;
}

/**
 * Define components registry class
 */
class Register {

	protected $category_registered = false;
	protected $current_widget = null;
	protected $current_component = null;
	protected $atomic_style_posts_processed = [];
	protected $atomic_style_component_previews_printed = [];
	protected $atomic_dynamic_style_instances_printed = [];
	protected $atomic_dynamic_styles_by_component = [];

	public function __construct() {

		add_filter(
			'jet-engine/twig-views/editor/css-variables',
			[ $this, 'register_css_vars_for_component_controls' ]
		);

		add_filter( 'jet-engine/listings/document-id', [ $this, 'set_components_document_id' ] );

		add_action(
			'jet-engine/listings/components/register-component-elements',
			[ $this, 'add_widget_for_component' ]
		);

		add_action( 'jet-engine/listings/components/update-settings', [ $this, 'on_settings_update' ] );
		add_action( 'jet-engine/elementor-views/documents-registered', [ $this, 'register_document' ] );
		add_action( 'elementor/document/after_save', [ $this, 'save_component_meta' ], 10, 2 );
		add_action( 'jet-engine/elementor-views/dynamic-tags/register', [ $this, 'register_tags' ] );
		add_action( 'jet-engine/component/before-content', [ $this, 'print_block_preview_css' ] );
		add_action( 'jet-engine/component/before-content', [ $this, 'print_editor_ajax_atomic_styles' ] );
		add_action( 'jet-engine/component/before-content', [ $this, 'print_atomic_dynamic_styles' ] );

		add_action(
			'jet-engine/elementor-views/frontend/before-inline-css',
			[ $this, 'fix_ajax_component_inline_css' ], 0, 2
		);

 		add_action(
 			'elementor/post/render',
 			[ $this, 'declare_atomic_styles_for_components' ]
 		);

		if ( ! $this->category_registered ) {
			add_action(
				'elementor/elements/categories_registered',
				[ $this, 'register_components_category' ]
			);
		}

		add_action(
			'jet-engine/component/before-content',
			[ $this, 'register_css_selectors_update' ]
		);

		add_action(
			'jet-engine/component/after-content',
			[ $this, 'unregister_css_selectors_update' ],
			99999
		);

		add_action(
			'jet-engine/elementor-views/components/current-widget',
			[ $this, 'set_current_widget' ]
		);
	}

	/**
	 * Current widget setter
	 *
	 * @param mixed $widget
	 * @return void
	 */
	public function set_current_widget( $widget ) {
		$this->current_widget = $widget;
	}

	/**
	 * Ensure CSS file is updated when component is rendered via AJAX request.
	 * @see https://github.com/Crocoblock/issues-tracker/issues/16542
	 *
	 * @param int $post_id Component/Listing to render CSS for.
	 * @param \Elementor\Core\Files\CSS\Post $css_file CSS file instance.
	 */
	public function fix_ajax_component_inline_css( $post_id, $css_file ) {

		static $posts_done = [];

		if ( ! jet_engine()->listings->components->is_component( $post_id ) ) {
			return;
		}

		if ( in_array( $post_id, $posts_done, true ) ) {
			return;
		}

		$css_file->update();

		$posts_done[] = $post_id;
	}

	/**
	 * Declare Elementor-rendered component documents so Atomic Editor local styles are enqueued.
	 *
	 * @param int $post_id Rendered Elementor document ID.
	 */
	public function declare_atomic_styles_for_components( $post_id ) {

		$post_id = absint( $post_id );

		if ( ! $post_id || isset( $this->atomic_style_posts_processed[ $post_id ] ) ) {
			return;
		}

		$this->atomic_style_posts_processed[ $post_id ] = true;

		if ( empty( jet_engine()->elementor_views->frontend )
			|| ! method_exists( jet_engine()->elementor_views->frontend, 'get_component_ids_from_elementor_post' )
		) {
			return;
		}

		$component_ids = jet_engine()->elementor_views->frontend->get_component_ids_from_elementor_post( $post_id );

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

		foreach ( $component_ids as $component_id ) {

			if ( $component_id === $post_id ) {
				continue;
			}

			$component = jet_engine()->listings->components->get(
				jet_engine()->listings->components->get_component_base_name() . '-' . $component_id
			);

			if ( ! $component || 'elementor' !== $component->get_render_view() ) {
				continue;
			}

			do_action( 'elementor/post/render', $component_id );
		}
	}

	/**
	 * Print Atomic Editor local styles for freshly rendered Elementor editor widgets.
	 *
	 * @param object $component Component object.
	 */
	public function print_editor_ajax_atomic_styles( $component ) {

		if ( 'elementor' !== $component->get_render_view() ) {
			return;
		}

		if ( ! wp_doing_ajax() || ! jet_engine()->elementor_views->is_editor_ajax() ) {
			return;
		}

		$component_id = absint( $component->get_id() );

		if ( ! $component_id || isset( $this->atomic_style_component_previews_printed[ $component_id ] ) ) {
			return;
		}

		$this->atomic_style_component_previews_printed[ $component_id ] = true;

		do_action( 'elementor/post/render', $component_id );
		do_action( 'elementor/frontend/after_enqueue_post_styles' );

		wp_print_styles();
	}

	/**
	 * Print instance-scoped Atomic Editor styles that depend on component controls.
	 *
	 * @param object $component Component object.
	 */
	public function print_atomic_dynamic_styles( $component ) {

		if ( 'elementor' !== $component->get_render_view() ) {
			return;
		}

		if ( ! class_exists( '\Elementor\Modules\AtomicWidgets\Styles\Styles_Renderer' )
			|| ! class_exists( '\Elementor\Modules\AtomicWidgets\Utils\Utils' )
		) {
			return;
		}

		if ( empty( \Elementor\Plugin::$instance->breakpoints )
			|| ! method_exists( \Elementor\Plugin::$instance->breakpoints, 'get_breakpoints_config' )
		) {
			return;
		}

		if ( ! empty( jet_engine()->dynamic_tags ) && method_exists( jet_engine()->dynamic_tags, 'init_module' ) ) {
			jet_engine()->dynamic_tags->init_module();
		}

		$component_id = absint( $component->get_id() );
		$state        = jet_engine()->listings->components->state->get();
		$unique_class = ! empty( $state['component_unique_class'] ) ? $state['component_unique_class'] : '';

		if ( ! $unique_class && $this->current_widget && method_exists( $this->current_widget, 'get_jet_component_instance_class' ) ) {
			$unique_class = $this->current_widget->get_jet_component_instance_class();
		}

		$unique_class = sanitize_html_class( $unique_class );

		if ( ! $component_id || ! $unique_class ) {
			return;
		}

		$print_key = $component_id . ':' . $unique_class;

		if ( isset( $this->atomic_dynamic_style_instances_printed[ $print_key ] ) ) {
			return;
		}

		$this->atomic_dynamic_style_instances_printed[ $print_key ] = true;

		$styles = $this->get_atomic_dynamic_styles_for_component( $component_id );

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

		$css = \Elementor\Modules\AtomicWidgets\Styles\Styles_Renderer::make(
			\Elementor\Plugin::$instance->breakpoints->get_breakpoints_config(),
			'.' . $unique_class
		)->render( $styles );

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

		$css = wp_check_invalid_utf8( $css );
		$css = wp_kses_no_null( $css );
		$css = \Jet_Engine_Sanitizer::sanitize_inline_css( $css );
		$css = str_ireplace( '</style', '<\/style', $css );

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

		printf(
			'<style type="text/css" data-jet-engine-component-atomic-dynamic="%1$s">%2$s</style>',
			esc_attr( $print_key ),
			$css // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
		);
	}

	/**
	 * Get only Atomic style variants that contain dynamic tag values.
	 *
	 * @param int $component_id Component post ID.
	 * @return array
	 */
	protected function get_atomic_dynamic_styles_for_component( $component_id ) {

		if ( isset( $this->atomic_dynamic_styles_by_component[ $component_id ] ) ) {
			return $this->atomic_dynamic_styles_by_component[ $component_id ];
		}

		$styles = [];

		\Elementor\Modules\AtomicWidgets\Utils\Utils::traverse_post_elements(
			(string) $component_id,
			function( $element_data ) use ( &$styles ) {

				if ( empty( $element_data['styles'] ) || ! is_array( $element_data['styles'] ) ) {
					return;
				}

				$element_styles = $element_data['styles'];

				if ( class_exists( '\Elementor\Modules\AtomicWidgets\Styles\Atomic_Widget_Styles' ) ) {
					$element_styles = \Elementor\Modules\AtomicWidgets\Styles\Atomic_Widget_Styles::get_license_based_filtered_styles( $element_styles );
				}

				foreach ( $element_styles as $style_id => $style ) {
					$style = $this->filter_atomic_style_to_dynamic_variants( $style );

					if ( ! empty( $style['variants'] ) ) {
						$styles[ $style_id ] = $style;
					}
				}
			}
		);

		$this->atomic_dynamic_styles_by_component[ $component_id ] = array_values( $styles );

		return $this->atomic_dynamic_styles_by_component[ $component_id ];
	}

	/**
	 * Keep only variants that include Atomic dynamic props.
	 *
	 * @param array $style Atomic style definition.
	 * @return array
	 */
	protected function filter_atomic_style_to_dynamic_variants( $style ) {

		if ( empty( $style['variants'] ) || ! is_array( $style['variants'] ) ) {
			return [];
		}

		$variants = [];

		foreach ( $style['variants'] as $variant ) {
			if ( ! empty( $variant['props'] ) && $this->has_atomic_dynamic_prop( $variant['props'] ) ) {
				$variants[] = $variant;
			}
		}

		$style['variants'] = $variants;

		return $style;
	}

	/**
	 * Check whether props contain an Atomic dynamic tag value.
	 *
	 * @param mixed $value Value to inspect.
	 * @return bool
	 */
	protected function has_atomic_dynamic_prop( $value ) {

		if ( ! is_array( $value ) ) {
			return false;
		}

		if ( isset( $value['$$type'] ) && 'dynamic' === $value['$$type'] ) {
			return true;
		}

		foreach ( $value as $child ) {
			if ( $this->has_atomic_dynamic_prop( $child ) ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Apply unique CSS ID for dynamic CSS
	 *
	 * @return string|null
	 */
	public function apply_unique_css_id() {

		$current_widget = $this->current_widget;

		if ( ! $current_widget && $this->current_component ) {

			$stack_index = jet_engine()->listings->components->stack->get_component_stack_index(
				$this->current_component
			);

			if ( false !== $stack_index ) {

				$stack_meta = jet_engine()->listings->components->stack->get_stack_meta(
					$stack_index,
					'current_widget'
				);

				if ( $stack_meta ) {
					$current_widget = $stack_meta;
				}
			}
		}
		return $current_widget ? $current_widget->get_jet_instance_id() : null;
	}

	/**
	 * Apply component selector for dynamic CSS
	 *
	 * @param string $selector
	 * @param int    $data_id
	 * @param int    $rendered_id
	 * @return string
	 */
	public function apply_component_selector( $selector, $data_id, $rendered_id ) {

		/**
		 * Prevent applying selector if we're not in the context of the current component
		 * @see https://github.com/Crocoblock/issues-tracker/issues/17873
		 */
		if (
			$this->current_component
			&& (int) $rendered_id !== (int) $this->current_component->get_id() )
		{
			return $selector;
		}

		$current_widget = $this->current_widget;

		// Try to set $current_widget from stack if not set already
		// This case is actual when component contains another component
		if ( ! $current_widget && $this->current_component ) {

			$stack_index = jet_engine()->listings->components->stack->get_component_stack_index(
				$this->current_component
			);

			if ( false !== $stack_index ) {

				$stack_meta = jet_engine()->listings->components->stack->get_stack_meta(
					$stack_index,
					'current_widget'
				);

				if ( $stack_meta ) {
					$current_widget = $stack_meta;
				}
			}
		}

		if ( ! $current_widget && ! $this->current_component ) {
			return $selector;
		} elseif ( ! $current_widget && $this->current_component ) {
			return '.jet-listing-grid--' . $this->current_component->get_id();
		}

		return $current_widget->apply_component_selector( $selector );
	}

	/**
	 * Register hooks to update CSS selectors for dynamic styles generated by Elementor
	 *
	 * @param object $component
	 * @return void
	 */
	public function register_css_selectors_update( $component ) {

		$this->current_component = $component;

		$stack_index = jet_engine()->listings->components->stack->get_component_stack_index( $component );

		if ( false !== $stack_index && $this->current_widget ) {
			jet_engine()->listings->components->stack->set_stack_meta(
				$stack_index,
				'current_widget',
				$this->current_widget
			);
		}

		add_filter(
			'jet-engine/elementor-views/dynamic-tags/dynamic-css-unique-id',
			[ $this, 'apply_unique_css_id' ]
		);

		add_filter(
			'jet-engine/elementor-views/dynamic-css/unique-listing-selector',
			[ $this, 'apply_component_selector' ], 0, 3
		);

	}

	/**
	 * Unregister hooks to update CSS selectors for dynamic styles generated by Elementor
	 *
	 * @param object $component
	 * @return void
	 */
	public function unregister_css_selectors_update( $component ) {

		$stack_index = jet_engine()->listings->components->stack->get_component_stack_index( $component );

		if ( 0 === $stack_index ) {

			$this->current_component = null;

			remove_filter(
				'jet-engine/elementor-views/dynamic-tags/dynamic-css-unique-id',
				[ $this, 'apply_unique_css_id' ]
			);

			remove_filter(
				'jet-engine/elementor-views/dynamic-css/unique-listing-selector',
				[ $this, 'apply_component_selector' ], 0, 3
			);
		} else {
			$this->current_component = jet_engine()->listings->components->stack->get_parent( $stack_index );
		}
	}

	/**
	 * Print Elementor CSS for Elementor-created component in block editor
	 *
	 * @return [type] [description]
	 */
	public function print_block_preview_css( $component ) {

		if ( 'elementor' !== $component->get_render_view() ) {
			return;
		}

		// phpcs:disable
		if ( empty( $_REQUEST['is_component_preview'] ) ) {
			return;
		}
		// phpcs:enable

		\Elementor\Plugin::instance()->frontend->register_styles();
		\Elementor\Plugin::instance()->frontend->enqueue_styles();

		wp_print_styles();
	}

	/**
	 * Synch elementor controls with component settings
	 *
	 * @param  [type] $component [description]
	 * @return [type]            [description]
	 */
	public function on_settings_update( $component ) {

		if ( 'elementor' !== $component->get_render_view() ) {
			return;
		}

		$props = $component->get_props();
		$styles = $component->get_styles();

		if ( ! empty( $props ) ) {
			foreach ( $props as $i => $prop ) {
				if ( empty( $prop['_id'] ) && ! empty( $prop['id'] ) ) {
					$props[ $i ]['_id'] = $prop['id'];
				}

			}
		}

		if ( ! empty( $styles ) ) {
			foreach ( $styles as $i => $prop ) {
				if ( empty( $prop['_id'] ) && ! empty( $prop['id'] ) ) {
					$styles[ $i ]['_id'] = $prop['id'];
				}

			}
		}

		$page_settings = get_post_meta( $component->get_id(), '_elementor_page_settings', true );
		$listing_data  = get_post_meta( $component->get_id(), '_listing_data', true );

		if ( empty( $page_settings ) ) {
			$page_settings = [];
		}

		if ( empty( $listing_data ) ) {
			$listing_data = [];
		}

		$page_settings['component_controls_list']       = $props;
		$listing_data['component_controls_list']        = $props;
		$page_settings['component_style_controls_list'] = $styles;
		$listing_data['component_style_controls_list']  = $styles;

		update_post_meta( $component->get_id(), '_elementor_page_settings', $page_settings );
		update_post_meta( $component->get_id(), '_listing_data', $listing_data );

	}

	/**
	 * Regsiter CSS variables for component controls
	 * @return [type] [description]
	 */
	public function register_css_vars_for_component_controls( $css_vars = [] ) {

		$kit = \Elementor\Plugin::$instance->kits_manager->get_active_kit();

		if ( ! $kit ) {
			return $css_vars;
		}

		$system_colors = $kit->get_settings( 'system_colors' );
		$custom_colors = $kit->get_settings( 'custom_colors' );

		if ( ! empty( $system_colors ) ) {
			foreach ( $system_colors as $color ) {
				$css_vars[] = [
					'var'   => sprintf( 'var( --e-global-color-%1$s )', $color['_id'] ),
					'value' => $color['color'],
				];
			}
		}

		if ( ! empty( $custom_colors ) ) {
			foreach ( $custom_colors as $color ) {
				$css_vars[] = [
					'var'   => sprintf( 'var( --e-global-color-%1$s )', $color['_id'] ),
					'value' => $color['color'],
				];
			}
		}

		return $css_vars;
	}

	/**
	 * Register dynamic tags to get component prop
	 *
	 * @param  [type] $tags_module [description]
	 * @return [type]              [description]
	 */
	public function register_tags( $tags_module ) {

		require jet_engine()->plugin_path( 'includes/components/elementor-views/components/dynamic-tags/text-tag.php' );
		require jet_engine()->plugin_path( 'includes/components/elementor-views/components/dynamic-tags/image-tag.php' );
		require jet_engine()->plugin_path( 'includes/components/elementor-views/components/dynamic-tags/color-tag.php' );

		$tags_module->register( new Dynamic_Tags\Text_Tag() );
		$tags_module->register( new Dynamic_Tags\Image_Tag() );
		$tags_module->register( new Dynamic_Tags\Color_Tag() );

	}


	/**
	 * Save component meta into a separate meta field on elementor component document save
	 *
	 * @param  [type] $document [description]
	 * @param  [type] $data     [description]
	 * @return [type]           [description]
	 */
	public function save_component_meta( $document, $data ) {

		if ( $document->get_name() !== jet_engine()->listings->components->get_component_base_name() ) {
			return;
		}

		$component = jet_engine()->listings->components->get(
			jet_engine()->listings->components->get_component_base_name() . '-' . $document->get_main_id()
		);

		if ( $component && ! empty( $data['settings']['component_controls_list'] ) ) {
			$component->set_props( $data['settings']['component_controls_list'] );
		}

		if ( $component && ! empty( $data['settings']['component_style_controls_list'] ) ) {
			$component->set_styles( $data['settings']['component_style_controls_list'] );
		}

	}

	/**
	 * Regster component document
	 *
	 * @return [type] [description]
	 */
	public function register_document( $documents_manager ) {

		require jet_engine()->plugin_path( 'includes/components/elementor-views/components/component-document.php' );

		$documents_manager->register_document_type(
			jet_engine()->listings->components->get_component_base_name(),
			'\Jet_Engine\Elementor_Views\Components\Document'
		);

	}

	/**
	 * Set document ID for component
	 *
	 * @param [type] $id [description]
	 */
	public function set_components_document_id( $id ) {

		// phpcs:disable
		if ( ! empty( $_REQUEST['listing_view_type'] )
			&& 'elementor' === $_REQUEST['listing_view_type']
			&& ! empty( $_REQUEST['template_entry_type'] )
			&& 'component' === $_REQUEST['template_entry_type']
		) {
			return jet_engine()->listings->components->get_component_base_name();
		}
		// phpcs:enable

		return $id;
	}

	/**
	 * Register separate widget for each component
	 * @param [type] $component [description]
	 */
	public function add_widget_for_component( $component ) {

		if ( ! $component->is_view_supported( 'elementor' ) ) {
			return;
		}

		add_action( 'elementor/widgets/register', function( $widgets_manager ) use ( $component ) {

			if ( ! class_exists( '\Jet_Engine\Elementor_Views\Components\Base_Widget' ) ) {
				require jet_engine()->plugin_path( 'includes/components/elementor-views/components/base-widget.php' );
			}

			$component_widget = new Base_Widget( [], null, $component );

			$widgets_manager->register( $component_widget );

		} );

	}

	/**
	 * Register components category
	 *
	 * @param  [type] $elements_manager [description]
	 * @return [type]                   [description]
	 */
	public function register_components_category( $elements_manager ) {

		$elements_manager->add_category(
			jet_engine()->listings->components->components_category( 'slug' ),
			[
				'title' => jet_engine()->listings->components->components_category( 'name' ),
				'icon' => 'fa fa-plug',
			]
		);

		$this->category_registered = true;

	}

}
