<?php declare(strict_types=1);

require_once "config.php";
require_once "events.php";
require_once "media_engine.php";

/*
* This is used by the media code when there is an error
*/
class MediaException extends SCoreException
{
}

class Media extends Extension
{
    /** @var MediaTheme */
    protected $theme;

    const WEBP_LOSSY = "webp-lossy";
    const WEBP_LOSSLESS = "webp-lossless";

    const IMAGE_MEDIA_ENGINES = [
        "GD" => MediaEngine::GD,
        "ImageMagick" => MediaEngine::IMAGICK,
    ];

    const LOSSLESS_FORMATS = [
        self::WEBP_LOSSLESS,
        EXTENSION_PNG,
        EXTENSION_PSD,
        EXTENSION_BMP,
        EXTENSION_ICO,
        EXTENSION_CUR,
        EXTENSION_ANI,
        EXTENSION_GIF

    ];

    const ALPHA_FORMATS = [
        self::WEBP_LOSSLESS,
        self::WEBP_LOSSY,
        EXTENSION_WEBP,
        EXTENSION_PNG,
    ];

    const FORMAT_ALIASES = [
        EXTENSION_TIF => EXTENSION_TIFF,
        EXTENSION_JPEG => EXTENSION_JPG,
    ];


    //RIFF####WEBPVP8?..............ANIM
    private const WEBP_ANIMATION_HEADER =
        [0x52, 0x49, 0x46, 0x46, null, null, null, null, 0x57, 0x45, 0x42, 0x50, 0x56, 0x50, 0x38, null,
            null, null, null, null, null, null, null, null, null, null, null, null, null, null, 0x41, 0x4E, 0x49, 0x4D];

    //RIFF####WEBPVP8L
    private const WEBP_LOSSLESS_HEADER =
        [0x52, 0x49, 0x46, 0x46, null, null, null, null, 0x57, 0x45, 0x42, 0x50, 0x56, 0x50, 0x38, 0x4C];


    public static function imagick_available(): bool
    {
        return extension_loaded("imagick");
    }

    /**
     * High priority just so that it can be early in the settings
     */
    public function get_priority(): int
    {
        return 30;
    }

    public function onInitExt(InitExtEvent $event)
    {
        global $config;
        $config->set_default_string(MediaConfig::FFPROBE_PATH, 'ffprobe');
        $config->set_default_int(MediaConfig::MEM_LIMIT, parse_shorthand_int('8MB'));
        $config->set_default_string(MediaConfig::FFMPEG_PATH, 'ffmpeg');
        $config->set_default_string(MediaConfig::CONVERT_PATH, 'convert');
    }

    public function onPageRequest(PageRequestEvent $event)
    {
        global $page, $user;

        if ($event->page_matches("media_rescan/") && $user->can(Permissions::RESCAN_MEDIA) && isset($_POST['image_id'])) {
            $image = Image::by_id(int_escape($_POST['image_id']));

            send_event(new MediaCheckPropertiesEvent($image));
            $image->save_to_db();

            $page->set_mode(PageMode::REDIRECT);
            $page->set_redirect(make_link("post/view/$image->id"));
        }
    }

    public function onSetupBuilding(SetupBuildingEvent $event)
    {
        $sb = new SetupBlock("Media Engines");

//        if (self::imagick_available()) {
//            try {
//                $image = new Imagick(realpath('tests/favicon.png'));
//                $image->clear();
//                $sb->add_label("ImageMagick detected");
//            } catch (ImagickException $e) {
//                $sb->add_label("<b style='color:red'>ImageMagick not detected</b>");
//            }
//        } else {
        $sb->start_table();
        $sb->add_table_header("Commands");

        $sb->add_text_option(MediaConfig::CONVERT_PATH, "convert", true);
//        }

        $sb->add_text_option(MediaConfig::FFMPEG_PATH, "ffmpeg", true);
        $sb->add_text_option(MediaConfig::FFPROBE_PATH, "ffprobe", true);

        $sb->add_shorthand_int_option(MediaConfig::MEM_LIMIT, "Mem limit: ", true);
        $sb->end_table();

        $event->panel->add_block($sb);
    }

