<?php
namespace AIOSEO\BrokenLinkChecker\Main;

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

use AIOSEO\BrokenLinkChecker\Highlighter;
use AIOSEO\BrokenLinkChecker\Links;
use AIOSEO\BrokenLinkChecker\LinkStatus;
use AIOSEO\BrokenLinkChecker\Models;

/**
 * Main class where core features are handled/registered.
 *
 * @since 1.0.0
 */
class Main {
	/**
	 * Paragraph class.
	 *
	 * @since 1.0.0
	 *
	 * @var Paragraph
	 */
	public $paragraph = null;

	/**
	 * Links class.
	 *
	 * @since 1.0.0
	 *
	 * @var Links\Links
	 */
	public $links = null;

	/**
	 * LinkStatus class.
	 *
	 * @since 1.0.0
	 *
	 * @var LinkStatus\LinkStatus
	 */
	public $linkStatus = null;

	/**
	 * Class constructor.
	 *
	 * @since 1.0.0
	 */
	public function __construct() {
		if (
			! aioseoBrokenLinkChecker()->core->db->tableExists( 'aioseo_blc_links' ) ||
			! aioseoBrokenLinkChecker()->core->db->tableExists( 'aioseo_blc_link_status' ) ||
			! aioseoBrokenLinkChecker()->core->db->tableExists( 'aioseo_blc_cache' )
		) {
			aioseoBrokenLinkChecker()->updates->addInitialTables();

			// Don't return here; otherwise the Setup Wizard won't show on the first activation, but on the second.
		}

		new Activate();

		$this->paragraph  = new Paragraph();
		$this->links      = new Links\Links();
		$this->linkStatus = new LinkStatus\LinkStatus();

		add_filter( 'the_content', [ $this, 'filterLinks' ], 999 ); // High prio to make sure other plugins get a chance to render their content, parse their blocks, etc..

		add_action( 'admin_enqueue_scripts', [ $this, 'enqueueStandaloneApp' ] );
		add_action( 'admin_footer', [ $this, 'adminFooter' ] );
	}

	/**
	 * Enqueues the standalone app for admin menu styles.
	 *
	 * @since 1.2.6
	 *
	 * @return void
	 */
	public function enqueueStandaloneApp() {
		aioseoBrokenLinkChecker()->core->assets->load( 'src/vue/standalone/app/main.js', [], [], 'aioseoBrokenLinkCheckerApp' );
	}

	/**
	 * Enqueue the footer div to let Vue attach.
	 *
	 * @since 1.2.6
	 *
	 * @return void
	 */
	public function adminFooter() {
		echo '<div id="aioseo-blc-admin"></div>';
	}

	/**
	 * Filters links in the post content.
	 *
	 * @since 1.0.0
	 *
	 * @param  string $postContent The post content.
	 * @return string              The post content.
	 */
	public function filterLinks( $postContent ) {
		if ( aioseoBrokenLinkChecker()->options->general->linkTweaks->nofollowBroken ) {
			$postContent = $this->nofollowBrokenLinks( $postContent );
		}

		return $postContent;
	}

	/**
	 * Adds rel="nofollow" to links in the post content that we know are broken.
	 *
	 * @since 1.0.0
	 *
	 * @param  string $postContent The post content.
	 * @return string              The post content.
	 */
	private function nofollowBrokenLinks( $postContent ) {
		// First, capture all link tags.
		preg_match_all( '/<a.*href="(.*?").*>(.*?)<\/a>/i', (string) $postContent, $linkTags );

		if ( empty( $linkTags[0] ) ) {
			return $postContent;
		}

		foreach ( $linkTags[0] as $linkTag ) {
			preg_match( '/href="(.*?)"/i', (string) $linkTag, $url );
			if ( empty( $url[1] ) ) {
				continue;
			}

			// Now check if we've indexed the link. If so, check if it's broken and act accordingly.
			$linkStatus = Models\LinkStatus::getByUrl( $url[1] );
			if ( ! $linkStatus->exists() || ! $linkStatus->broken ) {
				continue;
			}

			preg_match( '/rel="(.*?)"/i', (string) $linkTag, $relAttributes );
			if ( ! empty( $relAttributes[0] ) ) {
				$relAttributes = explode( ' ', $relAttributes[1] );
				if ( ! in_array( 'nofollow', $relAttributes, true ) ) {
					$relAttributes[] = 'nofollow';
				}
				$relAttributes = implode( ' ', $relAttributes );
			} else {
				$relAttributes = 'nofollow';
			}

			$newLinkTag = $this->insertAttribute( $linkTag, 'rel', $relAttributes );

			$oldLinkTag = aioseoBrokenLinkChecker()->helpers->escapeRegex( $linkTag );
			$newLinkTag = aioseoBrokenLinkChecker()->helpers->escapeRegexReplacement( $newLinkTag );

			$postContent = preg_replace( "/{$oldLinkTag}/i", $newLinkTag, (string) $postContent );
		}

		return $postContent;
	}

	/**
	 * Inserts a given value for a given image attribute.
	 *
	 * @since 1.0.0
	 *
	 * @param  string $linkTag       The HTML tag.
	 * @param  string $attributeName The attribute name.
	 * @param  string $value         The attribute value.
	 * @return array                 The modified attributes.
	 */
	private function insertAttribute( $linkTag, $attributeName, $value ) {
		if ( empty( $value ) ) {
			return $linkTag;
		}

		$value   = esc_attr( $value );
		$linkTag = preg_replace( $this->attributeRegex( $attributeName, true ), '${1}' . $value . '${2}', (string) $linkTag, 1, $count );

		// Attribute does not exist. Let's append it at the beginning of the tag.
		if ( ! $count ) {
			$linkTag = preg_replace( '/<a /', '<a ' . $this->attributeToHtml( $attributeName, $value ) . ' ', (string) $linkTag );
		}

		return $linkTag;
	}

	/**
	 * Returns a regex string to match an attribute.
	 *
	 * @since 1.0.0
	 *
	 * @param  string $attributeName      The attribute name.
	 * @param  bool   $groupReplaceValue  Regex groupings without the value.
	 * @return string                     The regex string.
	 */
	private function attributeRegex( $attributeName, $groupReplaceValue = false ) {
		$regex = $groupReplaceValue ? "/(\s%s=['\"]).*?(['\"])/" : "/\s%s=['\"](.*?)['\"]/";

		return sprintf( $regex, trim( $attributeName ) );
	}

	/**
	 * Returns an attribute as HTML.
	 *
	 * @since 1.0.0
	 *
	 * @param  string $attributeName The attribute name.
	 * @param  string $value         The attribute value.
	 * @return string                The HTML formatted attribute.
	 */
	private function attributeToHtml( $attributeName, $value ) {
		return sprintf( '%s="%s"', $attributeName, esc_attr( $value ) );
	}
}