<?php
class Images extends Buyers
{
    private $image;
    private $width;
    private $height;
    private $type;
    private $imageResized;
    private $image_quality = "90";
    //первые 3 байта для разных типов файлов
    private $allowed_extensions = array("ffd8ff" => "jpg", "3c7376"=>"svg", "3c3f78"=>"svg", "89504e" => "png", "474946" => "gif", "524946" => "webp");

    //названия папок строго привязаны к названиям таблиц: pages, banners, comments и т.п.
    //в таблице images, в поле relation, указывается к какой таблице принадлежит изображение (метод get_relation())
    public $images_dir              = "files/";
    public $banners_images_dir      = "files/banners/";
    public $blog_images_dir         = "files/blog/";
    public $pages_images_dir        = "files/pages/";
    public $resized_images_dir      = "cache/resize/";

    //дефолтное фото
    public $no_image = 'files/no-image.png';

    public function __construct()
    {
        // Создаем папку для ресайзов изображений, если её нет
        if(!is_dir($this->config->root_dir.$this->resized_images_dir))
            mkdir($this->config->root_dir.$this->resized_images_dir, 0755);
    }

    public function get_images($filter = array())
    {
        $relation_filter = '';
        $relation_id_filter = '';

        if(!empty($filter['relation']))
            $relation_filter = $this->db->placehold('AND relation = ?', $filter['relation']);

        if(!empty($filter['relation_id']))
            $relation_id_filter = $this->db->placehold('AND relation_id IN(?@)', (array)$filter['relation_id']);

        $this->db->query("SELECT * FROM images WHERE 1 $relation_id_filter $relation_filter ORDER BY position");
        return $this->db->results();
    }

    public function add_image($image)
    {
        if(empty($image->relation) || empty($image->relation_id))
            return false;

        $this->db->query("SELECT position FROM images WHERE relation = ? AND relation_id = ? ORDER BY position DESC LIMIT 1", $image->relation, $image->relation_id);
        $image->position = intval($this->db->result('position')) + 1;

        $this->db->query("INSERT INTO images SET ?%", $image);
        return $this->db->insert_id();
    }

    //используется для обновления (удаления, добавления) фоток
    //relation_id - ID связующей таблицы (админ модуля), например: page_id, comment_id, blog_id, banner_id...
    public function update_images($relation_id, $image_dir = false)
    {
        //по дефолту папка с изображения для страниц
        if(empty($image_dir))
            $image_dir = $this->pages_images_dir;

        //изображения которые надо удалить
        if($images = $this->request->post('delete_images'))
            $this->delete_images($images);

        //изображения которые надо закачать
        if($dropped_images = $this->request->files('dropped_images', 'tmp_name'))
        foreach($dropped_images as $tmp_file)
        {
            if($image = $this->upload_image($tmp_file, $image_dir, $relation_id))
                $this->add_image($image);
        }

        //обновим позиции
        if($images = $this->request->post('images'))
        foreach($images AS $position => $image_id)
            $this->db->query("UPDATE images SET position = ? WHERE id = ? LIMIT 1", $position++, $image_id);
    }

    public function upload_image($path_to_file, $image_dir, $relation_id)
    {
        if(empty($path_to_file))
            return false;

        $image = new stdClass();

        if(!$content = file_get_contents($path_to_file))
            return false;

        $image->relation_id = $relation_id;
        $image->filesize = strlen($content);
        $image->relation = $this->get_relation($image_dir);

        if(!$image->filetype = $type =  $this->get_extension($content))
            return false;

        //генерируем уникальное имя файла
        $image->filename = $this->custom->generate_name(10, $type);
        while($this->count_images($image->filename) > 0)
            $image->filename = $this->custom->generate_name(10, $type);

        //сохраняем файл
        $image->filepath = $image_dir . $image->filename;
        if(!file_put_contents($image->filepath, $content))
            return false;

        if($type != 'svg')
            list($image->width, $image->height, $type, $attr) = getimagesize($image->filepath);

        return $image;
    }