    public function onAdminBuilding(AdminBuildingEvent $event)
    {
        global $database;
        $types = $database->get_all("SELECT ext, count(*) count FROM images group by ext");

        $this->theme->display_form($types);
    }

    public function onAdminAction(AdminActionEvent $event)
    {
        $action = $event->action;
        if (method_exists($this, $action)) {
            $event->redirect = $this->$action();
        }
    }

    public function onImageAdminBlockBuilding(ImageAdminBlockBuildingEvent $event)
    {
        global $user;
        if ($user->can(Permissions::DELETE_IMAGE)) {
            $event->add_part($this->theme->get_buttons_html($event->image->id));
        }
    }

    public function onBulkActionBlockBuilding(BulkActionBlockBuildingEvent $event)
    {
        global $user;
        if ($user->can(Permissions::RESCAN_MEDIA)) {
            $event->add_action("bulk_media_rescan", "Scan Media Properties");
        }
    }

    public function onBulkAction(BulkActionEvent $event)
    {
        global $page, $user;

        switch ($event->action) {
            case "bulk_media_rescan":
                if ($user->can(Permissions::RESCAN_MEDIA)) {
                    $total = 0;
                    $failed = 0;
                    foreach ($event->items as $image) {
                        try {
                            log_debug("media", "Rescanning media for {$image->hash} ({$image->id})");
                            send_event(new MediaCheckPropertiesEvent($image));
                            $image->save_to_db();
                            $total++;
                        } catch (MediaException $e) {
                            $failed++;
                        }
                    }
                    $page->flash("Scanned media properties for $total items, failed for $failed");
                }
                break;
        }
    }

    public function onCommand(CommandEvent $event)
    {
        if ($event->cmd == "help") {
            print "\tmedia-rescan <id / hash>\n";
            print "\t\trefresh metadata for a given post\n\n";
        }
        if ($event->cmd == "media-rescan") {
            $uid = $event->args[0];
            $image = Image::by_id_or_hash($uid);
            if ($image) {
                send_event(new MediaCheckPropertiesEvent($image));
                $image->save_to_db();
            } else {
                print("No post with ID '$uid'\n");
            }
        }
    }

    /**
     * @param MediaResizeEvent $event
     * @throws MediaException
     * @throws InsufficientMemoryException
     */
    public function onMediaResize(MediaResizeEvent $event)
    {
        switch ($event->engine) {
            case MediaEngine::GD:
                $info = getimagesize($event->input_path);
                if ($info === false) {
                    throw new MediaException("getimagesize failed for " . $event->input_path);
                }

                self::image_resize_gd(
                    $event->input_path,
                    $info,
                    $event->target_width,
                    $event->target_height,
                    $event->output_path,
                    $event->target_format,
                    $event->ignore_aspect_ratio,
                    $event->target_quality,
                    $event->allow_upscale
                );

                break;
            case MediaEngine::IMAGICK:
//                if (self::imagick_available()) {
//                } else {
                self::image_resize_convert(
                    $event->input_path,
                    $event->input_type,
                    $event->target_width,
                    $event->target_height,
                    $event->output_path,
                    $event->target_format,
                    $event->ignore_aspect_ratio,
                    $event->target_quality,
                    $event->minimize,
                    $event->allow_upscale
                );
                //}
                break;
            case MediaEngine::STATIC:
                copy($event->input_path, $event->output_path);
                break;
            default:
                throw new MediaException("Engine not supported for resize: " . $event->engine);
        }

        // TODO: Get output optimization tools working better
//        if ($config->get_bool("thumb_optim", false)) {
//            exec("jpegoptim $outname", $output, $ret);
//        }
    }


    const CONTENT_SEARCH_TERM_REGEX = "/^content[=|:]((video)|(audio)|(image)|(unknown))$/i";


    public function onSearchTermParse(SearchTermParseEvent $event)
    {
        global $database;

        if (is_null($event->term)) {
            return;
        }

        $matches = [];
        if (preg_match(self::CONTENT_SEARCH_TERM_REGEX, $event->term, $matches)) {
            $field = $matches[1];
            if ($field==="unknown") {
                $event->add_querylet(new Querylet("video IS NULL OR audio IS NULL OR image IS NULL"));
            } else {
                $event->add_querylet(new Querylet($database->scoreql_to_sql("$field = SCORE_BOOL_Y")));
            }
        }
    }

