<?php
namespace DevOwl\RealThumbnailGenerator\attachment;

use DevOwl\RealThumbnailGenerator\base\UtilsProvider;
use DevOwl\RealThumbnailGenerator\lite\attachment\Thumbnail as AttachmentThumbnail;
use DevOwl\RealThumbnailGenerator\overrides\interfce\attachment\IOverrideThumbnail;
use DevOwl\RealThumbnailGenerator\Util;
use WP_Error;
use WP_Post;

// @codeCoverageIgnoreStart
defined('ABSPATH') or die('No script kiddies please!'); // Avoid direct file request
// @codeCoverageIgnoreEnd

/**
 * Thumbnail handler
 */
class Thumbnail implements IOverrideThumbnail {
    use UtilsProvider;
    use AttachmentThumbnail;

    const DEFAULT_SCHEMA_FILENAME = '%name%-%image-width%x%image-height%.%extension%';

    private static $me = null;

    /**
     * Cache the image sizes generated by this::get_image_sizes
     */
    private $imageSizes = null;

    /**
     * Make the big check array to a minimal array for the JS handling.
     *
     * - cntAvailable <= available
     * - cntMustGenerate <= mustGenerate
     * - cntRegistered
     * - cntDeletable <= deletable
     * - deletableSpace
     *
     * @param array $check The result of check()
     */
    public function prepareForJS($check) {
        if (!is_wp_error($check)) {
            return [
                'cntAvailable' => count($check['available']),
                'cntMustGenerate' => count($check['mustGenerate']),
                'cntRegistered' => $check['cntRegistered'],
                'cntDeletable' => count($check['deletable']),
                'newThumbnailsCount' => $check['newThumbnailsCount'],
                'deletableSpace' => $check['deletableSpace'],
                'filesize' => $check['filesize'],
            ];
        } else {
            return [
                'cntAvailable' => 0,
                'cntMustGenerate' => 0,
                'cntRegistered' => 0,
                'cntDeletable' => 0,
                'newThumbnailsCount' => 0,
                'deletableSpace' => 0,
                'filesize' => 0,
            ];
        }
    }