    public function delete_images($images)
    {
        foreach($images as $image_id)
        {
            $this->db->query("SELECT relation, filename FROM images WHERE id = ? LIMIT 1", $image_id);
            if($res = $this->db->result())
            {
                //очистим кэш по маске: relation.filename.*
                $filename = pathinfo($res->filename, PATHINFO_FILENAME);
                $mask = $res->relation.".".$filename.".*";
                $this->clear_image_cache($mask);

                @unlink($this->images_dir . $res->relation . "/" . $res->filename);
                $this->db->query("DELETE FROM images WHERE id = ?", $image_id);
            }
        }
    }

    private function get_relation($image_dir)
    {
        // получим вместо "files/banners/" = "banners"
        preg_match('#files\/(.*?)\/#', $image_dir, $relation);
        return $relation[1] ?? null;
    }

    private function count_images($filename)
    {
        $this->db->query("SELECT count(id) AS count FROM images WHERE filename = ?", $filename);
        return $this->db->result('count');
    }

    private function clear_image_cache($mask)
    {
        //путь к ресайзам
        $dir = $this->config->root_dir . $this->resized_images_dir;
        foreach (glob($dir . $mask) as $filename)
            unlink($filename);
    }

    private function get_extension($content)
    {
        //получаем первые 3 байта
        $header = substr($content, 0, 3);
        //берем 16-ти ричное значение прочитанных данных
        $check = unpack('H*', $header);
        $hex = strtolower($check[1]);
        return $this->allowed_extensions[$hex] ?? null;
    }

    /*
    модификатор смарти RESIZE.

    |resize:600:300
    создаст превью 600 на 300 с белым фоном если формат оригинала (допустим 1 к 1, 600 на 600) отличается от запрошенного формата (2 к 1)

    |resize:600:300:crop
    создаст превью 600 на 300 без белого фона обрезав изображение под требуемый размер.

    |resize:600:300:crop:blob
    создаст превью 600 на 300 без белого фона обрезав изображение под требуемый размер. выдаст base64 данные вместо урла на файл.

    без высоты:
    |resize:200
    создаст превью 200 на Х без белого фона, по формату как у оригинала. если оригинал 4 к 3 (800 на 600), то превью будет 4 к 3 (200 на 150)

    без ширины:
    |resize:products:0:200
    создаст превью Х на 150 без белого фона, по формату как у оригинала. если оригинал 4 к 3 (800 на 600), то превью будет 4 к 3 (266 на 200)

    без высоты и ширины
    |resize
    создаст превью по размерам оригинала
    */
    public function create_resize($image, $width = 0, $height = 0, $option = "auto", $blob = false)
    {
        if(is_string($image))
        {
            //удаляем слешь в начале если он есть
            $filepath = substr($image, 0, 1) == '/' ? substr($image, 1) : $image;
            $image_info = pathinfo($filepath);

            $image = new stdClass();
            $image->filepath = $filepath;
            $image->filename = $image_info['basename'];
            $image->filetype = $image_info['extension'];
            $image->relation = $this->get_relation($filepath) ?? 'files';
        }

        if(!is_object($image))
            return false;

        if(!file_exists($image->filepath))
            return false;

        //если файл svg то вернем данные сразу
        //растровую графику отдаём в base64, а вектор "как есть"
        if($image->filetype == "svg")
            return $blob ? file_get_contents($image->filepath) : $image->filepath;

        $cache_parts = array(
            "dir"    => $image->relation,
            "name"   => basename($image->filename, ".".$image->filetype),
            "width"  => $width,
            "height" => $height,
            "mime"   => $image->filetype
        );

        //формируем название кэш-файла через . обозначая параметры: папка.имя.ширина.высота.тип
        $cache_filename = $this->resized_images_dir.implode(".", $cache_parts);

        //проверяем есть у нас такая фотка или нет, чтобы не создавать лишнюю нагрузку каждый раз пересоздавая файл
        if(!file_exists($cache_filename))
        {
            //если опция создания изображения не указана
            if($option == "auto")
            {
                //если длины и высоты, то выводим оригинал
                if(!$width && !$height)
                    $option = "original";

                //если нет высоты
                if($width && !$height)
                    $option = "landscape";

                //если нет длины
                if(!$width && $height)
                    $option = "portrait";

                //если есть и то и другое, то рисуем изображеие с белым фоном
                if($width > 0 && $height > 0)
                    $option = "exact";
            }
            //оригинал изображения
            $this->start($image->filepath);
            //(options: exact, portrait, landscape, auto, crop)
            $this->resizeImage($width, $height, $option);
            //сохраняем изображение
            $this->saveImage($cache_filename);
        }

        $res = $cache_filename;
        //если включен вывод в base64, то отдаём в этом формате.
        if($blob)
        {
            $image_content = base64_encode(file_get_contents($cache_filename));
            $res = "data:image/".$image->filetype.";base64,".$image_content;
        }
        return $res;
    }