    public function onHelpPageBuilding(HelpPageBuildingEvent $event)
    {
        if ($event->key===HelpPages::SEARCH) {
            $block = new Block();
            $block->header = "Media";
            $block->body = $this->theme->get_help_html();
            $event->add_block($block);
        }
    }

    public function onTagTermCheck(TagTermCheckEvent $event)
    {
        if (preg_match(self::CONTENT_SEARCH_TERM_REGEX, $event->term)) {
            $event->metatag = true;
        }
    }

    public function onParseLinkTemplate(ParseLinkTemplateEvent $event)
    {
        if ($event->image->width && $event->image->height && $event->image->length) {
            $s = ((int)($event->image->length / 100))/10;
            $event->replace('$size', "{$event->image->width}x{$event->image->height}, ${s}s");
        } elseif ($event->image->width && $event->image->height) {
            $event->replace('$size', "{$event->image->width}x{$event->image->height}");
        } elseif ($event->image->length) {
            $s = ((int)($event->image->length / 100))/10;
            $event->replace('$size', "${s}s");
        }

        $event->replace('$ext', $event->image->ext);
    }

    /**
     * Check Memory usage limits
     *
     * Old check:   $memory_use = (filesize($image_filename)*2) + ($width*$height*4) + (4*1024*1024);
     * New check:   $memory_use = $width * $height * ($bits_per_channel) * channels * 2.5
     *
     * It didn't make sense to compute the memory usage based on the NEW size for the image. ($width*$height*4)
     * We need to consider the size that we are GOING TO instead.
     *
     * The factor of 2.5 is simply a rough guideline.
     * https://stackoverflow.com/questions/527532/reasonable-php-memory-limit-for-image-resize
     *
     * @param array $info The output of getimagesize() for the source file in question.
     * @return int The number of bytes an image resize operation is estimated to use.
     */
    public static function calc_memory_use(array $info): int
    {
        if (isset($info['bits']) && isset($info['channels'])) {
            $memory_use = ($info[0] * $info[1] * ($info['bits'] / 8) * $info['channels'] * 2.5) / 1024;
        } else {
            // If we don't have bits and channel info from the image then assume default values
            // of 8 bits per color and 4 channels (R,G,B,A) -- ie: regular 24-bit color
            $memory_use = ($info[0] * $info[1] * 1 * 4 * 2.5) / 1024;
        }
        return (int)$memory_use;
    }


    /**
     * Creates a thumbnail using ffmpeg.
     *
     * @param $hash
     * @return bool true if successful, false if not.
     * @throws MediaException
     */
    public static function create_thumbnail_ffmpeg($hash): bool
    {
        global $config;

        $ffmpeg = $config->get_string(MediaConfig::FFMPEG_PATH);
        if ($ffmpeg == null || $ffmpeg == "") {
            throw new MediaException("ffmpeg command configured");
        }

        $inname = warehouse_path(Image::IMAGE_DIR, $hash);
        $outname = warehouse_path(Image::THUMBNAIL_DIR, $hash);

        $orig_size = self::video_size($inname);
        $scaled_size = get_thumbnail_size($orig_size[0], $orig_size[1], true);

        $codec = "mjpeg";
        $quality = $config->get_int(ImageConfig::THUMB_QUALITY);
        if ($config->get_string(ImageConfig::THUMB_TYPE) == EXTENSION_WEBP) {
            $codec = "libwebp";
        } else {
            // mjpeg quality ranges from 2-31, with 2 being the best quality.
            $quality = floor(31 - (31 * ($quality / 100)));
            if ($quality < 2) {
                $quality = 2;
            }
        }

        $args = [
            escapeshellarg($ffmpeg),
            "-y", "-i", escapeshellarg($inname),
            "-vf", "thumbnail,scale={$scaled_size[0]}:{$scaled_size[1]}",
            "-f", "image2",
            "-vframes", "1",
            "-c:v", $codec,
            "-q:v", $quality,
            escapeshellarg($outname),
        ];

        $cmd = escapeshellcmd(implode(" ", $args));

        exec($cmd, $output, $ret);

        if ((int)$ret == (int)0) {
            log_debug('media', "Generating thumbnail with command `$cmd`, returns $ret");
            return true;
        } else {
            log_error('media', "Generating thumbnail with command `$cmd`, returns $ret");
            return false;
        }
    }


