<?php

namespace LasseRafn\InitialAvatarGenerator;

use Intervention\Image\Drivers\Gd\Driver as GdDriver;
use Intervention\Image\Drivers\Imagick\Driver as ImagickDriver;
use Intervention\Image\Geometry\Factories\CircleFactory;
use Intervention\Image\Image;
use Intervention\Image\ImageManager;
use LasseRafn\InitialAvatarGenerator\Translator\Base;
use LasseRafn\InitialAvatarGenerator\Translator\En;
use LasseRafn\InitialAvatarGenerator\Translator\Tr;
use LasseRafn\InitialAvatarGenerator\Translator\ZhCN;
use LasseRafn\Initials\Initials;
use LasseRafn\StringScript;
use RuntimeException;
use SVG\Nodes\Shapes\SVGCircle;
use SVG\Nodes\Shapes\SVGRect;
use SVG\Nodes\Structures\SVGFont;
use SVG\Nodes\Texts\SVGText;
use SVG\SVG;

class InitialAvatar
{
    /** @var ImageManager */
    protected $image;

    /** @var Initials */
    protected $initials_generator;

    protected $driver = 'gd'; // imagick or gd
    protected $fontSize = 0.5;
    protected $name = 'John Doe';
    protected $width = 48;
    protected $height = 48;
    protected $bgColor = '#f0e9e9';
    protected $fontColor = '#8b5d5d';
    protected $rounded = false;
    protected $smooth = false;
    protected $autofont = false;
    protected $keepCase = false;
    protected $allowSpecialCharacters = true;
    protected $fontFile = '/fonts/OpenSans-Regular.ttf';
    protected $fontName = 'OpenSans, sans-serif';
    protected $generated_initials = 'JD';
    protected $preferBold = false;
    protected $randomBgColor = false;

    /**
     * Language eg.en zh-CN.
     *
     * @var string
     */
    protected $language = 'en';

    /**
     * Role translator.
     *
     * @var Base
     */
    protected $translator;

    /**
     * Language related to translator.
     *
     * @var array
     */
    protected $translatorMap = [
        'en'    => En::class,
        'tr'    => Tr::class,
        'zh-CN' => ZhCN::class,
    ];

    public function __construct()
    {
        $this->setupImageManager();
        $this->initials_generator = new Initials();
    }

    /**
     * Create a ImageManager instance.
     */
    protected function setupImageManager()
    {
        $driver = $this->getDriver();

        $driverInstance = match ($driver) {
            'gd' => new GdDriver(),
            'imagick' => new ImagickDriver(),
            default => throw new RuntimeException("Unsupported driver."),
        };

        $this->image = new ImageManager($driverInstance);
    }

    /**
     * Set the name used for generating initials.
     *
     * @param string $nameOrInitials
     *
     * @return $this
     */
    public function name($nameOrInitials)
    {
        $nameOrInitials = $this->translate($nameOrInitials);
        $this->name = $nameOrInitials;
        $this->initials_generator->name($nameOrInitials);

        return $this;
    }

    /**
     * Transforms a unicode string to the proper format.
     *
     * @param string $char the code to be converted (e.g., f007 would mean the "user" symbol)
     *
     * @return $this
     */
    public function glyph($char)
    {
        $uChar = json_decode(sprintf('"\u%s"', $char), false);
        $this->name($uChar);

        return $this;
    }

    /**
     * Set the length of the generated initials.
     *
     * @param int $length
     *
     * @return $this
     */
    public function length($length = 2)
    {
        $this->initials_generator->length($length);

        return $this;
    }

    /**
     * Set the avatar/image size in pixels.
     *
     * @param int $size
     *
     * @return $this
     */
    public function size($size)
    {
        $this->width = (int) $size;
        $this->height = (int) $size;

        return $this;
    }

    /**
     * Set the avatar/image height in pixels.
     *
     * @param int $height
     *
     * @return $this
     */
    public function height($height)
    {
        $this->height = (int) $height;

        return $this;
    }

    /**
     * Set the avatar/image width in pixels.
     *
     * @param int $width
     *
     * @return $this
     */
    public function width($width)
    {
        $this->width = (int) $width;

        return $this;
    }

    /**
     * Prefer bold fonts (if possible).
     *
     * @return $this
     */
    public function preferBold()
    {
        $this->preferBold = true;

        return $this;
    }

    /**
     * Prefer regular fonts (if possible).
     *
     * @return $this
     */
    public function preferRegular()
    {
        $this->preferBold = false;

        return $this;
    }