    private function start($filename)
    {
        list($this->width, $this->height, $this->type, $attr) = array_values(getimagesize($this->config->root_dir . $filename));
        $this->image = $this->openImage($this->config->root_dir . $filename);
    }

    private function openImage($file)
    {
        //переопределяем переменную
        $this->type = image_type_to_mime_type($this->type);
        switch($this->type){
            case 'image/webp':
                $img = @imagecreatefromwebp($file);
                break;
            case 'image/jpeg':
                $img = @imagecreatefromjpeg($file);
                break;
            case 'image/gif':
                $img = @imagecreatefromgif($file);
                break;
            case 'image/png':
                $img = @imagecreatefrompng($file);
                imagealphablending($img, true);
                break;
            default:
                return false;
        }
        return $img;
    }

    public function resizeImage($newWidth, $newHeight, $option = "auto")
    {
        // *** Get optimal width and height - based on $option
        $optionArray = $this->getDimensions($newWidth, $newHeight, strtolower($option));
        $optimalWidth  = floor($optionArray['optimalWidth']);
        $optimalHeight = floor($optionArray['optimalHeight']);
        $xpos = 0;
        $ypos = 0;

        $colors = imagecolorstotal($this->image);
        // create destination image (indexed, if possible)
        if ($colors > 0 && $colors <= 256)
            $this->imageResized = imagecreate($optimalWidth, $optimalHeight);
        else
            $this->imageResized = imagecreatetruecolor($optimalWidth, $optimalHeight);

        if (empty($this->imageResized))
            return false;

        $transparent_index = imagecolortransparent($this->image);

        if ($transparent_index >= 0 && $transparent_index <= 128)
        {
            $t_c = imagecolorsforindex($this->image, $transparent_index);
            $transparent_index = imagecolorallocate($this->imageResized, $t_c['red'], $t_c['green'], $t_c['blue']);
        }
        else
        {
            if ($this->type === 'image/png')
                $transparent_index = imagecolorallocatealpha($this->imageResized, 255, 255, 255, 127);
            else
                $transparent_index = imagecolorallocate($this->imageResized, 255, 255, 255);
        }

        if ($transparent_index === false)
            return false;

        if (!imagefill($this->imageResized, 0, 0, $transparent_index))
            return false;

        imagecolortransparent($this->imageResized, $transparent_index);
        if($option == "exact")
        {
            $scale = min($optimalWidth / $this->width, $optimalHeight / $this->height);
            $new_width = (int)($this->width * $scale);
            $new_height = (int)($this->height * $scale);
            $xpos = (int)(($optimalWidth - $new_width) / 2);
            $ypos = (int)(($optimalHeight - $new_height) / 2);
            $optimalWidth = $new_width;
            $optimalHeight = $new_height;
        }

        // *** Resample - create image canvas of x, y size
        imagecopyresampled($this->imageResized, $this->image, $xpos, $ypos, 0, 0, $optimalWidth, $optimalHeight, $this->width, $this->height);

        // если указан кроп, то применяем
        if ($option == 'crop')
            $this->crop($optimalWidth, $optimalHeight, $newWidth, $newHeight);

        //возвращаем новое изображение
        return $this->imageResized;
    }

    private function getDimensions($newWidth, $newHeight, $option)
    {
       switch ($option){
           case 'original':
               $optimalWidth = $this->width;
               $optimalHeight= $this->height;
               break;
            case 'exact':
                $optimalWidth = $newWidth;
                $optimalHeight= $newHeight;
                break;
            case 'portrait':
                $optimalWidth = $this->getSizeByFixedHeight($newHeight);
                $optimalHeight= $newHeight;
                break;
            case 'landscape':
                $optimalWidth = $newWidth;
                $optimalHeight= $this->getSizeByFixedWidth($newWidth);
                break;
            case 'auto':
                $optionArray = $this->getSizeByAuto($newWidth, $newHeight);
                $optimalWidth = $optionArray['optimalWidth'];
                $optimalHeight = $optionArray['optimalHeight'];
                break;
            case 'crop':
                $optionArray = $this->getOptimalCrop($newWidth, $newHeight);
                $optimalWidth = $optionArray['optimalWidth'];
                $optimalHeight = $optionArray['optimalHeight'];
                break;
        }
        return array('optimalWidth' => $optimalWidth, 'optimalHeight' => $optimalHeight);
    }

