<?php
/**
 * This class is serving as the base for various placement types and providing a foundation for defining common placement attributes and methods.
 *
 * @package AdvancedAds
 * @author  Advanced Ads <info@wpadvancedads.com>
 * @since   1.48.0
 */

namespace AdvancedAds\Abstracts;

use RuntimeException;
use AdvancedAds\Traits;
use AdvancedAds\Constants;
use AdvancedAds\Frontend\Stats;
use AdvancedAds\Interfaces\Entity_Interface;
use AdvancedAds\Framework\Utilities\Formatting;

defined( 'ABSPATH' ) || exit;

/**
 * Placement.
 */
class Placement extends Data implements Entity_Interface {

	use Traits\Entity;
	use Traits\Wrapper;

	/**
	 * This is the object type.
	 *
	 * @var string
	 */
	protected $object_type = 'placement';

	/**
	 * This is the item object.
	 *
	 * @var Ad|Group|null
	 */
	protected $item_object = null;

	/**
	 * This is the item type.
	 *
	 * @var string
	 */
	protected $item_type = '';

	/**
	 * Core data for this object. Name value pairs (name + default value).
	 *
	 * @var array
	 */
	protected $data = [
		'title'    => '',
		'content'  => '',
		'type'     => 'default',
		'slug'     => '',
		'status'   => '',
		'item'     => '',
		'display'  => [],
		'visitors' => [],
	];

	/**
	 * Get the placement if ID is passed, otherwise the placement is new and empty.
	 *
	 * @param Placement|WP_Post|int $placement Placement to init.
	 */
	public function __construct( $placement = 0 ) {
		parent::__construct();

		$this->set_placement_id( $placement );
		$this->data_store = wp_advads_get_placement_repository();

		if ( $this->get_id() > 0 ) {
			$this->data_store->read( $this );
		}
	}

	/**
	 * Set the placement ID depending on what was passed.
	 *
	 * @param Placement|WP_Post|int $placement Placement instance, post instance or numeric.
	 *
	 * @return void
	 */
	private function set_placement_id( $placement ): void {
		if ( is_numeric( $placement ) && $placement > 0 ) {
			$this->set_id( $placement );
		} elseif ( $placement instanceof self ) {
			$this->set_id( absint( $placement->get_id() ) );
		} elseif ( ! empty( $placement->ID ) ) {
			$this->set_id( absint( $placement->ID ) );
		} else {
			$this->set_object_read( true );
		}
	}

	/* Getter ------------------- */

	/**
	 * Get item.
	 *
	 * @param string $context What the value is for. Valid values are view and edit.
	 *
	 * @return string
	 */
	public function get_item( $context = 'view' ): string {
		return $this->get_prop( 'item', $context );
	}

	/**
	 * Get the display conditions for the placement.
	 *
	 * @param string $context What the value is for. Valid values are view and edit.
	 *
	 * @return array
	 */
	public function get_display_conditions( $context = 'view' ): array {
		return $this->get_prop( 'display', $context );
	}

	/**
	 * Get the visitor conditions for the placement.
	 *
	 * @param string $context What the value is for. Valid values are view and edit.
	 *
	 * @return array
	 */
	public function get_visitor_conditions( $context = 'view' ): array {
		return $this->get_prop( 'visitors', $context );
	}

	/* Setter ------------------- */

	/**
	 * Set item.
	 *
	 * @param string $item Placement item.
	 *
	 * @return void
	 */
	public function set_item( $item ): void {
		$this->set_prop( 'item', $item );
	}

	/**
	 * Set display conditions.
	 *
	 * @param string $conditions Placement conditions.
	 *
	 * @return void
	 */
	public function set_display_conditions( $conditions ): void {
		$this->set_prop( 'display', $conditions );
	}

	/**
	 * Set conditions.
	 *
	 * @param string $conditions Placement conditions.
	 *
	 * @return void
	 */
	public function set_visitor_conditions( $conditions ): void {
		$this->set_prop( 'visitors', $conditions );
	}

