<?php
declare(strict_types = 1);

namespace BaconQrCode\Renderer\Image;

use BaconQrCode\Exception\RuntimeException;
use BaconQrCode\Renderer\Color\Alpha;
use BaconQrCode\Renderer\Color\Cmyk;
use BaconQrCode\Renderer\Color\ColorInterface;
use BaconQrCode\Renderer\Color\Gray;
use BaconQrCode\Renderer\Color\Rgb;
use BaconQrCode\Renderer\Path\Close;
use BaconQrCode\Renderer\Path\Curve;
use BaconQrCode\Renderer\Path\EllipticArc;
use BaconQrCode\Renderer\Path\Line;
use BaconQrCode\Renderer\Path\Move;
use BaconQrCode\Renderer\Path\Path;
use BaconQrCode\Renderer\RendererStyle\Gradient;
use BaconQrCode\Renderer\RendererStyle\GradientType;

final class EpsImageBackEnd implements ImageBackEndInterface
{
    private const PRECISION = 3;

    private ?string $eps;

    public function new(int $size, ColorInterface $backgroundColor) : void
    {
        $this->eps = "%!PS-Adobe-3.0 EPSF-3.0\n"
            . "%%Creator: BaconQrCode\n"
            . sprintf("%%%%BoundingBox: 0 0 %d %d \n", $size, $size)
            . "%%BeginProlog\n"
            . "save\n"
            . "50 dict begin\n"
            . "/q { gsave } bind def\n"
            . "/Q { grestore } bind def\n"
            . "/s { scale } bind def\n"
            . "/t { translate } bind def\n"
            . "/r { rotate } bind def\n"
            . "/n { newpath } bind def\n"
            . "/m { moveto } bind def\n"
            . "/l { lineto } bind def\n"
            . "/c { curveto } bind def\n"
            . "/z { closepath } bind def\n"
            . "/f { eofill } bind def\n"
            . "/rgb { setrgbcolor } bind def\n"
            . "/cmyk { setcmykcolor } bind def\n"
            . "/gray { setgray } bind def\n"
            . "%%EndProlog\n"
            . "1 -1 s\n"
            . sprintf("0 -%d t\n", $size);

        if ($backgroundColor instanceof Alpha && 0 === $backgroundColor->getAlpha()) {
            return;
        }

        $this->eps .= wordwrap(
            '0 0 m'
            . sprintf(' %s 0 l', (string) $size)
            . sprintf(' %s %s l', (string) $size, (string) $size)
            . sprintf(' 0 %s l', (string) $size)
            . ' z'
            . ' ' .$this->getColorSetString($backgroundColor) . " f\n",
            75,
            "\n "
        );
    }

    public function scale(float $size) : void
    {
        if (null === $this->eps) {
            throw new RuntimeException('No image has been started');
        }

        $this->eps .= sprintf("%1\$s %1\$s s\n", round($size, self::PRECISION));
    }

    public function translate(float $x, float $y) : void
    {
        if (null === $this->eps) {
            throw new RuntimeException('No image has been started');
        }

        $this->eps .= sprintf("%s %s t\n", round($x, self::PRECISION), round($y, self::PRECISION));
    }

    public function rotate(int $degrees) : void
    {
        if (null === $this->eps) {
            throw new RuntimeException('No image has been started');
        }

        $this->eps .= sprintf("%d r\n", $degrees);
    }

    public function push() : void
    {
        if (null === $this->eps) {
            throw new RuntimeException('No image has been started');
        }

        $this->eps .= "q\n";
    }

    public function pop() : void
    {
        if (null === $this->eps) {
            throw new RuntimeException('No image has been started');
        }

        $this->eps .= "Q\n";
    }

    public function drawPathWithColor(Path $path, ColorInterface $color) : void
    {
        if (null === $this->eps) {
            throw new RuntimeException('No image has been started');
        }

        $fromX = 0;
        $fromY = 0;
        $this->eps .= wordwrap(
            'n '
            . $this->drawPathOperations($path, $fromX, $fromY)
            . ' ' . $this->getColorSetString($color) . " f\n",
            75,
            "\n "
        );
    }

    public function drawPathWithGradient(
        Path $path,
        Gradient $gradient,
        float $x,
        float $y,
        float $width,
        float $height
    ) : void {
        if (null === $this->eps) {
            throw new RuntimeException('No image has been started');
        }

        $fromX = 0;
        $fromY = 0;
        $this->eps .= wordwrap(
            'q n ' . $this->drawPathOperations($path, $fromX, $fromY) . "\n",
            75,
            "\n "
        );

        $this->createGradientFill($gradient, $x, $y, $width, $height);
    }

    public function done() : string
    {
        if (null === $this->eps) {
            throw new RuntimeException('No image has been started');
        }

        $this->eps .= "%%TRAILER\nend restore\n%%EOF";
        $blob = $this->eps;
        $this->eps = null;

        return $blob;
    }