    public static function get_ffprobe_data($filename): array
    {
        global $config;

        $ffprobe = $config->get_string(MediaConfig::FFPROBE_PATH);
        if ($ffprobe == null || $ffprobe == "") {
            throw new MediaException("ffprobe command configured");
        }

        $args = [
            escapeshellarg($ffprobe),
            "-print_format", "json",
            "-v", "quiet",
            "-show_format",
            "-show_streams",
            escapeshellarg($filename),
        ];

        $cmd = escapeshellcmd(implode(" ", $args));

        exec($cmd, $output, $ret);

        if ((int)$ret == (int)0) {
            log_debug('media', "Getting media data `$cmd`, returns $ret");
            $output = implode($output);
            return json_decode($output, true);
        } else {
            log_error('media', "Getting media data `$cmd`, returns $ret");
            return [];
        }
    }

    public static function determine_ext(string $format): string
    {
        $format = self::normalize_format($format);
        switch ($format) {
            case self::WEBP_LOSSLESS:
            case self::WEBP_LOSSY:
                return EXTENSION_WEBP;
            default:
                return $format;
        }
    }

//    private static function image_save_imagick(Imagick $image, string $path, string $format, int $output_quality = 80, bool $minimize)
//    {
//        switch ($format) {
//            case EXTENSION_PNG:
//                $result = $image->setOption('png:compression-level', 9);
//                if ($result !== true) {
//                    throw new GraphicsException("Could not set png compression option");
//                }
//                break;
//            case Graphics::WEBP_LOSSLESS:
//                $result = $image->setOption('webp:lossless', true);
//                if ($result !== true) {
//                    throw new GraphicsException("Could not set lossless webp option");
//                }
//                break;
//            default:
//                $result = $image->setImageCompressionQuality($output_quality);
//                if ($result !== true) {
//                    throw new GraphicsException("Could not set compression quality for $path to $output_quality");
//                }
//                break;
//        }
//
//        if (self::supports_alpha($format)) {
//            $result = $image->setImageBackgroundColor(new \ImagickPixel('transparent'));
//        } else {
//            $result = $image->setImageBackgroundColor(new \ImagickPixel('black'));
//        }
//        if ($result !== true) {
//            throw new GraphicsException("Could not set background color");
//        }
//
//
//        if ($minimize) {
//            $profiles = $image->getImageProfiles("icc", true);
//            $result = $image->stripImage();
//            if ($result !== true) {
//                throw new GraphicsException("Could not strip information from image");
//            }
//            if (!empty($profiles)) {
//                $image->profileImage("icc", $profiles['icc']);
//            }
//        }
//
//        $ext = self::determine_ext($format);
//
//        $result = $image->writeImage($ext . ":" . $path);
//        if ($result !== true) {
//            throw new GraphicsException("Could not write image to $path");
//        }
//    }

//    public static function image_resize_imagick(
//        String $input_path,
//        String $input_type,
//        int $new_width,
//        int $new_height,
//        string $output_filename,
//        string $output_type = null,
//        bool $ignore_aspect_ratio = false,
//        int $output_quality = 80,
//        bool $minimize = false,
//        bool $allow_upscale = true
//    ): void
//    {
//        global $config;
//
//        if (!empty($input_type)) {
//            $input_type = self::determine_ext($input_type);
//        }
//
//        try {
//            $image = new Imagick($input_type . ":" . $input_path);
//            try {
//                $result = $image->flattenImages();
//                if ($result !== true) {
//                    throw new GraphicsException("Could not flatten image $input_path");
//                }
//
//                $height = $image->getImageHeight();
//                $width = $image->getImageWidth();
//                if (!$allow_upscale &&
//                    ($new_width > $width || $new_height > $height)) {
//                    $new_height = $height;
//                    $new_width = $width;
//                }
//
//                $result = $image->resizeImage($new_width, $new_width, Imagick::FILTER_LANCZOS, 0, !$ignore_aspect_ratio);
//                if ($result !== true) {
//                    throw new GraphicsException("Could not perform image resize on $input_path");
//                }
//
//
//                if (empty($output_type)) {
//                    $output_type = $input_type;
//                }
//
//                self::image_save_imagick($image, $output_filename, $output_type, $output_quality);
//
//            } finally {
//                $image->destroy();
//            }
//        } catch (ImagickException $e) {
//            throw new GraphicsException("Error while resizing with Imagick: " . $e->getMessage(), $e->getCode(), $e);
//        }
//    }