	/* Conditional ------------------- */

	/**
	 * Check if placement item is allowed
	 *
	 * @return bool
	 */
	public function is_item_allowed(): bool {
		$item_type   = $this->get_item_type();
		$item_object = $this->get_item_object();

		return $this->get_type_object()
			->is_entity_allowed( $item_object->get_type(), $item_type );
	}

	/**
	 * Determines whether the placement can be displayed.
	 *
	 * @return bool True if the placement can be displayed, false otherwise.
	 */
	public function can_display(): bool {
		return apply_filters( 'advanced-ads-can-display-placement', true, $this->get_id(), $this );
	}

	/**
	 * Determines whether the placement has cache busting enabled.
	 *
	 * @return bool
	 */
	public function has_cache_busting(): bool {
		$value = $this->get_prop( 'cache-busting' );
		return 'auto' === $value || Formatting::string_to_bool( $value );
	}

	/* Additional Methods ------------------- */

	/**
	 * Prepare frontend output.
	 *
	 * @return string
	 */
	public function generate_html(): string {
		$item_type = $this->get_item_type();
		$ad_args   = $this->get_prop( 'ad_args' );

		// Move override logic from prepare_output.
		$method            = Constants::ENTITY_AD === $item_type ? 'ad' : 'group';
		$args              = wp_advads_default_entity_arguments( $method, $this->item_object->get_id(), $ad_args );
		$overridden_entity = Constants::ENTITY_AD === $item_type
			? apply_filters( 'advanced-ads-ad-select-override-by-ad', false, $this->item_object, $args )
			: apply_filters( 'advanced-ads-ad-select-override-by-group', false, $this->item_object, $this->item_object->get_ordered_ad_ids(), $args );

		if ( false !== $overridden_entity ) {
			return $overridden_entity;
		}

		return $this->item_object->can_display() ? $this->item_object->output() : '';
	}

	/**
	 * Get the wrapper attributes.
	 *
	 * @return array
	 */
	public function get_wrapper_attributes(): array {
		$prefix  = wp_advads()->get_frontend_prefix();
		$ad_args = $this->get_prop( 'ad_args' );

		if ( ! $this->is_type( 'header' ) ) {
			$class = $ad_args['output']['class'] ?? [];
			if ( ! is_array( $class ) ) {
				$class = explode( ' ', $class );
			}

			if ( ! in_array( $prefix . $this->get_slug(), $class, true ) ) {
				$class[] = $prefix . $this->get_slug();
			}

			$ad_args['output']          = $ad_args['output'] ?? [];
			$ad_args['output']['class'] = $class;
		}

		// Create placement id for various features like ajax ads.
		$ad_args['output']['placement_id'] = $this->get_id();

		return $ad_args;
	}

	/**
	 * Prepares the output for the placement.
	 *
	 * @return mixed The prepared output or the overridden return value.
	 */
	public function prepare_output(): string {
		// Early bail!!
		if ( empty( $this->get_item_object() ) || Constants::ENTITY_PLACEMENT === $this->get_item_type() || 'publish' !== $this->get_status() ) {
			return '';
		}

		// Inject options.
		$attrs = $this->get_wrapper_attributes();
		$this->set_prop_temp( 'ad_args', $attrs );
		$this->item_object->set_prop_temp( 'ad_args', $attrs );

		$output = $this->generate_html();

		// Maintain Stats.
		if ( $output ) {
			Stats::get()->add_entity( 'placement', $this->get_id(), $this->get_title() );
			Stats::get()->add_entity( $this->get_item_type(), $this->item_object->get_id(), $this->item_object->get_title(), $this->get_id() );
		}

		return $output;
	}

