<?php


namespace Gek\Captcha;


class CaptchaBuilder
{

    #region Fields

    /**
     * @var string
     */
    protected string $phrase;

    /**
     * @var RgbColor
     */
    protected RgbColor $textColor;

    /**
     * @var RgbColor
     */
    protected RgbColor $frontLineColor;

    /**
     * @var RgbColor
     */
    protected RgbColor $backLineColor;

    protected RgbColor $backgroundColor;


    /**
     * @var int
     */
    protected int $minBackLineCount = 1;

    /**
     * @var int
     */
    protected int $maxBackLineCount = 5;

    /**
     * @var int
     */
    protected int $minFrontLineCount = 1;

    /**
     * @var int
     */
    protected int $maxFrontLineCount = 4;


    /** @var resource */
    protected $image = null;

    /** @var int */
    protected int $maxCharAngle = 10;

    /** @var int */
    protected int $maxCharOffset = 4;

    /**
     * @var bool
     */
    protected bool $interpolation = true;


    protected ?string $fontPath = null;

    /** @var bool */
    protected bool $distortion = true;

    /** @var bool  */
    protected bool $useEffects = false;

    /**
     * @var int
     */
    protected int $imageQuality = 90;




    #endregion Fields

    #region ctor

    public function __construct(?string $phrase = null)
    {
        if (empty($phrase)) {
            $phrase = PhraseBuilder::buildPhrase(rand(4,7));
        }

        $this->phrase = $phrase;

        $this->textColor = new RgbColor(rand(0, 150), rand(0, 150), rand(0, 150));
        $this->frontLineColor = new RgbColor(rand(130, 255), rand(130, 255), rand(130, 255));
        $this->backLineColor = new RgbColor(rand(80, 255), rand(80, 255), rand(80, 255));
        $this->backgroundColor = new RgbColor(rand(210, 255), rand(210, 255), rand(210, 255));


    }

    #endregion ctor

    #region properties

    /**
     * @return string
     */
    public function getPhrase(): string
    {
        return $this->phrase;
    }

    /**
     * @param string $phrase
     * @return CaptchaBuilder
     */
    public function setPhrase(string $phrase): CaptchaBuilder
    {
        $this->phrase = $phrase;
        return $this;
    }

    /**
     * @return RgbColor
     */
    public function getTextColor(): RgbColor
    {
        return $this->textColor;
    }

    /**
     * @param RgbColor $textColor
     * @return CaptchaBuilder
     */
    public function setTextColor(RgbColor $textColor): CaptchaBuilder
    {
        $this->textColor = $textColor;
        return $this;
    }

    /**
     * @return RgbColor
     */
    public function getFrontLineColor(): RgbColor
    {
        return $this->frontLineColor;
    }

    /**
     * @param RgbColor $frontLineColor
     * @return CaptchaBuilder
     */
    public function setFrontLineColor(RgbColor $frontLineColor): CaptchaBuilder
    {
        $this->frontLineColor = $frontLineColor;
        return $this;
    }

    /**
     * @return RgbColor
     */
    public function getBackLineColor(): RgbColor
    {
        return $this->backLineColor;
    }

    /**
     * @param RgbColor $backLineColor
     * @return CaptchaBuilder
     */
    public function setBackLineColor(RgbColor $backLineColor): CaptchaBuilder
    {
        $this->backLineColor = $backLineColor;
        return $this;
    }

    /**
     * @return int
     */
    public function getMinBackLineCount(): int
    {
        return $this->minBackLineCount;
    }

    /**
     * @param int $minBackLineCount
     * @return CaptchaBuilder
     */
    public function setMinBackLineCount(int $minBackLineCount): CaptchaBuilder
    {
        $this->minBackLineCount = $minBackLineCount;
        return $this;
    }

    /**
     * @return int
     */
    public function getMaxBackLineCount(): int
    {
        return $this->maxBackLineCount;
    }

    /**
     * @param int $maxBackLineCount
     * @return CaptchaBuilder
     */
    public function setMaxBackLineCount(int $maxBackLineCount): CaptchaBuilder
    {
        $this->maxBackLineCount = $maxBackLineCount;
        return $this;
    }

    /**
     * @return int
     */
    public function getMinFrontLineCount(): int
    {
        return $this->minFrontLineCount;
    }

    /**
     * @param int $minFrontLineCount
     * @return CaptchaBuilder
     */
    public function setMinFrontLineCount(int $minFrontLineCount): CaptchaBuilder
    {
        $this->minFrontLineCount = $minFrontLineCount;
        return $this;
    }

    /**
     * @return int
     */
    public function getMaxFrontLineCount(): int
    {
        return $this->maxFrontLineCount;
    }