    public static function is_lossless(string $filename, string $format)
    {
        if (in_array($format, self::LOSSLESS_FORMATS)) {
            return true;
        }
        switch ($format) {
            case EXTENSION_WEBP:
                return self::is_lossless_webp($filename);
                break;
        }
        return false;
    }

    public static function image_resize_convert(
        string $input_path,
        string $input_type,
        int $new_width,
        int $new_height,
        string $output_filename,
        string $output_type = null,
        bool $ignore_aspect_ratio = false,
        int $output_quality = 80,
        bool $minimize = false,
        bool $allow_upscale = true
    ): void {
        global $config;

        $convert = $config->get_string(MediaConfig::CONVERT_PATH);

        if (empty($convert)) {
            throw new MediaException("convert command not configured");
        }

        if (empty($output_type)) {
            $output_type = $input_type;
        }

        if ($output_type==EXTENSION_WEBP && self::is_lossless($input_path, $input_type)) {
            $output_type = self::WEBP_LOSSLESS;
        }

        $bg = "black";
        if (self::supports_alpha($output_type)) {
            $bg = "none";
        }
        if (!empty($input_type)) {
            $input_type = $input_type . ":";
        }


        $resize_args = "";
        if (!$allow_upscale) {
            $resize_args .= "\>";
        }
        if ($ignore_aspect_ratio) {
            $resize_args .= "\!";
        }

        $args = "";
        switch ($output_type) {
            case Media::WEBP_LOSSLESS:
                $args .= '-define webp:lossless=true';
                break;
            case EXTENSION_PNG:
                $args .= '-define png:compression-level=9';
                break;
        }

        if ($minimize) {
            $args .= " -strip -thumbnail";
        } else {
            $args .= " -resize";
        }


        $output_ext = self::determine_ext($output_type);

        $format = '"%s"  %s %ux%u%s -quality %u -background %s %s"%s[0]"  %s:"%s" 2>&1';
        $cmd = sprintf($format, $convert, $args, $new_width, $new_height, $resize_args, $output_quality, $bg, $input_type, $input_path, $output_ext, $output_filename);
        $cmd = str_replace("\"convert\"", "convert", $cmd); // quotes are only needed if the path to convert contains a space; some other times, quotes break things, see github bug #27
        exec($cmd, $output, $ret);
        if ($ret != 0) {
            throw new MediaException("Resizing image with command `$cmd`, returns $ret, outputting " . implode("\r\n", $output));
        } else {
            log_debug('media', "Generating thumbnail with command `$cmd`, returns $ret");
        }
    }

