<?php

declare(strict_types=1);

namespace Intervention\Image;

use Error;
use Intervention\Gif\DisposalMethod;
use Intervention\Image\Exceptions\DecoderException;
use Intervention\Image\Exceptions\FilesystemException;
use Intervention\Image\Interfaces\AnimationFactoryInterface;
use Intervention\Image\Interfaces\DriverInterface;
use Intervention\Image\Interfaces\FrameInterface;
use Intervention\Image\Interfaces\ImageInterface;

class AnimationFactory implements AnimationFactoryInterface
{
    /**
     * Current frame number.
     */
    protected int $currentFrameNumber = 0;

    /**
     * Image sources of animation frames.
     *
     * @var array<mixed>
     */
    protected array $sources = [];

    /**
     * Frame delays of animation frames in seconds.
     *
     * @var array<float>
     */
    protected array $delays = [];

    /**
     * Frame processing call names.
     *
     * @var array<null|string>
     */
    protected array $processingCalls = [];

    /**
     * Frame processing arguments of calls.
     *
     * @var array<null|array<mixed>>
     */
    protected array $processingArguments = [];

    /**
     * Create new instance.
     */
    public function __construct(
        protected int $width,
        protected int $height,
        null|callable $animation = null,
    ) {
        if (is_callable($animation)) {
            $animation($this);
        }
    }

    /**
     * {@inheritdoc}
     *
     * @see AnimationFactoryInterface::build()
     */
    public static function build(
        int $width,
        int $height,
        callable $animation,
        DriverInterface $driver,
    ): ImageInterface {
        return (new self($width, $height, $animation))->image($driver);
    }

    /**
     * {@inheritdoc}
     *
     * @see AnimationFactoryInterface::add()
     */
    public function add(mixed $source, float $delay = 1): AnimationFactoryInterface
    {
        $this->currentFrameNumber++;

        $this->sources[$this->currentFrameNumber] = $source;
        $this->delays[$this->currentFrameNumber] = $delay;
        $this->processingCalls[$this->currentFrameNumber] = null;
        $this->processingArguments[$this->currentFrameNumber] = null;

        return $this;
    }

    /**
     * {@inheritdoc}
     *
     * @see AnimationFactoryInterface::image()
     */
    public function image(DriverInterface $driver): ImageInterface
    {
        if (count($this->sources) === 0) {
            return $driver->createImage($this->width, $this->height);
        }

        $frames = array_map(
            $this->buildFrame(...),
            array_fill(0, count($this->sources), $driver),
            $this->sources,
            $this->delays,
            $this->processingCalls,
            $this->processingArguments,
        );

        return new Image($driver, $driver->createCore($frames));
    }

    /**
     * Build frame from given image source and delay.
     *
     * @param null|array<mixed> $processingArguments
     */
    private function buildFrame(
        DriverInterface $driver,
        mixed $source,
        float $delay,
        ?string $processingCall = null,
        ?array $processingArguments = null,
    ): FrameInterface {
        try {
            // try to decode image source
            $image = $driver->decodeImage($source);
        } catch (DecoderException | FilesystemException) {
            // create empty image with colored background
            $image = $driver->createImage($this->width, $this->height)
                ->fill($driver->decodeColor($source));
        }

        // adjust size if necessary
        if ($image->width() !== $this->width || $image->height() !== $this->height) {
            $image->cover($this->width, $this->height);
        }

        // apply processing call if available
        if ($processingCall !== null) {
            call_user_func_array([$image, $processingCall], $processingArguments);
        }

        // return ready-made frame with all attributes
        return $image
            ->core()
            ->first()
            ->setDelay($delay)
            ->setDisposalMethod(DisposalMethod::BACKGROUND->value);
    }

    /**
     * Collect processing calls on frame images.
     *
     * @param array<null|array<mixed>> $arguments
     * @throws Error
     */
    public function __call(string $name, array $arguments): self
    {
        if (!method_exists(Image::class, $name)) {
            throw new Error('Call to undefined method ' . Image::class . '::' . $name . '()');
        }

        $this->processingCalls[$this->currentFrameNumber] = $name;
        $this->processingArguments[$this->currentFrameNumber] = $arguments;

        return $this;
    }
}
