<?php
namespace Jdenticon\Rendering;

use Jdenticon\Color;

class ImagickRendererLine
{
    /**
     * Creates a new line from a vector.
     * 
     * @param float $x0 Vector start x coordinate.
     * @param float $y0 Vector start y coordinate.
     * @param float $x1 Vector end x coordinate.
     * @param float $y1 Vector end y coordinate.
     * @return self
     */
    public static function fromVector(float $x0, float $y0, float $x1, float $y1): self
    {
        $line = new ImagickRendererLine();

        $line->Px = $x0;
        $line->Py = $y0;
        
        $line->rx = $x1 - $x0;
        $line->ry = $y1 - $y0;

        return $line;
    }

    /**
     * Moves the line to the right relative the direction vector.
     * 
     * @param float $distance The number of pixels to move the line.
     */
    public function moveRight(float $distance): void
    {
        // Ortogonal direction vector
        $rx = -$this->ry;
        $ry = $this->rx;
        
        $multiplier = $distance / sqrt($rx * $rx + $ry * $ry);

        $this->Px += $rx * $multiplier;
        $this->Py += $ry * $multiplier;
    }

    /**
     * Computes the point at which two lines intersect.
     * 
     * @param ImagickRendererLine $l1
     * @param ImagickRendererLine $l2
     * @return Point|null
     */
    public static function intersection(ImagickRendererLine $l1, ImagickRendererLine $l2): ?Point
    {
        $rs = $l1->rx * $l2->ry - $l1->ry * $l2->rx;

        if ($rs == 0) {
            return null;
        }
        
        $u = (($l2->Px - $l1->Px) * $l1->ry - ($l2->Py - $l1->Py) * $l1->rx) / $rs;

        return new Point(
            $l2->Px + $u * $l2->rx,
            $l2->Py + $u * $l2->ry
        );
    }

    /**
     * X coordiate of a point on the line.
     */
    public float $Px;
    /**
     * Y coordiate of a point on the line.
     */
    public float $Py;

    /**
     * X component of the direction vector.
     */
    public float $rx;
    /**
     * Y component of the direction vector.
     */
    public float $ry;
}

/**
 * Renders icons as PNG using ImageMagick.
 * 
 * Unfortunately the ImageMagick vector renderer has a lot of quirks that
 * we need to accomodate. The most obvious is that the renderer always render
 * all polygons 1/2 pixel too large. This behavior is documented here:
 * http://www.imagemagick.org/Usage/draw/#bounds
 * 
 * To prevent this we shrink all polygons with 1/2 pixels before passing them
 * to ImageMagick. 
 * 
 * Another quirk is that if the polygon including the 1/2 pixel invisible border
 * align perfectly to the pixel grid, white pixels will appear near edge
 * intersections. Paths containing arcs will sometimes appear with horizontal 
 * lines drawn to the right side of the image.
 * 
 * To prevent this (in most cases) we add 0.00013 to all coordinates.
 * 
 */
class ImagickRenderer extends AbstractRenderer
{
    private \ImagickDraw $draw;
    private array $polygon;
    private int $width;
    private int $height;

    /**
     * This constant is added to all coordinates to avoid white pixels
     * that sometimes appear near edge intersections when a polygon including
     * its 1/2 invisible border is perfectly aligned to the pixel grid. 
     */
    private const PREVENT_WHITE_PIXELS_OFFSET = -0.00013;

    /**
     * Creates an instance of the class ImagickRenderer.
     *
     * @param int $width The width of the icon in pixels.
     * @param int $height The height of the icon in pixels.
     */
    public function __construct(int $width, int $height)
    {
        parent::__construct();
        $this->draw = new \ImagickDraw();
        $this->draw->setStrokeWidth(1);
        
        $this->width = $width;
        $this->height = $height;
    }

    /**
     * Gets the MIME type of the renderer output.
     *
     * @return string
     */
    public function getMimeType(): string
    {
        return 'image/png';
    }

