<?php

namespace YayMail\Countdown;

/*
    :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
    :: Formerly known as:::
    :: GIFEncoder Version 2.0 by László Zsidi, http://gifs.hu
    ::
    :: This class is a rewritten 'GifMerge.class.php' version.
    ::
    :: Modification:
    :: - Simplified and easy code,
    :: - Ultra fast encoding,
    :: - Built-in errors,
    :: - Stable working
    ::
    ::
    :: Updated at 2007. 02. 13. '00.05.AM'
    ::
    ::
    ::
    :: Try on-line GIFBuilder Form demo based on GIFEncoder.
    ::
    :: http://gifs.hu/phpclasses/demos/GifBuilder/
    ::
    :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
 */

/**
 * Encode animated gifs
 */
class AnimatedGif {

    /**
     * The built gif image
     *
     * @var resource
     */
    private $image = '';

    /**
     * The array of images to stack
     *
     * @var array
     */
    private $buffer = [];

    /**
     * How many times to loop? 0 = infinite
     *
     * @var int
     */
    private $number_of_loops = 0;

    /**
     *
     * @var int
     */
    private $dis = 2;

    /**
     * Which colour is transparent
     *
     * @var int
     */
    private $transparent_color = -1;

    /**
     * Is this the first frame
     *
     * @var int
     */
    private $first_frame = true;

    /**
     * Encode an animated gif.
     *
     * @param array $source_images An array of binary source images.
     * @param array $image_delays The delays associated with the source images.
     * @param int   $number_of_loops The number of times to loop; 0 means infinite.
     * @param int   $transparent_color_red Transparent color red channel (0-255) or -1 for none.
     * @param int   $transparent_color_green Transparent color green channel (0-255) or -1 for none.
     * @param int   $transparent_color_blue Transparent color blue channel (0-255) or -1 for none.
     */
    public function __construct( array $source_images, array $image_delays, $number_of_loops, $transparent_color_red = -1, $transparent_color_green = -1, $transparent_color_blue = -1 ) {
        /**
         * I have no idea what these even do, they appear to do nothing to the image so far
         */
        $transparent_color_red   = 0;
        $transparent_color_green = 0;
        $transparent_color_blue  = 0;

        $this->number_of_loops = ( $number_of_loops > -1 ) ? $number_of_loops : 0;
        $this->set_transparent_color( $transparent_color_red, $transparent_color_green, $transparent_color_blue );
        $this->buffer_images( $source_images );

        $this->add_header();
        $count = count( $this->buffer );

        for ( $i = 0; $i < $count; $i++ ) {
            $this->add_frame( $i, $image_delays [ $i ] );
        }
    }

    /**
     * Set the transparent color
     *
     * @param int $red
     * @param int $green
     * @param int $blue
     */
    private function set_transparent_color( $red, $green, $blue ) {
        $this->transparent_color = ( $red > -1 && $green > -1 && $blue > -1 ) ? ( $red | ( $green << 8 ) | ( $blue << 16 ) ) : -1;
    }

    /**
     * Buffer the images and check to make sure they are valid
     *
     * @param array $source_images The array of source images.
     *
     * @throws \Exception When an image is not a valid GIF or is an animated GIF.
     */
    private function buffer_images( $source_images ) {
        $count = count( $source_images );
        for ( $i = 0; $i < $count; $i++ ) {
            $this->buffer [] = $source_images [ $i ];
            if ( 'GIF87a' !== substr( $this->buffer [ $i ], 0, 6 ) && 'GIF89a' !== substr( $this->buffer [ $i ], 0, 6 ) ) {
                throw new \Exception( 'Image at position ' . $i . ' is not a gif' );
            }
            for ( $j = ( 13 + 3 * ( 2 << ( ord( $this->buffer [ $i ] [10] ) & 0x07 ) ) ), $k = true; $k; $j++ ) {
                switch ( $this->buffer [ $i ] [ $j ] ) {
                    case '!':
                        if ( 'NETSCAPE' === substr( $this->buffer [ $i ], ( $j + 3 ), 8 ) ) {
                            throw new \Exception( 'You cannot make an animation from an animated gif.' );
                        }
                        break;
                    case ';':
                        $k = false;
                        break;
                }
            }
        }
    }

    /**
     * Add the gif header to the image
     */
    private function add_header() {
        $cmap        = 0;
        $this->image = 'GIF89a';
        if ( ord( $this->buffer [0] [10] ) & 0x80 ) {
            $cmap         = 3 * ( 2 << ( ord( $this->buffer [0] [10] ) & 0x07 ) );
            $this->image .= substr( $this->buffer [0], 6, 7 );
            $this->image .= substr( $this->buffer [0], 13, $cmap );
            $this->image .= "!\377\13NETSCAPE2.0\3\1" . $this->word( $this->number_of_loops ) . "\0";
        }
    }