    /**
     * Set the background color.
     *
     * @param string $background
     *
     * @return $this
     */
    public function background($background)
    {
        $this->bgColor = (string) $background;
        $this->randomBgColor = false;

        return $this;
    }

    /**
     * Set background color to be randomly generated for each avatar.
     *
     * @param bool $random
     * @param int  $saturation Saturation value (0-100)
     * @param int  $luminance  Luminance value (0-100)
     *
     * @return $this
     */
    public function randomBackground($random = true, int $saturation = 85, int $luminance = 60)
    {
        $this->randomBgColor = (bool) $random;

        if ($random) {
            // Generate an initial random color
            $this->generateRandomColor($saturation, $luminance);
        }

        return $this;
    }

    /**
     * Set the font color.
     *
     * @param string $color
     *
     * @return $this
     */
    public function color($color)
    {
        $this->fontColor = (string) $color;

        return $this;
    }

    /**
     * Automatically set a font and/or background color based on the supplied name.
     *
     * @param bool $foreground
     * @param bool $background
     *
     * @return $this
     */
    public function autoColor(bool $foreground = true, bool $background = true, int $saturation = 85, int $luminance = 60)
    {
        $hue = (crc32($this->name) % 360) / 360;
        $saturation /= 100;
        $luminance /= 100;

        $this->bgColor = $this->convertHSLtoRGB($hue, $saturation, $luminance);
        $this->fontColor = $this->getContrastColor($this->bgColor);

        return $this;
    }

    /**
     * Set the font file by path.
     *
     * @param string $font
     *
     * @return $this
     */
    public function font($font)
    {
        $this->fontFile = $font;

        return $this;
    }

    /**
     * Set the font name.
     *
     * Example: "Open Sans"
     *
     * @param string $name
     *
     * @return $this
     */
    public function fontName($name)
    {
        $this->fontName = $name;

        return $this;
    }

    /**
     * Use imagick as the driver.
     *
     * @return $this
     */
    public function imagick()
    {
        $this->driver = 'imagick';

        $this->setupImageManager();

        return $this;
    }

    /**
     * Use GD as the driver.
     *
     * @return $this
     */
    public function gd()
    {
        $this->driver = 'gd';

        $this->setupImageManager();

        return $this;
    }

    /**
     * Set if should make a round image or not.
     *
     * @param bool $rounded
     *
     * @return $this
     */
    public function rounded($rounded = true)
    {
        $this->rounded = (bool) $rounded;

        return $this;
    }

    /**
     * Set if should detect character script
     * and use a font that supports it.
     *
     * @param bool $autofont
     *
     * @return $this
     */
    public function autoFont($autofont = true)
    {
        $this->autofont = (bool) $autofont;

        return $this;
    }

    /**
     * Set if should make a rounding smoother with a resizing hack.
     *
     * @param bool $smooth
     *
     * @return $this
     */
    public function smooth($smooth = true)
    {
        $this->smooth = (bool) $smooth;

        return $this;
    }

    /**
     * Set if should skip uppercasing the name.
     *
     * @param bool $keepCase
     *
     * @return $this
     */
    public function keepCase($keepCase = true)
    {
        $this->keepCase = (bool) $keepCase;

        return $this;
    }

    /**
     * Set if should allow (or remove) special characters.
     *
     * @param bool $allowSpecialCharacters
     *
     * @return $this
     */
    public function allowSpecialCharacters($allowSpecialCharacters = true)
    {
        $this->allowSpecialCharacters = (bool) $allowSpecialCharacters;

        return $this;
    }

    /**
     * Set the font size in percentage
     * (0.1 = 10%).
     *
     * @param float $size
     *
     * @return $this
     */
    public function fontSize($size = 0.5)
    {
        $this->fontSize = (float) round($size, 2);

        return $this;
    }

    /**
     * Generate the image.
     *
     * @param null|string $name
     *
     * @return Image
     */
    public function generate($name = null)
    {
        if ($name !== null) {
            $this->name = $name;
            $this->generated_initials = $this->initials_generator->keepCase($this->getKeepCase())
                                                                 ->allowSpecialCharacters($this->getAllowSpecialCharacters())
                                                                 ->generate($name);
        }

        return $this->makeAvatar($this->image);
    }

    /**
     * Generate the image.
     *
     * @param null|string $name
     *
     * @return SVG
     */
    public function generateSvg($name = null)
    {
        if ($name !== null) {
            $this->name = $name;
            $this->generated_initials = $this->initials_generator->keepCase($this->getKeepCase())
                                                                 ->allowSpecialCharacters($this->getAllowSpecialCharacters())
                                                                 ->generate($name);
        }

        return $this->makeSvgAvatar();
    }