    /**
     * Load a RTG container for a given post ID.
     *
     * @param int $postId The post id
     * @param boolean $checkCap Determines if you want to check the user capability
     * @param string $attachedFile Use this attached file for the check
     * @param array $meta
     * @return array|WP_Error
     */
    public function check($postId, $checkCap = true, $attachedFile = null, $meta = null) {
        $attachedFile = $attachedFile !== null ? $attachedFile : get_attached_file($postId);

        if ($attachedFile === false || !file_exists($attachedFile)) {
            return new WP_Error('not-found', __('The given image was not found on server.', RTG_TD));
        }

        if ($checkCap && !current_user_can('manage_options')) {
            return new WP_Error('no-permissions', __('You have not the permissions to regenerate thumbnails.', RTG_TD));
        }

        $isImage = wp_attachment_is_image($postId);
        $isPdf = $this->isPdf($postId);
        if (!($isImage || $isPdf)) {
            return new WP_Error('not-an-image', __('The given post ID is not an image.', RTG_TD));
        }

        // Get the metadata for the given attachment
        $meta = $meta !== null ? $meta : wp_get_attachment_metadata($postId);
        $disrupted = $isPdf && empty($meta); // @see https://app.clickup.com/2088/758836/t/1b47f
        if ($disrupted) {
            $meta = ['sizes' => []];
        }

        // Get registered sizes
        $wpSizes = $isPdf ? $this->get_pdf_sizes($meta) : $this->get_image_sizes();

        // Define the return array
        $result = [
            'id' => (int) $postId,
            'schema' => !empty($meta['schema']) ? $meta['schema'] : self::DEFAULT_SCHEMA_FILENAME,
            'schemaf' => !empty($meta['schemaf']) ? $meta['schemaf'] : '',
            'meta' => $meta,
            'attachedFile' => $attachedFile,
            'filename' => basename($attachedFile),
            'cntGenerated' => 0, // @uses $cnt
            'cntRegistered' => count($wpSizes),
            'newThumbnailsCount' => 0, // Count of thumbnails which are missing and gets regenerated
            'filesizeSum' => 0, // @uses $filesizeSum
            'filesize' => filesize($attachedFile), // Full filesize
            'unused' => [], // Array of unused (not anymore registered) thumbnail files
            'mustGenerate' => [], // Array of thumbnail sizes which are not generated yet
            'notFound' => [], // Array of thumbnail files which are generated but no more available
            'deletable' => [], // Array of thumbnail sizes which can be deleted from filesystem because they are no longer registered
            'available' => [], // Array of thumbnail sizes which are availabe on the file system
            'deletableSpace' => 0, // Filesize sum of the deletable files
            'rows' => [], // Prepared JSON scheme
        ];

        $cnt = 0;
        $filesizeSum = $result['filesize'];
        if (isset($meta['sizes']) && ($cnt = count($meta['sizes'])) > 0) {
            $result['cntGenerated'] = $cnt;
            foreach ($meta['sizes'] as $key => $value) {
                // Skip failing schema save (see Adapter::applyTemporaryMetadata)
                if (in_array($key, ['___schemas', '__schemas'], true)) {
                    unset($meta['sizes'][$key]);
                    unset($result['meta']['sizes'][$key]);
                    $GLOBALS['rtg_suppress_update_metadata'] = true;
                    wp_update_attachment_metadata($postId, $meta);
                    $GLOBALS['rtg_suppress_update_metadata'] = false;
                    continue;
                }

                // Read data for thumbnail
                $href = wp_get_attachment_image_src($postId, [$value['width'], $value['height']]);
                $thumbnailPath = dirname($attachedFile) . '/' . $value['file'];
                $registeredSize = isset($wpSizes[$key]) ? $wpSizes[$key] : null;
                $filesFound = true;
                $needsRegeneration = false;

                // Create dimension string
                $dimsString = $value['width'] . ' x ' . $value['height'];

                // Available on filesystem
                if (file_exists($thumbnailPath) && !is_dir($thumbnailPath)) {
                    $filesize = filesize($thumbnailPath);
                    $filesizeSum += $filesize;
                    $status = size_format($filesize, 2);
                    $result['available'][$key] = $value;

                    // Check if no longer registered and can be deleted
                    if (!is_array($registeredSize)) {
                        $result['deletable'][$key] = $value;
                        $result['deletableSpace'] += $filesize;
                    } elseif (
                        // Check if it needs regeneration
                        isset($meta['width'], $meta['height']) &&
                        ($dims = image_resize_dimensions(
                            $meta['width'],
                            $meta['height'],
                            $registeredSize['width'],
                            $registeredSize['height'],
                            $registeredSize['crop']
                        )) &&
                        ($shouldWidth = $dims[4]) > 0 &&
                        ($shouldHeight = $dims[5]) > 0
                    ) {
                        // If it is cropped, take the higher side and check if it is cropped already with our image
                        if ($registeredSize['crop']) {
                            $needsRegeneration =
                                !in_array($shouldWidth, [$value['width'], $value['height']], true) &&
                                !in_array($shouldHeight, [$value['width'], $value['height']], true);
                            if ($needsRegeneration) {
                                $dimsString .= ' ➜ ' . $dims[4] . ' x ' . $dims[5];
                                $result['mustGenerate'][$key] = $value;
                            }
                        } else {
                            $needsRegeneration = $shouldWidth !== $value['width'] || $shouldHeight !== $value['height'];

                            if ($needsRegeneration) {
                                $dimsString .= ' ➜ ' . $dims[4] . ' x ' . $dims[5];
                                $result['mustGenerate'][$key] = $value;
                            }
                        }
                    }
                } else {
                    $filesFound = false;
                    $status = __('Not found', RTG_TD);
                    $result['notFound'][$key] = $value;
                }

                $result['rows'][$key] = array_merge($value, [
                    'dimension' => $dimsString,
                    'status' => $status,
                    'href' => $href[0],
                    'needsRegeneration' => $needsRegeneration,
                    'registeredSize' => is_array($registeredSize),
                    'filesFound' => $filesFound,
                    'unused' => false,
                ]);
            }

            $result['filesizeSum'] = $filesizeSum;
        }

        /**
         * Check if there are sizes which are not available yet
         * and the regenerate is mandatory
         */
        foreach ($wpSizes as $key => $value) {
            if (isset($meta['width']) && isset($meta['height']) && isset($value['width']) && isset($value['height'])) {
                $dims = image_resize_dimensions(
                    $meta['width'],
                    $meta['height'],
                    $value['width'],
                    $value['height'],
                    $value['crop']
                );
            } elseif (!$disrupted) {
                continue;
            } else {
                $dims = true;
            }
            $status = '';

            // Generate width output
            $dimsString = '';
            $dimsString .= is_numeric($value['width']) && $value['width'] > 0 ? $value['width'] : 'auto';
            $dimsString .= ' x ';
            $dimsString .= is_numeric($value['height']) && $value['height'] ? $value['height'] : 'auto';

            // The size should be here
            if (
                !$disrupted &&
                $dims !== false &&
                isset($meta['sizes']) &&
                !isset($meta['sizes'][$key]) &&
                ($meta['width'] > $value['width'] || $meta['height'] > $value['height'])
            ) {
                $status = __('Not generated', RTG_TD);
                $result['mustGenerate'][$key] = $value;
            }

            // Fix bug with failed PDF regeneration
            if ($disrupted) {
                $status = __('Disrupted', RTG_TD);
                $result['mustGenerate'][$key] = $value;
            }

            // Is not needed
            $notNeeded = $dims === false && ($value['width'] > 0 || $value['height'] > 0);
            if ($notNeeded) {
                $status = __('Greater than original', RTG_TD);
                $result['unused'][$key] = $value;
            }

            // Generate output
            if (!empty($status)) {
                $result['rows'][$key] = array_merge($value, [
                    'dimension' => $dimsString,
                    'status' => $status,
                    'href' => '',
                    'needsRegeneration' => false,
                    'registeredSize' => true,
                    'filesFound' => false,
                    'unused' => $notNeeded,
                ]);
            }
        }

        $result['newThumbnailsCount'] = count($result['mustGenerate']) + count($result['notFound']);
        return $result;
    }