    private function drawPathOperations(Iterable $ops, &$fromX, &$fromY) : string
    {
        $pathData = [];

        foreach ($ops as $op) {
            switch (true) {
                case $op instanceof Move:
                    $fromX = $toX = round($op->getX(), self::PRECISION);
                    $fromY = $toY = round($op->getY(), self::PRECISION);
                    $pathData[] = sprintf('%s %s m', $toX, $toY);
                    break;

                case $op instanceof Line:
                    $fromX = $toX = round($op->getX(), self::PRECISION);
                    $fromY = $toY = round($op->getY(), self::PRECISION);
                    $pathData[] = sprintf('%s %s l', $toX, $toY);
                    break;

                case $op instanceof EllipticArc:
                    $pathData[] = $this->drawPathOperations($op->toCurves($fromX, $fromY), $fromX, $fromY);
                    break;

                case $op instanceof Curve:
                    $x1 = round($op->getX1(), self::PRECISION);
                    $y1 = round($op->getY1(), self::PRECISION);
                    $x2 = round($op->getX2(), self::PRECISION);
                    $y2 = round($op->getY2(), self::PRECISION);
                    $fromX = $x3 = round($op->getX3(), self::PRECISION);
                    $fromY = $y3 = round($op->getY3(), self::PRECISION);
                    $pathData[] = sprintf('%s %s %s %s %s %s c', $x1, $y1, $x2, $y2, $x3, $y3);
                    break;

                case $op instanceof Close:
                    $pathData[] = 'z';
                    break;

                default:
                    throw new RuntimeException('Unexpected draw operation: ' . get_class($op));
            }
        }

        return implode(' ', $pathData);
    }

    private function createGradientFill(Gradient $gradient, float $x, float $y, float $width, float $height) : void
    {
        $startColor = $gradient->getStartColor();
        $endColor = $gradient->getEndColor();

        if ($startColor instanceof Alpha) {
            $startColor = $startColor->getBaseColor();
        }

        $startColorType = get_class($startColor);

        if (! in_array($startColorType, [Rgb::class, Cmyk::class, Gray::class])) {
            $startColorType = Cmyk::class;
            $startColor = $startColor->toCmyk();
        }

        if (get_class($endColor) !== $startColorType) {
            switch ($startColorType) {
                case Cmyk::class:
                    $endColor = $endColor->toCmyk();
                    break;

                case Rgb::class:
                    $endColor = $endColor->toRgb();
                    break;

                case Gray::class:
                    $endColor = $endColor->toGray();
                    break;
            }
        }

        $this->eps .= "eoclip\n<<\n";

        if ($gradient->getType() === GradientType::RADIAL()) {
            $this->eps .= " /ShadingType 3\n";
        } else {
            $this->eps .= " /ShadingType 2\n";
        }

        $this->eps .= " /Extend [ true true ]\n"
            . " /AntiAlias true\n";

        switch ($startColorType) {
            case Cmyk::class:
                $this->eps .= " /ColorSpace /DeviceCMYK\n";
                break;

            case Rgb::class:
                $this->eps .= " /ColorSpace /DeviceRGB\n";
                break;

            case Gray::class:
                $this->eps .= " /ColorSpace /DeviceGray\n";
                break;
        }

        switch ($gradient->getType()) {
            case GradientType::HORIZONTAL():
                $this->eps .= sprintf(
                    " /Coords [ %s %s %s %s ]\n",
                    round($x, self::PRECISION),
                    round($y, self::PRECISION),
                    round($x + $width, self::PRECISION),
                    round($y, self::PRECISION)
                );
                break;

            case GradientType::VERTICAL():
                $this->eps .= sprintf(
                    " /Coords [ %s %s %s %s ]\n",
                    round($x, self::PRECISION),
                    round($y, self::PRECISION),
                    round($x, self::PRECISION),
                    round($y + $height, self::PRECISION)
                );
                break;

            case GradientType::DIAGONAL():
                $this->eps .= sprintf(
                    " /Coords [ %s %s %s %s ]\n",
                    round($x, self::PRECISION),
                    round($y, self::PRECISION),
                    round($x + $width, self::PRECISION),
                    round($y + $height, self::PRECISION)
                );
                break;

            case GradientType::INVERSE_DIAGONAL():
                $this->eps .= sprintf(
                    " /Coords [ %s %s %s %s ]\n",
                    round($x, self::PRECISION),
                    round($y + $height, self::PRECISION),
                    round($x + $width, self::PRECISION),
                    round($y, self::PRECISION)
                );
                break;

            case GradientType::RADIAL():
                $centerX = ($x + $width) / 2;
                $centerY = ($y + $height) / 2;

                $this->eps .= sprintf(
                    " /Coords [ %s %s 0 %s %s %s ]\n",
                    round($centerX, self::PRECISION),
                    round($centerY, self::PRECISION),
                    round($centerX, self::PRECISION),
                    round($centerY, self::PRECISION),
                    round(max($width, $height) / 2, self::PRECISION)
                );
                break;
        }

        $this->eps .= " /Function\n"
            . " <<\n"
            . "  /FunctionType 2\n"
            . "  /Domain [ 0 1 ]\n"
            . sprintf("  /C0 [ %s ]\n", $this->getColorString($startColor))
            . sprintf("  /C1 [ %s ]\n", $this->getColorString($endColor))
            . "  /N 1\n"
            . " >>\n>>\nshfill\nQ\n";
    }

    private function getColorSetString(ColorInterface $color) : string
    {
        if ($color instanceof Rgb) {
            return $this->getColorString($color) . ' rgb';
        }

        if ($color instanceof Cmyk) {
            return $this->getColorString($color) . ' cmyk';
        }

        if ($color instanceof Gray) {
            return $this->getColorString($color) . ' gray';
        }

        return $this->getColorSetString($color->toCmyk());
    }

    private function getColorString(ColorInterface $color) : string
    {
        if ($color instanceof Rgb) {
            return sprintf('%s %s %s', $color->getRed() / 255, $color->getGreen() / 255, $color->getBlue() / 255);
        }

        if ($color instanceof Cmyk) {
            return sprintf(
                '%s %s %s %s',
                $color->getCyan() / 100,
                $color->getMagenta() / 100,
                $color->getYellow() / 100,
                $color->getBlack() / 100
            );
        }

        if ($color instanceof Gray) {
            return sprintf('%s', $color->getGray() / 100);
        }

        return $this->getColorString($color->toCmyk());
    }
}