	/**
	 * Update placement item.
	 *
	 * @param string $new_item Placement item id.
	 *
	 * @throws \RuntimeException If the new item is equal to the old item or if the item type is not allowed for the placement type.
	 *
	 * @return mixed
	 */
	public function update_item( $new_item ) {
		list( 'type' => $item_type, 'id' => $item_id )         = $this->get_item_parts( $this->get_item() );
		list( 'type' => $new_item_type, 'id' => $new_item_id ) = $this->get_item_parts( $new_item );

		if ( $new_item_id === $item_id ) {
			throw new RuntimeException( 'New item is equal to old item.' );
		}

		$new_item_object = 'ad' === $new_item_type
			? wp_advads_get_ad( $new_item_id )
			: wp_advads_get_group( $new_item_id );

		$is_allowed = $this->get_type_object()
			->is_entity_allowed( $new_item_object->get_type(), $new_item_type );

		if ( ! $is_allowed ) {
			throw new RuntimeException(
				sprintf(
					/* translators: 1: Entity type 2: Item type 3: Placement title */
					esc_html__( '%1$s type "%2$s" not allowed for placement type "%3$s"', 'advanced-ads' ),
					strtoupper( $new_item_type ), // phpcs:ignore
					$new_item_object->get_type(), // phpcs:ignore
					$this->get_type_object()->get_title() // phpcs:ignore
				)
			);
		}

		if ( ! update_post_meta( $this->get_id(), 'item', $new_item ) ) {
			throw new RuntimeException( 'Can\'t update item.' );
		}

		return $new_item_object;
	}

	/**
	 * Remove placement item.
	 *
	 * @return bool
	 */
	public function remove_item(): bool {
		return delete_post_meta( $this->get_id(), 'item' );
	}

	/**
	 * Get placement item type
	 *
	 * @return string
	 */
	public function get_item_type(): string {
		$this->get_item_object();

		return $this->item_type;
	}

	/**
	 * Get placement item object
	 *
	 * @return Ad|Group|bool|null
	 */
	public function get_item_object() {
		global $sitepress;

		// Early bail!!
		if ( empty( $this->get_item() ) || null !== $this->item_object ) {
			return $this->item_object;
		}

		list( 'type' => $item_type, 'id' => $item_id ) = $this->get_item_parts( $this->get_item() );

		if ( Constants::ENTITY_AD === $item_type && defined( 'ICL_SITEPRESS_VERSION' ) ) {
			/**
			 * Deliver the translated version of an ad if set up with WPML.
			 * If an ad is not translated, show the ad in the original language when this is the selected option in the WPML settings.
			 *
			 * @source https://wpml.org/wpml-hook/wpml_object_id/
			 * @source https://wpml.org/forums/topic/backend-custom-post-types-page-overview-with-translation-options/
			 */
			$item_id = apply_filters( 'wpml_object_id', $item_id, 'advanced_ads', $sitepress->is_display_as_translated_post_type( 'advanced_ads' ) );
		}

		$this->item_type   = $item_type;
		$this->item_object = 'ad' === $item_type
			? wp_advads_get_ad( $item_id )
			: wp_advads_get_group( $item_id );

		if ( $this->item_object ) {
			$this->item_object->set_parent( $this );
		}

		return $this->item_object;
	}

	/**
	 * Get placement type object
	 *
	 * @return Placement_Type|bool
	 */
	public function get_type_object() {
		if ( ! wp_advads_has_placement_type( $this->get_type() ) ) {
			wp_advads_create_placement_type( $this->get_type() );
		}

		return wp_advads_get_placement_type( $this->get_type() );
	}

	/**
	 * Get the item parts
	 *
	 * @param string $item Placement item.
	 *
	 * @return array
	 */
	private function get_item_parts( $item ): array {
		$item_parts = explode( '_', $item );

		return [
			'type' => trim( $item_parts[0] ),
			'id'   => absint( $item_parts[1] ?? 0 ),
		];
	}

	/**
	 * Get the placement edit link
	 *
	 * @return string|null
	 */
	public function get_edit_link() {
		return add_query_arg(
			[
				'post_type' => Constants::POST_TYPE_PLACEMENT,
			],
			admin_url( 'edit.php#modal-placement-edit-' . $this->get_id() )
		);
	}
}