    /**
     * Get attached file absolute path from metadata.
     *
     * @param int $attachment_id
     * @param array $meta
     * @param boolean $unfiltered
     * @return string|false
     * @see https://developer.wordpress.org/reference/functions/get_attached_file/
     */
    public function get_attached_file_from_meta($attachment_id, $meta, $unfiltered = false) {
        //$file = get_post_meta( $attachment_id, '_wp_attached_file', true );
        $file = isset($meta['file']) ? $meta['file'] : get_attached_file($attachment_id);

        // If the file is relative, prepend upload dir.
        if (
            $file &&
            0 !== strpos($file, '/') &&
            !preg_match('|^.:\\\|', $file) &&
            (($uploads = wp_get_upload_dir()) && false === $uploads['error'])
        ) {
            $file = $uploads['basedir'] . "/$file";
        }

        if ($unfiltered) {
            return $file;
        }

        /**
         * Filters the attached file based on the given ID.
         *
         * @since 2.1.0
         *
         * @param string $file          Path to attached file.
         * @param int    $attachment_id Attachment ID.
         */
        return apply_filters('get_attached_file', $file, $attachment_id);
    }

    /**
     * Get size information for all currently-registered image sizes.
     *
     * @param boolean $fromCache
     * @param boolean $searchBy
     * @param int $searchWidth
     * @param int $searchHeight
     * @param boolean $searchCrop
     * @return array|false Data for all currently-registered image sizes or `false` if `$searchBy` is `true` and no result was found
     * @link https://codex.wordpress.org/Function_Reference/get_intermediate_image_sizes
     */
    public function get_image_sizes(
        $fromCache = true,
        $searchBy = false,
        $searchWidth = null,
        $searchHeight = null,
        $searchCrop = false
    ) {
        if ($fromCache === true && $this->imageSizes !== null && !$searchBy) {
            return $this->imageSizes;
        } else {
            global $_wp_additional_image_sizes;
            $sizes = [];

            foreach (get_intermediate_image_sizes() as $_size) {
                if (in_array($_size, ['thumbnail', 'medium', 'medium_large', 'large'], true)) {
                    // WordPress core
                    $sizes[$_size]['width'] = (int) get_option("{$_size}_size_w");
                    $sizes[$_size]['height'] = (int) get_option("{$_size}_size_h");
                    $sizes[$_size]['crop'] = (bool) get_option("{$_size}_crop");
                } elseif (isset($_wp_additional_image_sizes[$_size])) {
                    // Customized sizes
                    $width = $_wp_additional_image_sizes[$_size]['width'];
                    $height = $_wp_additional_image_sizes[$_size]['height'];
                    $crop = $_wp_additional_image_sizes[$_size]['crop'];

                    // WooCommerce compatibility
                    if (
                        function_exists('wc_get_image_size') &&
                        (Util::getInstance()->startsWith($_size, 'woocommerce_') ||
                            Util::getInstance()->startsWith($_size, 'shop_'))
                    ) {
                        $wc = wc_get_image_size($_size);
                        $width = $wc['width'];
                        $height = $wc['height'];
                        $crop = $wc['crop'];
                    }

                    // Create result
                    $sizes[$_size] = [
                        'width' => (int) $width,
                        'height' => (int) $height,
                        'crop' => $crop,
                    ];

                    // Check if numeric value so it is converted to boolean
                    if (is_numeric($crop)) {
                        $sizes[$_size]['crop'] = (bool) $crop;
                    }
                }
            }
            $this->imageSizes = $sizes;
        }

        if ($searchBy) {
            foreach ($sizes as $key => $search) {
                if (
                    // phpcs:disable Universal.Operators.StrictComparisons.LooseEqual
                    ($searchCrop && ($search['width'] == $searchWidth || $search['height'] == $searchHeight)) ||
                    ($search['width'] == $searchWidth && $search['height'] == $searchHeight)
                    // phpcs:enable Universal.Operators.StrictComparisons.LooseEqual
                ) {
                    return [$key, $search];
                }
            }

            return false; // Nothing found.
        }

        return $sizes;
    }

