<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet"
        integrity="sha384-GLhlTQ8iRABdZLl6O3oVMWSktQOp6b7In1Zl3/Jr59b6EGGoI1aFkw7cmDA6j6gD" crossorigin="anonymous">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.3.0/css/all.min.css"
        integrity="sha512-SzlrxWUlpfuzQ+pcUCosxcglQRNAq/DZjVsC0lE40xsADsfeQoEypE+enwcOiGjk/bSuGGKHEyjSoQ1zVisanQ=="
        crossorigin="anonymous" referrerpolicy="no-referrer" />
</head>
</html>
<?php

namespace Elementor\Modules\GlobalClasses;

use Elementor\Core\Base\Document;
use Elementor\Modules\GlobalClasses\Atomic_Global_Styles;
use Elementor\Modules\GlobalClasses\Concerns\Has_Preview_Context;
use Elementor\Modules\GlobalClasses\Utils\Atomic_Elements_Utils;
use Elementor\Plugin;

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

class Global_Classes_Relations {
	use Has_Preview_Context;

	const META_KEY_FRONTEND = '_elementor_used_global_class';
	const META_KEY_PREVIEW = '_elementor_used_global_class_preview';

	const META_KEY_USAGE_INDEXED_FRONTEND = '_elementor_global_class_usage_indexed';
	const META_KEY_USAGE_INDEXED_PREVIEW = '_elementor_global_class_usage_indexed_preview';

	const META_KEY_CLASS_RELATED_POSTS_FRONTEND = '_elementor_global_class_using_documents';
	const META_KEY_CLASS_RELATED_POSTS_PREVIEW = '_elementor_global_class_using_documents_preview';

	protected array $context_keys = [
		'used_classes' => [
			'frontend' => self::META_KEY_FRONTEND,
			'preview' => self::META_KEY_PREVIEW,
		],
		'usage_indexed' => [
			'frontend' => self::META_KEY_USAGE_INDEXED_FRONTEND,
			'preview' => self::META_KEY_USAGE_INDEXED_PREVIEW,
		],
		'related_posts' => [
			'frontend' => self::META_KEY_CLASS_RELATED_POSTS_FRONTEND,
			'preview' => self::META_KEY_CLASS_RELATED_POSTS_PREVIEW,
		],
	];

	public function register_hooks(): void {
		add_action(
			'elementor/document/after_save',
			fn( Document $document ) => $this->on_document_save( $document )
		);
	}

	public function collect_class_ids_from_post( int $post_id, ?array $restrict_to_ids = null ): array {
		$ids = $this->extract_class_ids_from_post( $post_id );

		if ( null === $restrict_to_ids ) {
			return $ids;
		}

		return array_values( array_intersect( $ids, $restrict_to_ids ) );
	}

	public function get_posts_by_style( string $style_id ): array {
		$from_index = $this->get_posts_from_reverse_index( $style_id );

		if ( ! empty( $from_index ) ) {
			return $from_index;
		}

		$post_ids = get_posts( [
			'fields' => 'ids',
			'meta_query' => [
				[
					'key' => $this->get_context_key( 'used_classes' ),
					'value' => $style_id,
					'compare' => '=',
				],
			],
			'no_found_rows' => true,
			'post_status' => 'any',
			'post_type' => 'any',
			'posts_per_page' => -1,
			'update_post_meta_cache' => false,
		] );

		return array_map( 'intval', $post_ids );
	}