    /**
     * Will return the generated initials.
     *
     * @return string
     */
    public function getInitials()
    {
        return $this->initials_generator->keepCase($this->getKeepCase())
                                        ->allowSpecialCharacters($this->getAllowSpecialCharacters())
                                        ->name($this->name)
                                        ->getInitials();
    }

    /**
     * Will return the background color parameter.
     * If randomBgColor is enabled, generates a new random color each time.
     *
     * @return string
     */
    public function getBackgroundColor()
    {
        if ($this->randomBgColor) {
            // Generate a new random color each time this method is called
            $this->generateRandomColor();
        }

        return $this->bgColor;
    }

    /**
     * Will return whether random background color is enabled.
     *
     * @return bool
     */
    public function getRandomBackgroundColor()
    {
        return $this->randomBgColor;
    }

    /**
     * Will return the set driver.
     *
     * @return string
     */
    public function getDriver()
    {
        return $this->driver;
    }

    /**
     * Will return the font color parameter.
     *
     * @return string
     */
    public function getColor()
    {
        return $this->fontColor;
    }

    /**
     * Will return the font size parameter.
     *
     * @return float
     */
    public function getFontSize()
    {
        return $this->fontSize;
    }

    /**
     * Will return the font file parameter.
     *
     * @return string
     */
    public function getFontFile()
    {
        return $this->fontFile;
    }

    /**
     * Will return the font name parameter for SVGs.
     *
     * @return string
     */
    public function getFontName()
    {
        return $this->fontName;
    }

    /**
     * Will return the round parameter.
     *
     * @return bool
     */
    public function getRounded()
    {
        return $this->rounded;
    }

    /**
     * Will return the smooth parameter.
     *
     * @return bool
     */
    public function getSmooth()
    {
        return $this->smooth;
    }

    /**
     * @deprecated for getWidth and getHeight
     */
    public function getSize()
    {
        return $this->getWidth();
    }

    /**
     * Will return the width parameter.
     *
     * @return int
     */
    public function getWidth()
    {
        return $this->width;
    }

    /**
     * Will return the height parameter.
     *
     * @return int
     */
    public function getHeight()
    {
        return $this->height;
    }

    /**
     * Will return the keepCase parameter.
     *
     * @return bool
     */
    public function getKeepCase()
    {
        return $this->keepCase;
    }

    /**
     * Will return the allowSpecialCharacters parameter.
     *
     * @return bool
     */
    public function getAllowSpecialCharacters()
    {
        return $this->allowSpecialCharacters;
    }

    /**
     * Will return the autofont parameter.
     *
     * @return bool
     */
    public function getAutoFont()
    {
        return $this->autofont;
    }

    /**
     * Set language of name, pls use `language` before `name`, just like
     * ```php
     * $avatar->language('en')->name('Mr Green'); // Right
     * $avatar->name('Mr Green')->language('en'); // Wrong
     * ```.
     *
     * @param string $language
     *
     * @return $this
     */
    public function language($language)
    {
        $this->language = $language ?: 'en';

        return $this;
    }

    /**
     * Add new translators designed by user.
     *
     * @param array $translatorMap
     *                             ```php
     *                             $translatorMap = [
     *                             'fr' => 'foo\bar\Fr',
     *                             'zh-TW' => 'foo\bar\ZhTW'
     *                             ];
     *                             ```
     *
     * @return $this
     */
    public function addTranslators($translatorMap)
    {
        $this->translatorMap = array_merge($this->translatorMap, $translatorMap);

        return $this;
    }

    protected function translate($nameOrInitials)
    {
        return $this->getTranslator()->translate($nameOrInitials);
    }

    /**
     * Instance the translator by language.
     *
     * @return Base
     */
    protected function getTranslator()
    {
        if ($this->translator instanceof Base && $this->translator->getSourceLanguage() === $this->language) {
            return $this->translator;
        }

        $translatorClass = array_key_exists($this->language, $this->translatorMap) ? $this->translatorMap[$this->language] : 'LasseRafn\\InitialAvatarGenerator\\Translator\\En';

        return $this->translator = new $translatorClass();
    }

