<?php
/**
 * WPSEO plugin file.
 *
 * @package WPSEO\Admin\Links
 */

/**
 * Represents the link columns. This class will add and handle the link columns.
 */
class WPSEO_Link_Columns {

	/**
	 * Partial column name.
	 *
	 * @var string
	 */
	const COLUMN_LINKED = 'linked';

	/**
	 * Partial column name.
	 *
	 * @var string
	 */
	const COLUMN_LINKS = 'links';

	/**
	 * Holds the link column count instance.
	 *
	 * @var WPSEO_Link_Column_Count
	 */
	protected $link_count;

	/**
	 * Storage to use.
	 *
	 * @var WPSEO_Meta_Storage
	 */
	protected $storage;

	/**
	 * List of public post types.
	 *
	 * @var array
	 */
	protected $public_post_types = [];

	/**
	 * WPSEO_Link_Columns constructor.
	 *
	 * @param WPSEO_Meta_Storage $storage The storage object to use.
	 */
	public function __construct( WPSEO_Meta_Storage $storage ) {
		$this->storage = $storage;
	}

	/**
	 * Registers the hooks.
	 */
	public function register_hooks() {
		global $pagenow;
		$is_ajax_request = wp_doing_ajax();

		if ( ! WPSEO_Metabox::is_post_overview( $pagenow ) && ! $is_ajax_request ) {
			return;
		}

		// Exit when either table is not present or accessible.
		if ( ! WPSEO_Link_Table_Accessible::is_accessible() || ! WPSEO_Meta_Table_Accessible::is_accessible() ) {
			return;
		}

		if ( $is_ajax_request ) {
			add_action( 'admin_init', [ $this, 'set_count_objects' ] );
		}

		// Hook into tablenav to calculate links and linked.
		add_action( 'manage_posts_extra_tablenav', [ $this, 'count_objects' ] );

		add_filter( 'posts_clauses', [ $this, 'order_by_links' ], 1, 2 );
		add_filter( 'posts_clauses', [ $this, 'order_by_linked' ], 1, 2 );

		add_filter( 'admin_init', [ $this, 'register_init_hooks' ] );
	}

	/**
	 * Register hooks that require to be registered after `init`.
	 */
	public function register_init_hooks() {
		$this->public_post_types = apply_filters( 'wpseo_link_count_post_types', WPSEO_Post_Type::get_accessible_post_types() );

		if ( is_array( $this->public_post_types ) && $this->public_post_types !== [] ) {
			array_walk( $this->public_post_types, [ $this, 'set_post_type_hooks' ] );
		}
	}

	/**
	 * Modifies the query pieces to allow ordering column by links to post.
	 *
	 * @param array     $pieces Array of Query pieces.
	 * @param \WP_Query $query  The Query on which to apply.
	 *
	 * @return array
	 */
	public function order_by_links( $pieces, $query ) {
		if ( 'wpseo-' . self::COLUMN_LINKS !== $query->get( 'orderby' ) ) {
			return $pieces;
		}

		return $this->build_sort_query_pieces( $pieces, $query, 'internal_link_count' );
	}

	/**
	 * Modifies the query pieces to allow ordering column by links to post.
	 *
	 * @param array     $pieces Array of Query pieces.
	 * @param \WP_Query $query  The Query on which to apply.
	 *
	 * @return array
	 */
	public function order_by_linked( $pieces, $query ) {
		if ( 'wpseo-' . self::COLUMN_LINKED !== $query->get( 'orderby' ) ) {
			return $pieces;
		}

		return $this->build_sort_query_pieces( $pieces, $query, 'incoming_link_count' );
	}

	/**
	 * Builds the pieces for a sorting query.
	 *
	 * @param array     $pieces Array of Query pieces.
	 * @param \WP_Query $query  The Query on which to apply.
	 * @param string    $field  The field in the table to JOIN on.
	 *
	 * @return array Modified Query pieces.
	 */
	protected function build_sort_query_pieces( $pieces, $query, $field ) {
		global $wpdb;

		// We only want our code to run in the main WP query.
		if ( ! $query->is_main_query() ) {
			return $pieces;
		}

		// Get the order query variable - ASC or DESC.
		$order = strtoupper( $query->get( 'order' ) );

		// Make sure the order setting qualifies. If not, set default as ASC.
		if ( ! in_array( $order, [ 'ASC', 'DESC' ], true ) ) {
			$order = 'ASC';
		}

		$table = $this->storage->get_table_name();

		$pieces['join']   .= " LEFT JOIN $table AS yst_links ON yst_links.object_id = {$wpdb->posts}.ID ";
		$pieces['orderby'] = "{$field} $order, FIELD( {$wpdb->posts}.post_status, 'publish' ) $order, {$pieces['orderby']}";

		return $pieces;
	}