	public function get_styles_by_post( int $post_id ): array {
		$stored_ids = $this->get_stored_style_ids( $post_id );
		$live_ids = array_values( array_unique( $this->extract_class_ids_from_post( $post_id ) ) );
		$has_elementor_data = $this->document_has_elementor_data( $post_id );

		$normalize = static function ( array $ids ): string {
			$ids = array_values( array_unique( $ids ) );
			sort( $ids );

			return wp_json_encode( $ids );
		};

		if ( ! $has_elementor_data ) {
			if ( ! empty( $stored_ids ) ) {
				$this->mark_usage_indexed( $post_id );

				return $stored_ids;
			}

			if ( $this->is_usage_indexed( $post_id ) ) {
				return [];
			}

			$this->mark_usage_indexed( $post_id );

			return [];
		}

		if ( $normalize( $stored_ids ) !== $normalize( $live_ids ) ) {
			$this->set_styles_for_post( $post_id, $live_ids );

			return $live_ids;
		}

		if ( ! empty( $live_ids ) ) {
			$this->mark_usage_indexed( $post_id );
		}

		return $live_ids;
	}

	public function clear_class_relations( string $class_id ): void {
		$post_ids = $this->get_posts_by_style( $class_id );

		foreach ( $post_ids as $post_id ) {
			$stored_ids = $this->get_stored_style_ids( $post_id );
			$updated_ids = array_values( array_filter( $stored_ids, fn( $id ) => $id !== $class_id ) );
			$this->replace_stored_style_ids( $post_id, $updated_ids );
		}
	}

	public function clear_post_styles( int $post_id ): void {
		$saved_preview = $this->is_preview();

		try {
			foreach ( [ false, true ] as $preview_flag ) {
				$this->set_preview( $preview_flag );
				$old_ids = $this->get_stored_style_ids( $post_id );

				delete_post_meta( $post_id, $this->get_context_key( 'used_classes' ) );
				delete_post_meta( $post_id, $this->get_context_key( 'usage_indexed' ) );

				foreach ( $old_ids as $class_id ) {
					$this->unlink_post_from_class( $class_id, $post_id );
				}
			}
		} finally {
			$this->set_preview( $saved_preview );
		}
	}

	public function set_styles_for_post( int $post_id, array $style_ids ): void {
		$old_ids = $this->get_stored_style_ids( $post_id );

		foreach ( array_diff( $old_ids, $style_ids ) as $class_id ) {
			$this->unlink_post_from_class( $class_id, $post_id );
		}

		$this->replace_stored_style_ids( $post_id, $style_ids );

		foreach ( array_diff( $style_ids, $old_ids ) as $class_id ) {
			$this->link_post_to_class( $class_id, $post_id );
		}

		$this->mark_usage_indexed( $post_id );
	}

	private function get_posts_from_reverse_index( string $class_id ): array {
		$ids = $this->read_reverse_index_for_class( $class_id );

		if (
			! empty( $ids )
			|| Global_Classes_Repository::CONTEXT_PREVIEW !== $this->get_context_key( 'used_classes' )
		) {
			return $ids;
		}

		return ( new self() )->read_reverse_index_for_class( $class_id );
	}

	private function read_reverse_index_for_class( string $class_id ): array {
		$post = Global_Class_Post::find_by_class_id( $class_id );

		if ( ! $post ) {
			return [];
		}

		$ids = get_post_meta( $post->get_post_id(), $this->get_context_key( 'related_posts' ), true );

		if ( ! is_array( $ids ) ) {
			return [];
		}

		return array_values( array_unique( array_map( 'intval', $ids ) ) );
	}

	private function link_post_to_class( string $class_id, int $document_post_id ): void {
		$post = Global_Class_Post::find_by_class_id( $class_id );

		if ( ! $post ) {
			return;
		}

		$cpt_id = $post->get_post_id();
		$ids = get_post_meta( $cpt_id, $this->get_context_key( 'related_posts' ), true );
		$ids = is_array( $ids ) ? array_map( 'intval', $ids ) : [];

		if ( in_array( $document_post_id, $ids, true ) ) {
			return;
		}

		$ids[] = $document_post_id;

		update_post_meta( $cpt_id, $this->get_context_key( 'related_posts' ), $ids );
	}

