<?php

/**
 * Blocks uploader class
 */

namespace Extendify\Shared\Services\Import;

defined('ABSPATH') || die('No direct access.');

/**
 * This class responsible for updating the blocks.
 */

class BlocksUpdater
{
    /**
     * The class to target.
     *
     * @var array The class names that we want to target.
     */
    protected $classesToTarget = ['extendify-image-import', 'ext-import'];

    /**
     * Update the content of the blocks in a specific post.
     *
     * @param \WP_Post $post WordPress post.
     * @return string The updated post content.
     */
    public function getModifiedBlocksInPost($post)
    {
        $blocks = parse_blocks($post->post_content);

        $updatedBlocks = $this->processAndMutateBlocks($blocks, $post->post_author);

        return str_replace('\u002d\u002d', '--', serialize_blocks($updatedBlocks));
    }

    /**
     * The logic for the update blocks code.
     *
     * @param array  $blocks WordPress post blocks.
     * @param string $author WordPress post author.
     * @return array
     */
    protected function processAndMutateBlocks($blocks, $author)
    {
        return array_map(function ($block) use ($author) {
            $block = $this->processBlock($block, $author);

            if (is_wp_error($block)) {
                return $block;
            }

            if (!empty($block['innerBlocks']) && !is_null($block['blockName'])) {
                $block['innerBlocks'] = $this->processAndMutateBlocks($block['innerBlocks'], $author);
            }

            return $block;
        }, $blocks);
    }

    /**
     *  Check if the block should not be processed.
     *
     * @param array $block the core/image block that we need to update.
     * @return bool
     */
    protected function needsImageProcessing($block)
    {
        // Check if the attributes has an element called `className` with the value of `extendify-image-import`.
        // if the returned array is empty, then the block should not be processed.
        $attrs = ($block['attrs'] ?? []);
        if (array_key_exists('className', $attrs)) {
            $className = is_array($attrs['className']) ? $attrs['className'] : explode(' ', $attrs['className']);
            return !empty(array_intersect($this->classesToTarget, $className));
        }

        return false;
    }

    /**
     * This function process the image block and return the new code.
     *
     * @param array  $block  the core/image block that we need to update.
     * @param string $author the post author.
     * @return array|\WP_Error
     */
    protected function processBlock($block, $author)
    {
        // Check if the block has the targeted class anywhere (even in unexpected places).
        $needsToRemoveClassName = $this->hasTargetedClassName($block);

        // Return the block unmodified if we don't find the class.
        if (!$this->needsImageProcessing($block) && !$needsToRemoveClassName) {
            return $block;
        }

        // Check if the block has an image.
        $image = $this->getImageSource($block['innerHTML']);

        if (!$image) {
            // If we found the class, but no image, then just remove the class.
            if ($needsToRemoveClassName) {
                $block = $this->removeTargetedClassAttribute($block);
                $block = $this->removeClassAttributeFromAttrs($block);
                // In some cases the block might become unformatted.
                foreach ($this->classesToTarget as $cls) {
                    $block['innerHTML'] = str_replace($cls, '', $block['innerHTML']);
                    $block['innerContent'] = array_map(function ($item) use ($cls) {
                        return !is_null($item) ? str_replace($cls, '', $item) : null;
                    }, ($block['innerContent'] ?? []));
                }
            }

            return $block;
        }

        $upload = (new ImageUploader())->uploadImage($image, $author);

        if (is_wp_error($upload)) {
            // This is used for recording the error in the logs.
            return new \WP_Error($upload->get_error_code(), $upload->get_error_message());
        }

        $block = $this->updateNewBlockAttributes($block, $upload);
        $block = $this->addImageAttributes($block, $upload);
        $block = $this->removeTargetedClassAttribute($block);
        $block = $this->removeClassAttributeFromAttrs($block);

        return $block;
    }

    /**
     * Return the image source link or an empty string.
     *
     * @param string $htmlContent The html tag that contains the image tag.
     * @return string
     */
    protected function getImageSource($htmlContent)
    {
        $html = new \WP_HTML_Tag_Processor($htmlContent);
        $html->next_tag('img');
        $src = $html->get_attribute('src');

        return $src && preg_match(
            '(' . implode('|', array_map('preg_quote', ImageUploader::$imagesDomains, ['/'])) . ')i',
            $src
        )
        ? $src
        : '';
    }