    /**
     * Performs a resize operation on an image file using GD.
     *
     * @param String $image_filename The source file to be resized.
     * @param array $info The output of getimagesize() for the source file.
     * @param int $new_width
     * @param int $new_height
     * @param string $output_filename
     * @param string|null $output_type If set to null, the output file type will be automatically determined via the $info parameter. Otherwise an exception will be thrown.
     * @param int $output_quality Defaults to 80.
     * @throws MediaException
     * @throws InsufficientMemoryException if the estimated memory usage exceeds the memory limit.
     */
    public static function image_resize_gd(
        string $image_filename,
        array $info,
        int $new_width,
        int $new_height,
        string $output_filename,
        string $output_type = null,
        bool $ignore_aspect_ratio = false,
        int $output_quality = 80,
        bool $allow_upscale = true
    ) {
        $width = $info[0];
        $height = $info[1];

        if ($output_type == null) {
            /* If not specified, output to the same format as the original image */
            switch ($info[2]) {
                case IMAGETYPE_GIF:
                    $output_type = EXTENSION_GIF;
                    break;
                case IMAGETYPE_JPEG:
                    $output_type = EXTENSION_JPEG;
                    break;
                case IMAGETYPE_PNG:
                    $output_type = EXTENSION_PNG;
                    break;
                case IMAGETYPE_WEBP:
                    $output_type = EXTENSION_WEBP;
                    break;
                case IMAGETYPE_BMP:
                    $output_type = EXTENSION_BMP;
                    break;
                default:
                    throw new MediaException("Failed to save the new image - Unsupported image type.");
            }
        }

        $memory_use = self::calc_memory_use($info);
        $memory_limit = get_memory_limit();
        if ($memory_use > $memory_limit) {
            throw new InsufficientMemoryException("The image is too large to resize given the memory limits. ($memory_use > $memory_limit)");
        }

        if (!$ignore_aspect_ratio) {
            list($new_width, $new_height) = get_scaled_by_aspect_ratio($width, $height, $new_width, $new_height);
        }
        if (!$allow_upscale &&
            ($new_width > $width || $new_height > $height)) {
            $new_height = $height;
            $new_width = $width;
        }

        $image = imagecreatefromstring(file_get_contents($image_filename));
        $image_resized = imagecreatetruecolor($new_width, $new_height);
        try {
            if ($image === false) {
                throw new MediaException("Could not load image: " . $image_filename);
            }
            if ($image_resized === false) {
                throw new MediaException("Could not create output image with dimensions $new_width c $new_height ");
            }

            // Handle transparent images
            switch ($info[2]) {
                case IMAGETYPE_GIF:
                    $transparency = imagecolortransparent($image);
                    $pallet_size = imagecolorstotal($image);

                    // If we have a specific transparent color
                    if ($transparency >= 0 && $transparency < $pallet_size) {
                        // Get the original image's transparent color's RGB values
                        $transparent_color = imagecolorsforindex($image, $transparency);

                        // Allocate the same color in the new image resource
                        $transparency = imagecolorallocate($image_resized, $transparent_color['red'], $transparent_color['green'], $transparent_color['blue']);
                        if ($transparency === false) {
                            throw new MediaException("Unable to allocate transparent color");
                        }

                        // Completely fill the background of the new image with allocated color.
                        if (imagefill($image_resized, 0, 0, $transparency) === false) {
                            throw new MediaException("Unable to fill new image with transparent color");
                        }

                        // Set the background color for new image to transparent
                        imagecolortransparent($image_resized, $transparency);
                    }
                    break;
                case IMAGETYPE_PNG:
                case IMAGETYPE_WEBP:
                    //
                    // More info here:  https://stackoverflow.com/questions/279236/how-do-i-resize-pngs-with-transparency-in-php
                    //
                    if (imagealphablending($image_resized, false) === false) {
                        throw new MediaException("Unable to disable image alpha blending");
                    }
                    if (imagesavealpha($image_resized, true) === false) {
                        throw new MediaException("Unable to enable image save alpha");
                    }
                    $transparent_color = imagecolorallocatealpha($image_resized, 255, 255, 255, 127);
                    if ($transparent_color === false) {
                        throw new MediaException("Unable to allocate transparent color");
                    }
                    if (imagefilledrectangle($image_resized, 0, 0, $new_width, $new_height, $transparent_color) === false) {
                        throw new MediaException("Unable to fill new image with transparent color");
                    }
                    break;
            }

            // Actually resize the image.
            if (imagecopyresampled(
                $image_resized,
                $image,
                0,
                0,
                0,
                0,
                $new_width,
                $new_height,
                $width,
                $height
            ) === false) {
                throw new MediaException("Unable to copy resized image data to new image");
            }

            switch ($output_type) {
                case EXTENSION_BMP:
                    $result = imagebmp($image_resized, $output_filename, true);
                    break;
                case EXTENSION_WEBP:
                case Media::WEBP_LOSSY:
                    $result = imagewebp($image_resized, $output_filename, $output_quality);
                    break;
                case EXTENSION_JPG:
                case EXTENSION_JPEG:
                    $result = imagejpeg($image_resized, $output_filename, $output_quality);
                    break;
                case EXTENSION_PNG:
                    $result = imagepng($image_resized, $output_filename, 9);
                    break;
                case EXTENSION_GIF:
                    $result = imagegif($image_resized, $output_filename);
                    break;
                default:
                    throw new MediaException("Failed to save the new image - Unsupported image type: $output_type");
            }
            if ($result === false) {
                throw new MediaException("Failed to save the new image, function returned false when saving type: $output_type");
            }
        } finally {
            @imagedestroy($image);
            @imagedestroy($image_resized);
        }
    }