    /**
     * Add a frame to the animation.
     *
     * @param int $frame The frame to be added.
     * @param int $delay The delay associated with the frame.
     */
    private function add_frame( $frame, $delay ) {
        $locals_str = 13 + 3 * ( 2 << ( ord( $this->buffer [ $frame ] [10] ) & 0x07 ) );

        $locals_end = strlen( $this->buffer [ $frame ] ) - $locals_str - 1;
        $locals_tmp = substr( $this->buffer [ $frame ], $locals_str, $locals_end );

        $global_len = 2 << ( ord( $this->buffer [0] [10] ) & 0x07 );
        $locals_len = 2 << ( ord( $this->buffer [ $frame ] [10] ) & 0x07 );

        $global_rgb = substr( $this->buffer [0], 13, 3 * ( 2 << ( ord( $this->buffer [0] [10] ) & 0x07 ) ) );
        $locals_rgb = substr( $this->buffer [ $frame ], 13, 3 * ( 2 << ( ord( $this->buffer [ $frame ] [10] ) & 0x07 ) ) );

        $locals_ext = "!\xF9\x04" . chr( ( $this->dis << 2 ) + 0 ) .
                chr( ( $delay >> 0 ) & 0xFF ) . chr( ( $delay >> 8 ) & 0xFF ) . "\x0\x0";

        if ( $this->transparent_color > -1 && ord( $this->buffer [ $frame ] [10] ) & 0x80 ) {
            for ( $j = 0; $j < $locals_len; $j++ ) {
                if (
                        ord( $locals_rgb [ 3 * $j + 0 ] ) === ( ( $this->transparent_color >> 16 ) & 0xFF ) &&
                        ord( $locals_rgb [ 3 * $j + 1 ] ) === ( ( $this->transparent_color >> 8 ) & 0xFF ) &&
                        ord( $locals_rgb [ 3 * $j + 2 ] ) === ( ( $this->transparent_color >> 0 ) & 0xFF )
                ) {
                    $locals_ext = "!\xF9\x04" . chr( ( $this->dis << 2 ) + 1 ) .
                            chr( ( $delay >> 0 ) & 0xFF ) . chr( ( $delay >> 8 ) & 0xFF ) . chr( $j ) . "\x0";
                    break;
                }
            }
        }
        switch ( $locals_tmp [0] ) {
            case '!':
                $locals_img = substr( $locals_tmp, 8, 10 );
                $locals_tmp = substr( $locals_tmp, 18, strlen( $locals_tmp ) - 18 );
                break;
            case ',':
                $locals_img = substr( $locals_tmp, 0, 10 );
                $locals_tmp = substr( $locals_tmp, 10, strlen( $locals_tmp ) - 10 );
                break;
        }
        if ( ord( $this->buffer [ $frame ] [10] ) & 0x80 && $this->first_frame === false ) {
            if ( $global_len === $locals_len ) {
                if ( $this->block_compare( $global_rgb, $locals_rgb, $global_len ) ) {
                    $this->image .= ( $locals_ext . $locals_img . $locals_tmp );
                } else {
                    $byte           = ord( $locals_img [9] );
                    $byte          |= 0x80;
                    $byte          &= 0xF8;
                    $byte          |= ( ord( $this->buffer [0] [10] ) & 0x07 );
                    $locals_img [9] = chr( $byte );
                    $this->image   .= ( $locals_ext . $locals_img . $locals_rgb . $locals_tmp );
                }
            } else {
                $byte           = ord( $locals_img [9] );
                $byte          |= 0x80;
                $byte          &= 0xF8;
                $byte          |= ( ord( $this->buffer [ $frame ] [10] ) & 0x07 );
                $locals_img [9] = chr( $byte );
                $this->image   .= ( $locals_ext . $locals_img . $locals_rgb . $locals_tmp );
            }
        } else {
            $this->image .= ( $locals_ext . $locals_img . $locals_tmp );
        }//end if
        $this->first_frame = false;
    }

    /**
     * Add the gif footer
     */
    private function add_footer() {
        $this->image .= ';';
    }

    /**
     * Compare gif blocks? What is a block?
     *
     * @param string $global_block
     * @param string $local_block
     * @param int    $length
     * @return int
     */
    private function block_compare( $global_block, $local_block, $length ) {
        for ( $i = 0; $i < $length; $i++ ) {
            if (
                    $global_block [ 3 * $i + 0 ] !== $local_block [ 3 * $i + 0 ] ||
                    $global_block [ 3 * $i + 1 ] !== $local_block [ 3 * $i + 1 ] ||
                    $global_block [ 3 * $i + 2 ] !== $local_block [ 3 * $i + 2 ]
            ) {
                return ( 0 );
            }
        }

        return ( 1 );
    }

    /**
     * No clue
     *
     * @param int $number
     * @return string the char you meant?
     */
    private function word( $number ) {
        return ( chr( $number & 0xFF ) . chr( ( $number >> 8 ) & 0xFF ) );
    }

    /**
     * Return the animated gif
     *
     * @return string
     */
    public function get_animation() {
        return $this->image;
    }

    /**
     * Return the animated gif
     *
     * @return void
     */
    public function display() {
        // late footer add
        $this->add_footer();
        header( 'Content-type:image/gif' );
        // Output raw GIF bytes; do not sanitize binary data
        // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
        echo $this->image;
    }
}