    /**
     * @param int $maxFrontLineCount
     * @return CaptchaBuilder
     */
    public function setMaxFrontLineCount(int $maxFrontLineCount): CaptchaBuilder
    {
        $this->maxFrontLineCount = $maxFrontLineCount;
        return $this;
    }

    /**
     * @return int
     */
    public function getMaxCharAngle(): int
    {
        return $this->maxCharAngle;
    }

    /**
     * @param int $maxCharAngle
     * @return CaptchaBuilder
     */
    public function setMaxCharAngle(int $maxCharAngle): CaptchaBuilder
    {
        $this->maxCharAngle = $maxCharAngle;
        return $this;
    }

    /**
     * @return int
     */
    public function getMaxCharOffset(): int
    {
        return $this->maxCharOffset;
    }

    /**
     * @param int $maxCharOffset
     * @return CaptchaBuilder
     */
    public function setMaxCharOffset(int $maxCharOffset): CaptchaBuilder
    {
        $this->maxCharOffset = $maxCharOffset;
        return $this;
    }

    /**
     * @return bool
     */
    public function isInterpolation(): bool
    {
        return $this->interpolation;
    }

    /**
     * @param bool $interpolation
     * @return CaptchaBuilder
     */
    public function setInterpolation(bool $interpolation): CaptchaBuilder
    {
        $this->interpolation = $interpolation;
        return $this;
    }

    /**
     * @return string|null
     */
    public function getFontPath(): ?string
    {
        return $this->fontPath;
    }

    /**
     * @param string|null $fontPath
     * @return CaptchaBuilder
     */
    public function setFontPath(?string $fontPath): CaptchaBuilder
    {
        $this->fontPath = $fontPath;
        return $this;
    }

    /**
     * @return resource|null
     */
    public function getImage()
    {
        return $this->image;
    }

    /**
     * @return RgbColor
     */
    public function getBackgroundColor(): RgbColor
    {
        return $this->backgroundColor;
    }

    /**
     * @param RgbColor $backgroundColor
     * @return CaptchaBuilder
     */
    public function setBackgroundColor(RgbColor $backgroundColor): CaptchaBuilder
    {
        $this->backgroundColor = $backgroundColor;
        return $this;
    }

    /**
     * @return bool
     */
    public function isDistortion(): bool
    {
        return $this->distortion;
    }

    /**
     * @param bool $distortion
     * @return CaptchaBuilder
     */
    public function setDistortion(bool $distortion): CaptchaBuilder
    {
        $this->distortion = $distortion;
        return $this;
    }

    /**
     * @return bool
     */
    public function isUseEffects(): bool
    {
        return $this->useEffects;
    }

    /**
     * @param bool $useEffects
     * @return CaptchaBuilder
     */
    public function setUseEffects(bool $useEffects): CaptchaBuilder
    {
        $this->useEffects = $useEffects;
        return $this;
    }

    /**
     * @return int
     */
    public function getImageQuality(): int
    {
        return $this->imageQuality;
    }

    /**
     * @param int $imageQuality
     * @return CaptchaBuilder
     */
    public function setImageQuality(int $imageQuality): CaptchaBuilder
    {
        $this->imageQuality = $imageQuality;
        return $this;
    }


    #endregion properties

    #region methods

    /**
     * @param int $width
     * @param int $height
     * @return $this
     */
    public function build(int $width = 140, int $height = 40):self
    {
        $image = imagecreatetruecolor($width, $height);
        $bgClr = $this->allocateColor($image, $this->backgroundColor);
        imagefill($image, 0, 0, $bgClr);

        $backLineCount = rand($this->minBackLineCount, $this->maxBackLineCount);
        $blCol = $this->allocateColor($image, $this->backLineColor);
        for ($i = 0; $i < $backLineCount; $i++) {
            $this->drawLine($image, $width, $height, $blCol);
        }

        $font = $this->getFontPath();
        if (empty($font)) {
            $font = $this->randomFontPath();
        }

        $textColor = $this->allocateColor($image, $this->textColor);
        $this->drawText($image, $this->phrase, $width, $height, $font, $textColor);

        $frontLineCount = rand($this->minFrontLineCount, $this->maxFrontLineCount);
        $flCol = $this->allocateColor($image, $this->frontLineColor);
        for ($i = 0; $i < $frontLineCount; $i++) {
            $this->drawLine($image, $width, $height, $flCol);
        }

        if ($this->distortion) {
            $image = $this->distort($image, $width, $height, $bgClr);
        }

        if ($this->useEffects) {
            $this->randomEffects($image);
        }

        $this->image = $image;
        return $this;
    }