	/**
	 * Sets the hooks for each post type.
	 *
	 * @param string $post_type The post type.
	 */
	public function set_post_type_hooks( $post_type ) {
		add_filter( 'manage_' . $post_type . '_posts_columns', [ $this, 'add_post_columns' ] );
		add_action( 'manage_' . $post_type . '_posts_custom_column', [ $this, 'column_content' ], 10, 2 );
		add_filter( 'manage_edit-' . $post_type . '_sortable_columns', [ $this, 'column_sort' ] );
	}

	/**
	 * Adds the columns for the post overview.
	 *
	 * @param array $columns Array with columns.
	 *
	 * @return array The extended array with columns.
	 */
	public function add_post_columns( $columns ) {
		if ( ! is_array( $columns ) ) {
			return $columns;
		}

		$columns[ 'wpseo-' . self::COLUMN_LINKS ] = sprintf(
			'<span class="yoast-linked-to yoast-column-header-has-tooltip" data-tooltip-text="%1$s"><span class="screen-reader-text">%2$s</span></span>',
			esc_attr__( 'Number of outgoing internal links in this post. See "Yoast Columns" text in the help tab for more info.', 'wordpress-seo' ),
			esc_html__( 'Outgoing internal links', 'wordpress-seo' )
		);

		if ( ! WPSEO_Link_Query::has_unprocessed_posts( $this->public_post_types ) ) {
			$columns[ 'wpseo-' . self::COLUMN_LINKED ] = sprintf(
				'<span class="yoast-linked-from yoast-column-header-has-tooltip" data-tooltip-text="%1$s"><span class="screen-reader-text">%2$s</span></span>',
				esc_attr__( 'Number of internal links linking to this post. See "Yoast Columns" text in the help tab for more info.', 'wordpress-seo' ),
				esc_html__( 'Received internal links', 'wordpress-seo' )
			);
		}

		return $columns;
	}

	/**
	 * Makes sure we calculate all values in one query.
	 *
	 * @param string $target Extra table navigation location which is triggered.
	 */
	public function count_objects( $target ) {
		if ( 'top' === $target ) {
			$this->set_count_objects();
		}
	}

	/**
	 * Sets the objects to use for the count.
	 */
	public function set_count_objects() {
		global $wp_query;

		$posts    = empty( $wp_query->posts ) ? $wp_query->get_posts() : $wp_query->posts;
		$post_ids = [];

		// Post lists return a list of objects.
		if ( isset( $posts[0] ) && is_object( $posts[0] ) ) {
			$post_ids = wp_list_pluck( $posts, 'ID' );
		}
		elseif ( ! empty( $posts ) ) {
			// Page list returns an array of post IDs.
			$post_ids = array_keys( $posts );
		}

		$post_ids = WPSEO_Link_Query::filter_unprocessed_posts( $post_ids );

		$links = new WPSEO_Link_Column_Count();
		$links->set( $post_ids );

		$this->link_count = $links;
	}

	/**
	 * Displays the column content for the given column.
	 *
	 * @param string $column_name Column to display the content for.
	 * @param int    $post_id     Post to display the column content for.
	 */
	public function column_content( $column_name, $post_id ) {
		$link_count = null;

		switch ( $column_name ) {
			case 'wpseo-' . self::COLUMN_LINKS:
				$link_count = $this->link_count->get( $post_id, 'internal_link_count' );
				break;
			case 'wpseo-' . self::COLUMN_LINKED:
				if ( get_post_status( $post_id ) === 'publish' ) {
					$link_count = $this->link_count->get( $post_id, 'incoming_link_count' );
				}
				break;
		}

		if ( isset( $link_count ) ) {
			echo (int) $link_count;
		}
	}

	/**
	 * Sets the sortable columns.
	 *
	 * @param array $columns Array with sortable columns.
	 *
	 * @return array The extended array with sortable columns.
	 */
	public function column_sort( array $columns ) {
		$columns[ 'wpseo-' . self::COLUMN_LINKS ]  = 'wpseo-' . self::COLUMN_LINKS;
		$columns[ 'wpseo-' . self::COLUMN_LINKED ] = 'wpseo-' . self::COLUMN_LINKED;

		return $columns;
	}
}