    /**
     * Short-hand for get_image_sizes.
     *
     * @param int $width
     * @param int $height
     * @param boolean|array $crop
     */
    public function get_image_sizes_by($width, $height, $crop) {
        return $this->get_image_sizes(false, true, $width, $height, $crop);
    }

    /**
     * Get available PDF sizes. Learn more about it in wp-admin/includes/image.php#L208.
     *
     * @param array $metadata
     * @param boolean $searchBy
     * @param int $width
     * @param int $height
     * @param boolean|array $crop
     * @return array|false Data for all currently-registered image sizes or `false` if `$searchBy` is `true` and no result was found
     */
    public function get_pdf_sizes($metadata, $searchBy = false, $width = null, $height = null, $crop = false) {
        $fallback_sizes = apply_filters(
            'fallback_intermediate_image_sizes',
            ['thumbnail', 'medium', 'large'],
            $metadata
        );
        $sizes = [];
        $_wp_additional_image_sizes = wp_get_additional_image_sizes();
        foreach ($fallback_sizes as $s) {
            if (isset($_wp_additional_image_sizes[$s]['width'])) {
                $sizes[$s]['width'] = intval($_wp_additional_image_sizes[$s]['width']);
            } else {
                $sizes[$s]['width'] = get_option("{$s}_size_w");
            }
            if (isset($_wp_additional_image_sizes[$s]['height'])) {
                $sizes[$s]['height'] = intval($_wp_additional_image_sizes[$s]['height']);
            } else {
                $sizes[$s]['height'] = get_option("{$s}_size_h");
            }
            if (isset($_wp_additional_image_sizes[$s]['crop'])) {
                $sizes[$s]['crop'] = $_wp_additional_image_sizes[$s]['crop'];
            } else {
                // Force thumbnails to be soft crops.
                if (!'thumbnail' === $s) {
                    $sizes[$s]['crop'] = get_option("{$s}_crop");
                } else {
                    $sizes[$s]['crop'] = false;
                }
            }
        }

        // Add full size
        if (isset($metadata) && isset($metadata['sizes']) && isset($metadata['sizes']['full'])) {
            $sizes['full'] = [
                'width' => $metadata['sizes']['full']['width'],
                'height' => $metadata['sizes']['full']['height'],
                'crop' => false,
            ];
        }

        if ($searchBy) {
            foreach ($sizes as $key => $search) {
                if (
                    // phpcs:disable Universal.Operators.StrictComparisons.LooseEqual
                    ($crop && ($search['width'] == $width || $search['height'] == $height)) ||
                    ($search['width'] == $width && $search['height'] == $height)
                    // phpcs:enable Universal.Operators.StrictComparisons.LooseEqual
                ) {
                    return [$key, $search];
                }
            }

            return false; // Nothing found.
        }

        return $searchBy ? false : $sizes;
    }

