<?php

declare(strict_types=1);

namespace Intervention\Image\Drivers\Gd\Modifiers;

use RuntimeException;
use Intervention\Image\Exceptions\GeometryException;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Interfaces\SpecializedInterface;
use Intervention\Image\Modifiers\DrawBezierModifier as GenericDrawBezierModifier;

class DrawBezierModifier extends GenericDrawBezierModifier implements SpecializedInterface
{
    /**
     * {@inheritdoc}
     *
     * @see ModifierInterface::apply()
     *
     * @throws RuntimeException
     * @throws GeometryException
     */
    public function apply(ImageInterface $image): ImageInterface
    {
        foreach ($image as $frame) {
            if ($this->drawable->count() !== 3 && $this->drawable->count() !== 4) {
                throw new GeometryException('You must specify either 3 or 4 points to create a bezier curve');
            }

            [$polygon, $polygon_border_segments] = $this->calculateBezierPoints();

            if ($this->drawable->hasBackgroundColor() || $this->drawable->hasBorder()) {
                imagealphablending($frame->native(), true);
                imageantialias($frame->native(), true);
            }

            if ($this->drawable->hasBackgroundColor()) {
                $background_color = $this->driver()->colorProcessor($image->colorspace())->colorToNative(
                    $this->backgroundColor()
                );

                imagesetthickness($frame->native(), 0);
                imagefilledpolygon(
                    $frame->native(),
                    $polygon,
                    $background_color
                );
            }

            if ($this->drawable->hasBorder() && $this->drawable->borderSize() > 0) {
                $border_color = $this->driver()->colorProcessor($image->colorspace())->colorToNative(
                    $this->borderColor()
                );

                if ($this->drawable->borderSize() === 1) {
                    imagesetthickness($frame->native(), $this->drawable->borderSize());

                    $count = count($polygon);
                    for ($i = 0; $i < $count; $i += 2) {
                        if (array_key_exists($i + 2, $polygon) && array_key_exists($i + 3, $polygon)) {
                            imageline(
                                $frame->native(),
                                $polygon[$i],
                                $polygon[$i + 1],
                                $polygon[$i + 2],
                                $polygon[$i + 3],
                                $border_color
                            );
                        }
                    }
                } else {
                    $polygon_border_segments_total = count($polygon_border_segments);

                    for ($i = 0; $i < $polygon_border_segments_total; $i += 1) {
                        imagefilledpolygon(
                            $frame->native(),
                            $polygon_border_segments[$i],
                            $border_color
                        );
                    }
                }
            }
        }

        return $image;
    }

    /**
     * Calculate interpolation points for quadratic beziers using the Bernstein polynomial form
     *
     * @return array{'x': float, 'y': float}
     */
    private function calculateQuadraticBezierInterpolationPoint(float $t = 0.05): array
    {
        $remainder = 1 - $t;
        $control_point_1_multiplier = $remainder * $remainder;
        $control_point_2_multiplier = $remainder * $t * 2;
        $control_point_3_multiplier = $t * $t;

        $x = (
            $this->drawable->first()->x() * $control_point_1_multiplier +
            $this->drawable->second()->x() * $control_point_2_multiplier +
            $this->drawable->last()->x() * $control_point_3_multiplier
        );
        $y = (
            $this->drawable->first()->y() * $control_point_1_multiplier +
            $this->drawable->second()->y() * $control_point_2_multiplier +
            $this->drawable->last()->y() * $control_point_3_multiplier
        );

        return ['x' => $x, 'y' => $y];
    }

    /**
     * Calculate interpolation points for cubic beziers using the Bernstein polynomial form
     *
     * @return array{'x': float, 'y': float}
     */
    private function calculateCubicBezierInterpolationPoint(float $t = 0.05): array
    {
        $remainder = 1 - $t;
        $t_squared = $t * $t;
        $remainder_squared = $remainder * $remainder;
        $control_point_1_multiplier = $remainder_squared * $remainder;
        $control_point_2_multiplier = $remainder_squared * $t * 3;
        $control_point_3_multiplier = $t_squared * $remainder * 3;
        $control_point_4_multiplier = $t_squared * $t;

        $x = (
            $this->drawable->first()->x() * $control_point_1_multiplier +
            $this->drawable->second()->x() * $control_point_2_multiplier +
            $this->drawable->third()->x() * $control_point_3_multiplier +
            $this->drawable->last()->x() * $control_point_4_multiplier
        );
        $y = (
            $this->drawable->first()->y() * $control_point_1_multiplier +
            $this->drawable->second()->y() * $control_point_2_multiplier +
            $this->drawable->third()->y() * $control_point_3_multiplier +
            $this->drawable->last()->y() * $control_point_4_multiplier
        );

        return ['x' => $x, 'y' => $y];
    }