	private function unlink_post_from_class( string $class_id, int $document_post_id ): void {
		$post = Global_Class_Post::find_by_class_id( $class_id );

		if ( ! $post ) {
			return;
		}

		$cpt_id = $post->get_post_id();
		$ids = get_post_meta( $cpt_id, $this->get_context_key( 'related_posts' ), true );

		if ( ! is_array( $ids ) ) {
			return;
		}

		$ids = array_values(
			array_filter(
				array_map( 'intval', $ids ),
				fn( $id ) => $id !== $document_post_id
			)
		);

		update_post_meta( $cpt_id, $this->get_context_key( 'related_posts' ), $ids );
	}

	private function on_document_save( Document $document ): void {
		$post_id = $document->get_main_id();

		static $in_progress = [];
		if ( isset( $in_progress[ $post_id ] ) ) {
			return;
		}
		$in_progress[ $post_id ] = true;

		$saved_preview = $this->is_preview();

		try {
			$this->invalidate_document_styles_cache( $post_id );

			$this->set_preview( false );
			$this->set_styles_for_post( $post_id, $this->extract_class_ids_from_post( $post_id ) );

			$this->set_preview( true );
			$this->set_styles_for_post( $post_id, $this->extract_class_ids_from_post( $post_id ) );
		} finally {
			$this->set_preview( $saved_preview );
			unset( $in_progress[ $post_id ] );
		}
	}

	private function extract_class_ids_from_post( int $post_id ): array {
		$used_class_ids = [];

		$document = $this->get_document_for_post( $post_id );

		if ( ! $document ) {
			return [];
		}

		$elements_data = $document->get_elements_data();

		if ( empty( $elements_data ) ) {
			return [];
		}

		Plugin::$instance->db->iterate_data( $elements_data, function ( $element_data ) use ( &$used_class_ids ) {
			$used_class_ids = array_merge(
				$used_class_ids,
				Atomic_Elements_Utils::collect_class_ids_from_element_data( $element_data )
			);
		} );

		return array_values( array_unique( $used_class_ids ) );
	}

	private function get_stored_style_ids( int $post_id ): array {
		$meta_values = get_post_meta( $post_id, $this->get_context_key( 'used_classes' ), false );

		if ( ! is_array( $meta_values ) ) {
			return [];
		}

		return array_values( array_unique( $meta_values ) );
	}

	private function replace_stored_style_ids( int $post_id, array $style_ids ): void {
		delete_post_meta( $post_id, $this->get_context_key( 'used_classes' ) );

		$unique_ids = array_unique( $style_ids );

		foreach ( $unique_ids as $class_id ) {
			add_post_meta( $post_id, $this->get_context_key( 'used_classes' ), $class_id );
		}
	}

	private function is_usage_indexed( int $post_id ): bool {
		return '1' === get_post_meta( $post_id, $this->get_context_key( 'usage_indexed' ), true );
	}

	private function mark_usage_indexed( int $post_id ): void {
		update_post_meta( $post_id, $this->get_context_key( 'usage_indexed' ), '1' );
	}

	private function invalidate_document_styles_cache( int $post_id ): void {
		do_action( 'elementor/atomic-widgets/styles/clear', [ Atomic_Global_Styles::STYLES_KEY, $post_id ] );
		do_action(
			'elementor/atomic-widgets/styles/clear',
			[ Atomic_Global_Styles::STYLES_KEY, $post_id, Global_Classes_Repository::CONTEXT_PREVIEW ]
		);
	}

	private function document_has_elementor_data( int $post_id ): bool {
		$document = $this->get_document_for_post( $post_id );

		if ( ! $document ) {
			return false;
		}

		$elements_data = $document->get_elements_data();

		return ! empty( $elements_data );
	}

	private function get_document_for_post( int $post_id ): ?Document {
		$documents = Plugin::$instance->documents;

		if ( ! $this->is_preview() ) {
			$document = $documents->get( $post_id );

			if ( ! $document ) {
				return null;
			}

			return $document ?? null;
		}

		$document = $documents->get_doc_or_auto_save( $post_id, get_current_user_id() );

		if ( ! $document ) {
			$document = $documents->get( $post_id );
		}

		return $document ?? null;
	}
}