    /**
     * Short-hand for get_pdf_sizes.
     *
     * @param int $width
     * @param int $height
     * @param boolean|array $crop
     */
    public function get_pdf_sizes_by($width, $height, $crop) {
        return $this->get_pdf_sizes(null, true, $width, $height, $crop);
    }

    /**
     * Get synchronized attachment details.
     *
     * @param int $post_id The post id
     * @return object|false
     */
    public function getData($post_id) {
        global $wpdb;
        $table_name = $this->getTableName('attachments');
        // phpcs:disable WordPress.DB.PreparedSQL
        $result = $wpdb->get_row($wpdb->prepare("SELECT * FROM $table_name WHERE post_id=%d", $post_id));
        // phpcs:enable WordPress.DB.PreparedSQL
        return is_object($result) ? $result : false;
    }

    /**
     * Get current schema.
     */
    public function getSchema() {
        return trailingslashit($this->getSchemaFolder()) . $this->getSchemaFilename();
    }

    /**
     * Check if passed post is a PDF.
     *
     * @param int|WP_Post $post
     */
    public function isPdf($post) {
        return $this->isPdfable() && get_post_mime_type($post) === 'application/pdf';
    }

    /**
     * Check if current WordPress version supports PDF thumbnails.
     */
    public function isPdfable() {
        global $wp_version;
        return version_compare($wp_version, '4.7', '>=');
    }

    /**
     * When an attachment gets deleted prune also RTG data.
     *
     * @param int $post_id
     */
    public function delete_attachment($post_id) {
        global $wpdb;
        $table_name = $this->getTableName('attachments');
        // phpcs:disable WordPress.DB.PreparedSQL
        $wpdb->query($wpdb->prepare("DELETE FROM $table_name WHERE post_id=%d", $post_id));
        // phpcs:enable WordPress.DB.PreparedSQL
    }

    /**
     * Get singleton instance.
     *
     * @return Thumbnail
     */
    public static function getInstance() {
        return self::$me === null ? (self::$me = new Thumbnail()) : self::$me;
    }
}