    /**
     * @param bool $destroyImg
     * @param int|null $quality
     */
    public function output(bool $destroyImg = false, ?int $quality = null){
        if($quality === null){
            $quality = $this->imageQuality;
        }
        imagejpeg($this->image, null, $quality);
        if($destroyImg){
            imagedestroy($this->image);
            $this->image = null;
        }
    }

    /**
     * @param bool $destroyImg
     * @param int|null $quality
     * @return false|string
     */
    public function getImageData(bool $destroyImg = false,?int $quality = null){
        ob_start();
        $this->output($destroyImg,$quality);
        return ob_get_clean();
    }

    /**
     * @param bool $destroyImg
     * @param int|null $quality
     * @return string
     */
    public function getBase64String(bool $destroyImg = false,?int $quality = null):string {
        return base64_encode($this->getImageData($destroyImg,$quality));
    }

    /**
     * @param bool $destroyImg
     * @param int|null $quality
     * @return string
     */
    public function getInlineBase64(bool $destroyImg = false,?int $quality = null):string {
        return 'data:image/jpeg;base64,'. $this->getBase64String($destroyImg,$quality);
    }

    /**
     * @param string $filename
     * @param bool $destroyImg
     * @param int|null $quality
     */
    public function save(string $filename, bool $destroyImg = false, ?int $quality = null)
    {
        if($quality === null){
            $quality = $this->imageQuality;
        }
        imagejpeg($this->image, $filename, $quality);
        if($destroyImg){
            imagedestroy($this->image);
            $this->image = null;
        }
    }

    #endregion methods

    #region utils

    protected function randomFontPath()
    {
        return __DIR__ . DIRECTORY_SEPARATOR . 'Fonts' . DIRECTORY_SEPARATOR . (rand(1, 20)) . '.ttf';
    }

    /**
     * @param resource $image
     * @param RgbColor $color
     * @return false|int
     */
    protected function allocateColor($image, RgbColor $color)
    {
        return imagecolorallocate($image, $color->getR(), $color->getG(), $color->getB());
    }

    /**
     * @param resource $image
     * @param int $width
     * @param int $height
     * @param int|RgbColor $color
     */
    protected function drawLine($image, int $width, int $height, $color)
    {
        if ($color instanceof RgbColor) {
            $color = $this->allocateColor($image, $color);
        }


        $vertical = (bool)rand(0, 1);

        if ($vertical) {
            // dikey
            $startX = rand(0, $width);
            $endX = rand(0, $width);
            $startY = rand(0, ($height / 2));
            $endY = rand(($height / 2), $height);
        } else {
            // yatay
            $startX = rand(0, ($width / 2));
            $endX = rand(($width / 2), $width);
            $startY = rand(0, $height);
            $endY = rand(0, $height);
        }

        $t = rand(1, 3);

        imagesetthickness($image, $t);
        imageline($image, $startX, $startY, $endX, $endY, $color);
    }

    /**
     * @param resource $image
     * @param string $text
     * @param int $width
     * @param int $height
     * @param string $fontPath
     * @param int|RgbColor $color
     */
    protected function drawText($image, string $text, int $width, int $height, string $fontPath, $color)
    {
        $charCount = mb_strlen($text);
        if ($charCount == 0) {
            return;
        }
        if ($color instanceof RgbColor) {
            $color = $this->allocateColor($image, $color);
        }

        $boxSize = ($width / $charCount) - (rand(1, 4) / 2);
        $box = imagettfbbox($boxSize, 0, $fontPath, $text);

        /*
         anahtar	içeriği
            0	Sol alt köşenin X konumu
            1	Sol alt köşenin Y konumu
            2	Sağ alt köşenin X konumu
            3	Sağ alt köşenin Y konumu
            4	Sağ üst köşenin X konumu
            5	Sağ üst köşenin Y konumu
            6	Sol üst köşenin X konumu
            7	Sol üst köşenin Y konumu
         */
        $boxWidth = $box[2] - $box[0];
        $boxHeight = $box[1] - $box[7];

        $x = ($width - $boxWidth) / 2;
        $y = (($height - $boxHeight) / 2) + $boxSize;

        for ($i = 0; $i < $charCount; $i++) {
            $charAngle = rand(0, $this->maxCharAngle);
            $charOffset = rand(0, $this->maxCharOffset);

            $curChar = mb_substr($text, $i, 1);
            $box = \imagettfbbox($boxSize, 0, $fontPath, $curChar);
            $wdth = $box[2] - $box[0];
            \imagettftext($image, $boxSize, $charAngle, $x, ($y + $charOffset), $color, $fontPath, $curChar);
            $x += $wdth;
        }


    }