    /**
     * Adds a circle without translating its coordinates.
     *
     * @param float $x The x-coordinate of the bounding rectangle upper-left corner.
     * @param float $y The y-coordinate of the bounding rectangle upper-left corner.
     * @param float $size The size of the bounding rectangle.
     * @param bool $counterClockwise If true the circle will be drawn counter clockwise.
     */
    protected function addCircleNoTransform(float $x, float $y, float $size, bool $counterClockwise): void
    {
        if ($counterClockwise) {
            $x -= $size + 0.5;
            $y -= 1;
        } else {
            $size -= 1;
            $y -= 0.5;
        }

        $radius = $size / 2;
        $this->draw->pathMoveToAbsolute(
            $x + $size + self::PREVENT_WHITE_PIXELS_OFFSET, 
            $y + $radius + self::PREVENT_WHITE_PIXELS_OFFSET);
        $this->draw->pathEllipticArcRelative($radius, $radius, 
            M_PI * 2, true, $counterClockwise, 0, 1);
        $this->draw->pathClose();
    }

    /**
     * Adds a polygon without translating its coordinates.
     *
     * @param array<Point> $points An array of the points that the polygon consists of.
     */
    protected function addPolygonNoTransform(array $points): void
    {
        $firstPoint = $points[0];
        $lastPoint = end($points);

        // Ensure polygon is closed
        if ($firstPoint->x != $lastPoint->x ||
            $firstPoint->y != $lastPoint->y
        ) {
            $points[] = $firstPoint;
        }

        // Determine if polygon is an outer ring
        // (source: https://stackoverflow.com/a/1165943)
        $sum = 0;
        $previousPoint = null;

        foreach ($points as $point) {
            if ($previousPoint !== null) {
                $sum += ($point->x - $previousPoint->x) * ($point->y + $previousPoint->y);
            }
            $previousPoint = $point;
        }

        $isOuterRing = $sum < 0;

        // ImageMagick draws all polygons 1 pixel too large. To prevent this,
        // shrink polygons by 1 pixel.
        $lines = [];
        $previousPoint = null;

        // Transform all edges to lines.
        foreach ($points as $point) {
            if ($previousPoint !== null) {
                $lines[] = $line = ImagickRendererLine::fromVector(
                    $previousPoint->x, $previousPoint->y,
                    $point->x, $point->y
                );

                // ImageMagick draws a 1px border along the outer ring. To 
                // prevent the border overlaps inner rings too close to the 
                // outer ring, only inflate inner rings by 1/4 pixel.
                $line->moveRight($isOuterRing ? 0.5 : 0.25);
            }
            $previousPoint = $point;
        }

        $first = true;
        $previousLine = end($lines);

        // Reconstruct point array from line intersections and draw the polygon
        foreach ($lines as $line) {
            $points[] = $point = ImagickRendererLine::intersection($previousLine, $line);

            // Subtract 1/2 pixel to align the shapes to the pixel grid.
            if ($first) {
                $this->draw->pathMoveToAbsolute(
                    $point->x - 0.5 + self::PREVENT_WHITE_PIXELS_OFFSET,
                    $point->y - 0.5 + self::PREVENT_WHITE_PIXELS_OFFSET);
                    
                $first = false;
            } else {
                $this->draw->pathLineToAbsolute(
                    $point->x - 0.5 + self::PREVENT_WHITE_PIXELS_OFFSET,
                    $point->y - 0.5 + self::PREVENT_WHITE_PIXELS_OFFSET);
            }

            $previousLine = $line;
        }

        $this->draw->pathClose();
    }

    /**
     * Begins a new shape. The shape should be ended with a call to endShape.
     *
     * @param \Jdenticon\Color $color The color of the shape.
     */
    public function beginShape(Color $color): void
    {
        $this->draw->setFillColor($color->__toString());
        $this->draw->pathStart();
    }
    
    /**
     * Ends the currently drawn shape.
     */
    public function endShape(): void
    {
        $this->draw->pathFinish();
    }

    /**
     * Renders this image as a PNG data stream.
     * 
     * @return string
     */
    public function getData(): string
    {
        $imagick = new \Imagick();
        $imagick->newImage($this->width, $this->height, $this->backgroundColor->__toString());
        $imagick->setImageFormat('png');

        if (method_exists($imagick, 'setImageProperty')) {
            $imagick->setImageProperty('Software', 'Jdenticon');
        } else {
            $imagick->setImageAttribute('Software', 'Jdenticon');
        }
        
        $imagick->drawImage($this->draw);
        return $imagick->getImageBlob();
    }
}
