<?php

/**
 * @license GNU GPLv3+
 * @author Aleksander Machniak <alec@alec.pl>
 */
class identicon_engine
{
    private $ident;
    private $width;
    private $height;
    private $margin;
    private $binary;
    private $color;
    private $bgcolor  = '#F9F9F9';
    private $mimetype = 'image/png';
    private $palette  = [
        '#F44336', '#E91E63', '#9C27B0', '#673AB7', '#3F51B5', '#2196F3',
        '#03A9F4', '#00BCD4', '#009688', '#4CAF50', '#8BC34A', '#CDDC39',
        '#FFEB3B', '#FFC107', '#FF9800', '#FF5722', '#795548', '#607D8B',
    ];
    private $grid = [
         0,  1,  2,  1,  0,
         3,  4,  5,  4,  3,
         6,  7,  8,  7,  6,
         9, 10, 11, 10,  9,
        12, 13, 14, 13, 12,
    ];

    const GRID_SIZE = 5;
    const ICON_SIZE = 150;


    /**
     * Class constructor
     *
     * @param string $ident   Unique identifier (email address)
     * @param int    $size    Icon size in pixels
     * @param string $bgcolor Icon background color
     */
    public function __construct($ident, $size = null, $bgcolor = null)
    {
        if (!$size) {
            $size = self::ICON_SIZE;
        }

        $this->ident  = $ident;
        $this->margin = (int) round($size / 10);
        $this->width  = (int) round(($size - $this->margin * 2) / self::GRID_SIZE) * self::GRID_SIZE + $this->margin * 2;
        $this->height = $this->width;

        if ($bgcolor) {
            if (preg_match('/^#?[0-9a-f]{6}$/', $bgcolor)) {
                if ($bgcolor[0] != '#') {
                    $bgcolor = "#{$bgcolor}";
                }
                $this->bgcolor = $bgcolor;
            }
            else if ($bgcolor === 'transparent') {
                $this->bgcolor = $bgcolor;
            }
        }

        $this->generate();
    }

    /**
     * Returns image mimetype
     */
    public function getMimetype()
    {
        return $this->mimetype;
    }

    /**
     * Returns the image in binary form
     */
    public function getBinary()
    {
        return $this->binary;
    }

    /**
     * Sends the image to the browser
     */
    public function sendOutput()
    {
        if ($this->binary) {
            $rcmail = rcmail::get_instance();
            $rcmail->output->future_expire_header(10 * 60);

            header('Content-Type: ' . $this->mimetype);
            header('Content-Size: ' . strlen($this->binary));
            echo $this->binary;

            return true;
        }

        return false;
    }

    /**
     * Icon generator
     */
    private function generate()
    {
        $ident = md5($this->ident, true);

        // set icon color
        $div         = intval(255/count($this->palette));
        $index       = intval(ord($ident[0]) / $div);
        $this->color = $this->palette[$index] ?? $this->palette[0];

        // set cell size
        $cell_width  = ($this->width - $this->margin * 2) / self::GRID_SIZE;
        $cell_height = ($this->height - $this->margin * 2) / self::GRID_SIZE;

        // create a grid
        foreach ($this->grid as $i => $idx) {
            $row_num    = intval($i / self::GRID_SIZE);
            $cell_num_h = $i - $row_num * self::GRID_SIZE;

            $this->grid[$i] = [
                'active' => ord($ident[$idx]) % 2 > 0,
                'x1'     => $cell_width * $cell_num_h + $this->margin,
                'y1'     => $cell_height * $row_num + $this->margin,
                'x2'     => $cell_width * ($cell_num_h + 1) + $this->margin,
                'y2'     => $cell_height * ($row_num + 1) + $this->margin,
            ];
        }

        // really generate the image using supported methods
        if (function_exists('imagepng')) {
            $this->generateGD();
        }
        else {
            // log an error
            $error = [
                'code'    => 500,
                'message' => "PHP-GD module not found. It's required by identicon plugin.",
            ];

            rcube::raise_error($error, true, false);
        }
    }

    /**
     * GD-based icon generation worker
     */
    private function generateGD()
    {
        // create an image, setup colors
        $image = imagecreate($this->width, $this->height);

        $color = $this->toRGB($this->color);
        $color = imagecolorallocate($image, $color[0], $color[1], $color[2]);

        if ($this->bgcolor === 'transparent') {
            $bgcolor = imagecolorallocatealpha($image, 0, 0, 0, 127);
            imagesavealpha($image, true);
        }
        else {
            $bgcolor = $this->toRGB($this->bgcolor);
            $bgcolor = imagecolorallocate($image, $bgcolor[0], $bgcolor[1], $bgcolor[2]);
        }

        imagefilledrectangle($image, 0, 0, $this->width, $this->height, $bgcolor);

        // draw the grid created in self::generate()
        foreach ($this->grid as $item) {
            if (!empty($item['active'])) {
                imagefilledrectangle($image, $item['x1'], $item['y1'], $item['x2'], $item['y2'], $color);
            }
        }

        // generate an image and save it to a variable
        ob_start();
        imagepng($image, null, 6, PNG_ALL_FILTERS);
        $this->binary = ob_get_contents();
        ob_end_clean();

        // cleanup
        imagedestroy($image);
    }

    /**
     * Convert #FFFFFF color format to 3-value RGB
     */
    private function toRGB($color)
    {
        preg_match('/^#?([A-F0-9]{2})([A-F0-9]{2})([A-F0-9]{2})/i', $color, $m);

        return [hexdec($m[1]), hexdec($m[2]), hexdec($m[3])];
    }
}