    private function getSizeByFixedHeight($newHeight)
    {
        $newWidth = ($this->width / $this->height) * $newHeight;
        return $newWidth;
    }

    private function getSizeByFixedWidth($newWidth)
    {
        $newHeight = ($this->height / $this->width) * $newWidth;
        return $newHeight;
    }

    private function getSizeByAuto($newWidth, $newHeight)
    {
        if ($this->height < $this->width)
        // *** Image to be resized is wider (landscape)
        {
            $optimalWidth = $newWidth;
            $optimalHeight= $this->getSizeByFixedWidth($newWidth);
        }
        elseif ($this->height > $this->width)
        // *** Image to be resized is taller (portrait)
        {
            $optimalWidth = $this->getSizeByFixedHeight($newHeight);
            $optimalHeight= $newHeight;
        }
        else
        // *** Image to be resizerd is a square
        {
            if ($newHeight < $newWidth) {
                $optimalWidth = $newWidth;
                $optimalHeight= $this->getSizeByFixedWidth($newWidth);
            } else if ($newHeight > $newWidth) {
                $optimalWidth = $this->getSizeByFixedHeight($newHeight);
                $optimalHeight= $newHeight;
            } else {
                // *** Sqaure being resized to a square
                $optimalWidth = $newWidth;
                $optimalHeight= $newHeight;
            }
        }

        return array('optimalWidth' => $optimalWidth, 'optimalHeight' => $optimalHeight);
    }

    private function getOptimalCrop($newWidth, $newHeight)
    {
        $heightRatio = $this->height / $newHeight;
        $widthRatio  = $this->width /  $newWidth;

        if ($heightRatio < $widthRatio) {
            $optimalRatio = $heightRatio;
        } else {
            $optimalRatio = $widthRatio;
        }

        $optimalHeight = $this->height / $optimalRatio;
        $optimalWidth  = $this->width  / $optimalRatio;

        return array('optimalWidth' => $optimalWidth, 'optimalHeight' => $optimalHeight);
    }

    private function crop($optimalWidth, $optimalHeight, $newWidth, $newHeight)
    {
        // *** Find center - this will be used for the crop
        $cropStartX = ( $optimalWidth / 2) - ( $newWidth /2 );
        $cropStartY = ( $optimalHeight/ 2) - ( $newHeight/2 );

        $crop = $this->imageResized;
        //imagedestroy($this->imageResized);

        // *** Now crop from center to exact requested size
        $this->imageResized = imagecreatetruecolor($newWidth , $newHeight);
        //сохраняем прозрачность у пнг файлов
        imagealphablending($this->imageResized, false);
        imagesavealpha($this->imageResized, true);

        // integer representation of the color black (rgb: 0,0,0)
        $background = imagecolorallocate($this->imageResized , 0, 0, 0);
        // removing the black from the placeholder
        imagecolortransparent($this->imageResized, $background);

        imagecopyresampled($this->imageResized, $crop , 0, 0, $cropStartX, $cropStartY, $newWidth, $newHeight , $newWidth, $newHeight);
    }

    private function saveImage($savePath)
    {
        switch($this->type)
        {
            case 'image/webp':
                imagewebp($this->imageResized, $savePath);
            break;

            case 'image/jpeg':
                imagejpeg($this->imageResized, $savePath, $this->image_quality);
            break;
            case 'image/gif':
                imagegif($this->imageResized, $savePath);
            break;
            case 'image/png':
                imagesavealpha($this->imageResized, true);
                // *** Scale quality from 0-100 to 0-9
                $scaleQuality = round(($this->image_quality/100) * 9);
                // *** Invert quality setting as 0 is best, not 9
                $invertScaleQuality = 9 - $scaleQuality;
                imagepng($this->imageResized, $savePath, $invertScaleQuality);
            break;
            default:
                // *** No extension - No save.
            break;
        }

        imagedestroy($this->imageResized);
    }
}