    /**
     * Determines if a file is an animated gif.
     *
     * @param String $image_filename The path of the file to check.
     * @return bool true if the file is an animated gif, false if it is not.
     */
    public static function is_animated_gif(string $image_filename): bool
    {
        $is_anim_gif = 0;
        if (($fh = @fopen($image_filename, 'rb'))) {
            try {
                //check if gif is animated (via https://www.php.net/manual/en/function.imagecreatefromgif.php#104473)
                while (!feof($fh) && $is_anim_gif < 2) {
                    $chunk = fread($fh, 1024 * 100);
                    $is_anim_gif += preg_match_all('#\x00\x21\xF9\x04.{4}\x00[\x2C\x21]#s', $chunk, $matches);
                }
            } finally {
                @fclose($fh);
            }
        }
        return ($is_anim_gif == 0);
    }


    private static function compare_file_bytes(string $file_name, array $comparison): bool
    {
        $size = filesize($file_name);
        if ($size < count($comparison)) {
            // Can't match because it's too small
            return false;
        }

        if (($fh = @fopen($file_name, 'rb'))) {
            try {
                $chunk = unpack("C*", fread($fh, count($comparison)));

                for ($i = 0; $i < count($comparison); $i++) {
                    $byte = $comparison[$i];
                    if ($byte == null) {
                        continue;
                    } else {
                        $fileByte = $chunk[$i + 1];
                        if ($fileByte != $byte) {
                            return false;
                        }
                    }
                }
                return true;
            } finally {
                @fclose($fh);
            }
        } else {
            throw new MediaException("Unable to open file for byte check: $file_name");
        }
    }

    public static function is_animated_webp(string $image_filename): bool
    {
        return self::compare_file_bytes($image_filename, self::WEBP_ANIMATION_HEADER);
    }

    public static function is_lossless_webp(string $image_filename): bool
    {
        return self::compare_file_bytes($image_filename, self::WEBP_LOSSLESS_HEADER);
    }

    public static function supports_alpha(string $format): bool
    {
        return in_array(self::normalize_format($format), self::ALPHA_FORMATS);
    }

    public static function is_input_supported(string $engine, string $format, ?bool $lossless = null): bool
    {
        $format = self::normalize_format($format, $lossless);
        if (!in_array($format, MediaEngine::INPUT_SUPPORT[$engine])) {
            return false;
        }
        return true;
    }

    public static function is_output_supported(string $engine, string $format, ?bool $lossless = false): bool
    {
        $format = self::normalize_format($format, $lossless);
        if (!in_array($format, MediaEngine::OUTPUT_SUPPORT[$engine])) {
            return false;
        }
        return true;
    }

    /**
     * Checks if a format (normally a file extension) is a variant name of another format (ie, jpg and jpeg).
     * If one is found, then the maine name that the Media extension will recognize is returned,
     * otherwise the incoming format is returned.
     *
     * @param $format
     * @return string|null The format name that the media extension will recognize.
     */
    public static function normalize_format(string $format, ?bool $lossless = null): ?string
    {
        if ($format == EXTENSION_WEBP) {
            if ($lossless === true) {
                $format = Media::WEBP_LOSSLESS;
            } else {
                $format = Media::WEBP_LOSSY;
            }
        }

        if (array_key_exists($format, Media::FORMAT_ALIASES)) {
            return self::FORMAT_ALIASES[$format];
        }
        return $format;
    }