    /**
     * Calculate the points needed to draw a quadratic or cubic bezier with optional border/stroke
     *
     * @throws GeometryException
     * @return array{0: array<mixed>, 1: array<mixed>}
     */
    private function calculateBezierPoints(): array
    {
        if ($this->drawable->count() !== 3 && $this->drawable->count() !== 4) {
            throw new GeometryException('You must specify either 3 or 4 points to create a bezier curve');
        }

        $polygon = [];
        $inner_polygon = [];
        $outer_polygon = [];
        $polygon_border_segments = [];

        // define ratio t; equivalent to 5 percent distance along edge
        $t = 0.05;

        $polygon[] = $this->drawable->first()->x();
        $polygon[] = $this->drawable->first()->y();
        for ($i = $t; $i < 1; $i += $t) {
            if ($this->drawable->count() === 3) {
                $ip = $this->calculateQuadraticBezierInterpolationPoint($i);
            } elseif ($this->drawable->count() === 4) {
                $ip = $this->calculateCubicBezierInterpolationPoint($i);
            }
            $polygon[] = (int) $ip['x'];
            $polygon[] = (int) $ip['y'];
        }
        $polygon[] = $this->drawable->last()->x();
        $polygon[] = $this->drawable->last()->y();

        if ($this->drawable->hasBorder() && $this->drawable->borderSize() > 1) {
            // create the border/stroke effect by calculating two new curves with offset positions
            // from the main polygon and then connecting the inner/outer curves to create separate
            // 4-point polygon segments
            $polygon_total_points = count($polygon);
            $offset = ($this->drawable->borderSize() / 2);

            for ($i = 0; $i < $polygon_total_points; $i += 2) {
                if (array_key_exists($i + 2, $polygon) && array_key_exists($i + 3, $polygon)) {
                    $dx = $polygon[$i + 2] - $polygon[$i];
                    $dy = $polygon[$i + 3] - $polygon[$i + 1];
                    $dxy_sqrt = ($dx * $dx + $dy * $dy) ** 0.5;

                    // inner polygon
                    $scale = $offset / $dxy_sqrt;
                    $ox = -$dy * $scale;
                    $oy = $dx * $scale;

                    $inner_polygon[] = $ox + $polygon[$i];
                    $inner_polygon[] = $oy + $polygon[$i + 1];
                    $inner_polygon[] = $ox + $polygon[$i + 2];
                    $inner_polygon[] = $oy + $polygon[$i + 3];

                    // outer polygon
                    $scale = -$offset / $dxy_sqrt;
                    $ox = -$dy * $scale;
                    $oy = $dx * $scale;

                    $outer_polygon[] = $ox + $polygon[$i];
                    $outer_polygon[] = $oy + $polygon[$i + 1];
                    $outer_polygon[] = $ox + $polygon[$i + 2];
                    $outer_polygon[] = $oy + $polygon[$i + 3];
                }
            }

            $inner_polygon_total_points = count($inner_polygon);

            for ($i = 0; $i < $inner_polygon_total_points; $i += 2) {
                if (array_key_exists($i + 2, $inner_polygon) && array_key_exists($i + 3, $inner_polygon)) {
                    $polygon_border_segments[] = [
                        $inner_polygon[$i],
                        $inner_polygon[$i + 1],
                        $outer_polygon[$i],
                        $outer_polygon[$i + 1],
                        $outer_polygon[$i + 2],
                        $outer_polygon[$i + 3],
                        $inner_polygon[$i + 2],
                        $inner_polygon[$i + 3],
                    ];
                }
            }
        }

        return [$polygon, $polygon_border_segments];
    }
}