    /**
     * @param ImageManager $image
     *
     * @return Image
     */
    protected function makeAvatar($image)
    {
        $width = $this->getWidth();
        $height = $this->getHeight();
        $bgColor = $this->getBackgroundColor();
        $name = $this->getInitials();
        $fontFile = $this->findFontFile();
        $color = $this->getColor();
        $fontSize = $this->getFontSize();

        if ($this->getRounded() && $this->getSmooth()) {
            $width *= 5;
            $height *= 5;
        }

        $avatar = $image->create($width, $height);

        if (!$this->getRounded()) {
            $avatar = $avatar->fill($bgColor);
        }

        if ($this->getRounded()) {
            $avatar = $avatar->drawCircle(
                x: $width / 2,
                y: $height / 2,
                init: function (CircleFactory $circle) use ($width, $bgColor) {
                    $circle->radius($width -2);
                    $circle->background($bgColor);
                }
            );
        }

        if ($this->getRounded() && $this->getSmooth()) {
            $width /= 5;
            $height /= 5;
            $avatar->resize($width, $height);
        }

        return $avatar->text($name, $width / 2, $height / 2, function ($draw) use ($width, $color, $fontFile, $fontSize) {
            if ($fontFile !== null) {
                $draw->filename($fontFile);
            }

            $draw->size($width * $fontSize);
            $draw->color($color);
            $draw->align('center');
            $draw->valign('center');
        });
    }

    /**
     * @return SVG
     */
    protected function makeSvgAvatar()
    {
        // Original document
        $image = new SVG($this->getWidth(), $this->getHeight());
        $document = $image->getDocument();
        $document->setAttribute('viewBox', "0 0 {$this->getWidth()} {$this->getHeight()}");

        // Background
        if ($this->getRounded()) {
            // Circle
            $background = new SVGCircle($this->getWidth() / 2, $this->getHeight() / 2, $this->getWidth() / 2);
        } else {
            // Rectangle
            $background = new SVGRect(0, 0, $this->getWidth(), $this->getHeight());
        }

        $background->setStyle('fill', $this->getBackgroundColor());
        $document->addChild($background);

        // Text
        $text = new SVGText($this->getInitials(), '50%', '50%');
        $text->setFont(new SVGFont($this->getFontName(), $this->findFontFile()));
        $text->setStyle('line-height', 1);
        $text->setAttribute('dy', '.1em');
        $text->setAttribute('fill', $this->getColor());
        $text->setAttribute('font-size', $this->getFontSize() * $this->getWidth());
        $text->setAttribute('text-anchor', 'middle');
        $text->setAttribute('dominant-baseline', 'middle');

        if ($this->preferBold) {
            $text->setStyle('font-weight', 600);
        }

        $document->addChild($text);

        return $image;
    }


    /**
     * @return string|null
     */
    protected function findFontFile()
    {
        $fontFile = $this->getFontFile();

        if ($this->getAutoFont()) {
            $fontFile = $this->getFontByScript();
        }

        $weightsToTry = ['Regular'];

        if ($this->preferBold) {
            $weightsToTry = ['Bold', 'Semibold', 'Regular'];
        }

        $originalFile = $fontFile;

        foreach ($weightsToTry as $weight) {
            $fontFile = preg_replace('/(\-(Bold|Semibold|Regular))/', "-{$weight}", $originalFile);

            if (file_exists($fontFile)) {
                return $fontFile;
            }

            if (file_exists(__DIR__.$fontFile)) {
                return __DIR__.$fontFile;
            }

            if (file_exists(__DIR__.'/'.$fontFile)) {
                return __DIR__.'/'.$fontFile;
            }
        }

        return null;
    }

    protected function getFontByScript()
    {
        // Arabic
        if (StringScript::isArabic($this->getInitials())) {
            return __DIR__.'/fonts/script/Noto-Arabic-Regular.ttf';
        }

        // Armenian
        if (StringScript::isArmenian($this->getInitials())) {
            return __DIR__.'/fonts/script/Noto-Armenian-Regular.ttf';
        }

        // Bengali
        if (StringScript::isBengali($this->getInitials())) {
            return __DIR__.'/fonts/script/Noto-Bengali-Regular.ttf';
        }

        // Georgian
        if (StringScript::isGeorgian($this->getInitials())) {
            return __DIR__.'/fonts/script/Noto-Georgian-Regular.ttf';
        }

        // Hebrew
        if (StringScript::isHebrew($this->getInitials())) {
            return __DIR__.'/fonts/script/Noto-Hebrew-Regular.ttf';
        }

        // Mongolian
        if (StringScript::isMongolian($this->getInitials())) {
            return __DIR__.'/fonts/script/Noto-Mongolian-Regular.ttf';
        }

        // Thai
        if (StringScript::isThai($this->getInitials())) {
            return __DIR__.'/fonts/script/Noto-Thai-Regular.ttf';
        }

        // Tibetan
        if (StringScript::isTibetan($this->getInitials())) {
            return __DIR__.'/fonts/script/Noto-Tibetan-Regular.ttf';
        }

        // Turkish
        if (StringScript::isLatin($this->getInitials())) {
            return __DIR__.'/fonts/NotoSans-Regular.ttf';
        }

        // Chinese & Japanese
        if (StringScript::isJapanese($this->getInitials()) || StringScript::isChinese($this->getInitials())) {
            return __DIR__.'/fonts/script/Noto-CJKJP-Regular.otf';
        }

        return $this->getFontFile();
    }