    /**
     * Update the content of the block to remove the targeted class attribute.
     *
     * @param array $block The block we need to update.
     * @return array The parsed block after updates.
     */
    protected function removeTargetedClassAttribute(array $block)
    {
        $block['innerContent'] = array_map(function ($item) {
            return !is_null($item) ? $this->removeClassAttributeFromContent($item) : null;
        }, ($block['innerContent'] ?? []));

        $block['innerHTML'] = $this->removeClassAttributeFromContent($block['innerHTML']);

        return $block;
    }

    /**
     * Remove the targeted class from the html content.
     *
     * @param string $content The html tag that contains the targeted class.
     * @return string
     */
    protected function removeClassAttributeFromContent($content)
    {
        foreach ($this->classesToTarget as $targetedClass) {
            $html = new \WP_HTML_Tag_Processor($content);
            do {
                $html->remove_class($targetedClass);
            } while ($html->next_tag(['class' => $targetedClass]));
            $content = $html->get_updated_html();
        }

        return $content;
    }

    /**
     * Remove the targeted class from the className attrs.
     *
     * @param array $block The block.
     * @return array The parsed block after updates.
     */
    protected function removeClassAttributeFromAttrs($block)
    {
        if (isset($block['attrs']['className'])) {
            $className = is_array($block['attrs']['className'])
                ? $block['attrs']['className']
                : explode(' ', $block['attrs']['className']);
            $className = array_diff($className, $this->classesToTarget);
            $block['attrs']['className'] = implode(' ', $className);
        }

        return $block;
    }

    /**
     * Update the block attributes with information about the image.
     *
     * @param array $block  Block.
     * @param array $upload The uploaded file information.
     * @return array The parse block after updates.
     */
    protected function updateNewBlockAttributes(array $block, array $upload)
    {
        $block['attrs']['id'] = $upload['attachment_id'];

        if ($block['blockName'] === 'core/media-text') {
            $block['attrs']['mediaId'] = $upload['attachment_id'];
            $block['attrs']['mediaLink'] = $upload['url'];
        }

        if ($block['blockName'] === 'core/cover') {
            $block['attrs']['url'] = $upload['url'];
        }

        return $block;
    }

    /**
     * Update the inner content for the block.
     *
     * @param array $block  Block inner content.
     * @param array $upload The uploaded file information.
     * @return array
     */
    protected function addImageAttributes($block, $upload)
    {
        $isMediaText = $block['blockName'] === 'core/media-text';

        $block['innerContent'] = array_map(function ($item) use ($upload, $isMediaText) {
            return !is_null($item) ? $this->updateImageTagAttributes($item, $upload, $isMediaText) : null;
        }, ($block['innerContent'] ?? []));

        $block['innerHTML'] = $this->updateImageTagAttributes($block['innerHTML'], $upload, $isMediaText);

        return $block;
    }

    /**
     * Checks the html and content for the class name.
     *
     * @param array $block The block.
     * @return boolean
     */
    protected function hasTargetedClassName(array $block)
    {
        if (
            array_reduce($this->classesToTarget, function (bool $carry, string $targetClass) use ($block) {
                return $carry || (strpos(($block['innerHTML'] ?? ''), $targetClass) !== false);
            }, false)
        ) {
            return true;
        }

        $classList = is_array(($block['attrs']['className'] ?? null))
            ? $block['attrs']['className']
            : explode(' ', ($block['attrs']['className'] ?? ''));

        return !empty(array_intersect($this->classesToTarget, $classList));
    }

    /**
     * Return the new html content after making the required changes.
     *
     * @param string $htmlContent The html tag that contains the image tag.
     * @param array  $upload      The uploaded file information.
     * @param bool   $isMediaText Is the block a media text block, if so, we need to update the style attribute.
     * @return string
     */
    protected function updateImageTagAttributes($htmlContent, $upload, $isMediaText = false)
    {
        $html = new \WP_HTML_Tag_Processor($htmlContent);

        // Media text block needs a bit more work to update the style attribute.
        if (
            $isMediaText && $html->next_tag([
            'tag_name' => 'figure',
            'class_name' => 'wp-block-media-text__media',
            ])
        ) {
            $style = $html->get_attribute('style');
            if (preg_match('/:url\(*.+\)/m', $style, $matches)) {
                $result = str_replace($matches[0], ':url(' . $upload['url'] . ')', $style);
                $html->set_attribute('style', ($result ?? ''));
            }
        }

        // Update the image tag attributes.
        if ($html->next_tag('img')) {
            $html->set_attribute('src', $upload['url']);
            $html->add_class('wp-image-' . $upload['attachment_id']);
            if ($isMediaText && !$html->has_class('size-full')) {
                $html->add_class('size-full');
            }
        }

        return $html->get_updated_html();
    }
}