    protected function randomEffects($image)
    {
        if (false === function_exists('imagefilter')) {
            return;
        }

        $coin = (bool)rand(0, 1);
        if($coin){
            //invert
            imagefilter($image, IMG_FILTER_NEGATE);
        }

        $coin = (bool)rand(0, 1);
        if($coin){
            //edge
            imagefilter($image, IMG_FILTER_EDGEDETECT);
        }

        $coin = (bool)rand(0, 1);
        if($coin){
            //contrast
            imagefilter($image, IMG_FILTER_CONTRAST, rand(-40,15));
        }

        $coin = (bool)rand(0, 1);
        if($coin){
            //colorize
            imagefilter($image, IMG_FILTER_COLORIZE, rand(-80,50),rand(-80,50),rand(-80,50));
        }
    }

    /**
     * @param resource $image
     * @param int $width
     * @param int $height
     * @param int|RgbColor $bgColor
     * @return false|resource
     */
    protected function distort($image, int $width, int $height, $bgColor)
    {
        if ($bgColor instanceof RgbColor) {
            $bgColor = $this->allocateColor($image, $bgColor);
        }
        $layer = imagecreatetruecolor($width, $height);
        $evr = rand(0, 10);
        $x = rand(0, $width);
        $y = rand(0, $height);
        $scale = 1.1 + (rand(0, 10000) / 30000);
        for ($i = 0; $i < $width; $i++) {
            for ($j = 0; $j < $height; $j++) {
                $Vx = $i - $x;
                $Vy = $j - $y;
                $Vn = sqrt($Vx * $Vx + $Vy * $Vy);

                if ($Vn != 0) {
                    $Vn2 = $Vn + 4 * sin($Vn / 30);
                    $nX = $x + ($Vx * $Vn2 / $Vn);
                    $nY = $y + ($Vy * $Vn2 / $Vn);
                } else {
                    $nX = $x;
                    $nY = $y;
                }
                $nY = $nY + $scale * sin($evr + $nX * 0.2);

                if ($this->interpolation) {
                    $p = $this->interpolate(
                        $nX - floor($nX),
                        $nY - floor($nY),
                        $this->getPixelColor($image, floor($nX), floor($nY), $bgColor),
                        $this->getPixelColor($image, ceil($nX), floor($nY), $bgColor),
                        $this->getPixelColor($image, floor($nX), ceil($nY), $bgColor),
                        $this->getPixelColor($image, ceil($nX), ceil($nY), $bgColor)
                    );
                } else {
                    $p = $this->getPixelColor($image, round($nX), round($nY), $bgColor);
                }

                if ($p == 0) {
                    $p = $bgColor;
                }

                imagesetpixel($layer, $i, $j, $p);
            }
        }

        return $layer;
    }

    /**
     * @param $x
     * @param $y
     * @param $nw
     * @param $ne
     * @param $sw
     * @param $se
     * @return int
     */
    protected function interpolate($x, $y, $nw, $ne, $sw, $se)
    {
        $col0 = RgbColor::createFromInt($nw);
        $col1 = RgbColor::createFromInt($ne);
        $col2 = RgbColor::createFromInt($sw);
        $col3 = RgbColor::createFromInt($se);


        $cx = 1.0 - $x;
        $cy = 1.0 - $y;

        $m0 = $cx * $col0->getR() + $x * $col1->getR();
        $m1 = $cx * $col2->getR() + $x * $col3->getR();
        $r = (int)($cy * $m0 + $y * $m1);

        $m0 = $cx * $col0->getG() + $x * $col1->getG();
        $m1 = $cx * $col2->getG() + $x * $col3->getG();
        $g = (int)($cy * $m0 + $y * $m1);

        $m0 = $cx * $col0->getB() + $x * $col1->getB();
        $m1 = $cx * $col2->getB() + $x * $col3->getB();
        $b = (int)($cy * $m0 + $y * $m1);

        return ($r << 16) | ($g << 8) | $b;
    }

    /**
     * @param resource $image
     * @param int $x
     * @param int $y
     * @param int $default
     * @return int
     */
    protected function getPixelColor($image, int $x, int $y, int $default)
    {
        $L = imagesx($image);
        $H = imagesy($image);
        if ($x < 0 || $x >= $L || $y < 0 || $y >= $H) {
            return $default;
        }

        return imagecolorat($image, $x, $y);
    }

    #endregion utils

    #region statics

    /**
     * @param string|null $phrase
     * @return static
     */
    public static function create(?string $phrase = null):self {
        return new self($phrase);
    }

    #endregion statics

}