    /**
     * Convert HSL color value produced by autoColor() to RGB value expected by image driver.
     */
    protected function convertHSLtoRGB($h, $s, $l, $toHex = true)
    {
        assert((0 <= $h) && ($h <= 1));

        $red = $l;
        $green = $l;
        $blue = $l;

        $v = ($l <= 0.5) ? ($l * (1.0 + $s)) : ($l + $s - $l * $s);
        if ($v > 0) {
            $m = $l + $l - $v;
            $sv = ($v - $m) / $v;
            $h *= 6.0;
            $sextant = floor($h);
            $fract = $h - $sextant;
            $vsf = $v * $sv * $fract;
            $mid1 = $m + $vsf;
            $mid2 = $v - $vsf;

            switch ($sextant) {
                case 0:
                    $red = $v;
                    $green = $mid1;
                    $blue = $m;
                    break;
                case 1:
                    $red = $mid2;
                    $green = $v;
                    $blue = $m;
                    break;
                case 2:
                    $red = $m;
                    $green = $v;
                    $blue = $mid1;
                    break;
                case 3:
                    $red = $m;
                    $green = $mid2;
                    $blue = $v;
                    break;
                case 4:
                    $red = $mid1;
                    $green = $m;
                    $blue = $v;
                    break;
                case 5:
                    $red = $v;
                    $green = $m;
                    $blue = $mid2;
                    break;
            }
        }

        $red = round($red * 255, 0);
        $green = round($green * 255, 0);
        $blue = round($blue * 255, 0);

        if ($toHex) {
            $red = ($red < 15) ? '0'.dechex($red) : dechex($red);
            $green = ($green < 15) ? '0'.dechex($green) : dechex($green);
            $blue = ($blue < 15) ? '0'.dechex($blue) : dechex($blue);

            return "#{$red}{$green}{$blue}";
        } else {
            return ['red' => $red, 'green' => $green, 'blue' => $blue];
        }
    }

    /**
     * Get contrasting foreground color for autoColor background.
     */
    protected function getContrastColor($hexColor)
    {
        // hexColor RGB
        $R1 = hexdec(substr($hexColor, 1, 2));
        $G1 = hexdec(substr($hexColor, 3, 2));
        $B1 = hexdec(substr($hexColor, 5, 2));

        // Black RGB
        $blackColor = '#000000';
        $R2BlackColor = hexdec(substr($blackColor, 1, 2));
        $G2BlackColor = hexdec(substr($blackColor, 3, 2));
        $B2BlackColor = hexdec(substr($blackColor, 5, 2));

        // Calc contrast ratio
        $L1 = 0.2126 * pow($R1 / 255, 2.2) +
            0.7152 * pow($G1 / 255, 2.2) +
            0.0722 * pow($B1 / 255, 2.2);

        $L2 = 0.2126 * pow($R2BlackColor / 255, 2.2) +
            0.7152 * pow($G2BlackColor / 255, 2.2) +
            0.0722 * pow($B2BlackColor / 255, 2.2);

        $contrastRatio = 0;
        if ($L1 > $L2) {
            $contrastRatio = (int) (($L1 + 0.05) / ($L2 + 0.05));
        } else {
            $contrastRatio = (int) (($L2 + 0.05) / ($L1 + 0.05));
        }

        // If contrast is more than 5, return black color
        if ($contrastRatio > 5) {
            return '#000000';
        } else {
            // if not, return white color.
            return '#FFFFFF';
        }
    }

    /**
     * Generate a random background color.
     *
     * @param int $saturation Saturation value (0-100)
     * @param int $luminance  Luminance value (0-100)
     *
     * @return string The generated hex color
     */
    protected function generateRandomColor(int $saturation = 85, int $luminance = 60)
    {
        // Generate a random hue (0-1)
        $hue = mt_rand(0, 359) / 360;
        $saturation /= 100;
        $luminance /= 100;

        $hexColor = $this->convertHSLtoRGB($hue, $saturation, $luminance);
        $this->bgColor = $hexColor;
        $this->fontColor = $this->getContrastColor($hexColor);

        return $hexColor;
    }
}