    /**
     * Determines the dimensions of a video file using ffmpeg.
     *
     * @param string $filename
     * @return array [width, height]
     */
    public static function video_size(string $filename): array
    {
        global $config;
        $ffmpeg = $config->get_string(MediaConfig::FFMPEG_PATH);
        $cmd = escapeshellcmd(implode(" ", [
            escapeshellarg($ffmpeg),
            "-y", "-i", escapeshellarg($filename),
            "-vstats"
        ]));
        $output = shell_exec($cmd . " 2>&1");
        // error_log("Getting size with `$cmd`");

        $regex_sizes = "/Video: .* ([0-9]{1,4})x([0-9]{1,4})/";
        if (preg_match($regex_sizes, $output, $regs)) {
            if (preg_match("/displaymatrix: rotation of (90|270).00 degrees/", $output)) {
                $size = [(int)$regs[2], (int)$regs[1]];
            } else {
                $size = [(int)$regs[1], (int)$regs[2]];
            }
        } else {
            $size = [1, 1];
        }
        log_debug('media', "Getting video size with `$cmd`, returns $output -- $size[0], $size[1]");
        return $size;
    }

    public function onDatabaseUpgrade(DatabaseUpgradeEvent $event)
    {
        global $config, $database;
        if ($this->get_version(MediaConfig::VERSION) < 1) {
            $current_value = $config->get_string("thumb_ffmpeg_path");
            if (!empty($current_value)) {
                $config->set_string(MediaConfig::FFMPEG_PATH, $current_value);
            } elseif ($ffmpeg = shell_exec((PHP_OS == 'WINNT' ? 'where' : 'which') . ' ffmpeg')) {
                //ffmpeg exists in PATH, check if it's executable, and if so, default to it instead of static
                if (is_executable(strtok($ffmpeg, PHP_EOL))) {
                    $config->set_default_string(MediaConfig::FFMPEG_PATH, 'ffmpeg');
                }
            }

            if ($ffprobe = shell_exec((PHP_OS == 'WINNT' ? 'where' : 'which') . ' ffprobe')) {
                //ffprobe exists in PATH, check if it's executable, and if so, default to it instead of static
                if (is_executable(strtok($ffprobe, PHP_EOL))) {
                    $config->set_default_string(MediaConfig::FFPROBE_PATH, 'ffprobe');
                }
            }

            $current_value = $config->get_string("thumb_convert_path");
            if (!empty($current_value)) {
                $config->set_string(MediaConfig::CONVERT_PATH, $current_value);
            } elseif ($convert = shell_exec((PHP_OS == 'WINNT' ? 'where' : 'which') . ' convert')) {
                //ffmpeg exists in PATH, check if it's executable, and if so, default to it instead of static
                if (is_executable(strtok($convert, PHP_EOL))) {
                    $config->set_default_string(MediaConfig::CONVERT_PATH, 'convert');
                }
            }

            $current_value = $config->get_int("thumb_mem_limit");
            if (!empty($current_value)) {
                $config->set_int(MediaConfig::MEM_LIMIT, $current_value);
            }

            $this->set_version(MediaConfig::VERSION, 1);
        }

        if ($this->get_version(MediaConfig::VERSION) < 2) {
            $database->execute($database->scoreql_to_sql(
                "ALTER TABLE images ADD COLUMN image SCORE_BOOL NULL"
            ));

            switch ($database->get_driver_name()) {
                case DatabaseDriver::PGSQL:
                case DatabaseDriver::SQLITE:
                    $database->execute('CREATE INDEX images_image_idx ON images(image) WHERE image IS NOT NULL');
                    break;
                default:
                    $database->execute('CREATE INDEX images_image_idx ON images(image)');
                    break;
            }

            $database->set_timeout(300000); // These updates can take a little bit

            if ($database->transaction === true) {
                $database->commit(); // Each of these commands could hit a lot of data, combining them into one big transaction would not be a good idea.
            }
            log_info("upgrade", "Setting predictable media values for known file types");
            $database->execute($database->scoreql_to_sql("UPDATE images SET image = SCORE_BOOL_N WHERE ext IN ('swf','mp3','ani','flv','mp4','m4v','ogv','webm')"));
            $database->execute($database->scoreql_to_sql("UPDATE images SET image = SCORE_BOOL_Y WHERE ext IN ('jpg','jpeg','ico','cur','png')"));

            $this->set_version(MediaConfig::VERSION, 2);

            $database->beginTransaction();
        }
    }
}