<?php

namespace Gregwar\Image\Adapter;

use Gregwar\Image\Image;
use Gregwar\Image\Source\Create;
use Gregwar\Image\Source\Data;
use Gregwar\Image\Source\File;
use Gregwar\Image\Source\Resource;

abstract class Common extends Adapter
{
    /**
     * {@inheritdoc}
     */
    public function zoomCrop($width, $height, $background = 'transparent', $xPosLetter = 'center', $yPosLetter = 'center')
    {
        $originalWidth = $this->width();
        $originalHeight = $this->height();

        // Calculate the different ratios
        $originalRatio = $originalWidth / $originalHeight;
        $newRatio = $width / $height;

        // Compare ratios
        if ($originalRatio > $newRatio) {
            // Original image is wider
            $newHeight = $height;
            $newWidth = (int) $height * $originalRatio;
        } else {
            // Equal width or smaller
            $newHeight = (int) $width / $originalRatio;
            $newWidth = $width;
        }

        // Perform resize
        $this->resize($newWidth, $newHeight, $background, true);

        // Define x position
        switch ($xPosLetter) {
            case 'L':
            case 'left':
                $xPos = 0;
                break;
            case 'R':
            case 'right':
                $xPos = (int) $newWidth - $width;
                break;
            case 'center':
                $xPos = (int) ($newWidth - $width) / 2;
                break;
            default:
                $factorW = $newWidth / $originalWidth;
                $xPos = $xPosLetter * $factorW;

                // If the desired cropping position goes beyond the width then
                // set the crop to be within the correct bounds.
                if ($xPos + $width > $newWidth) {
                    $xPos = (int) $newWidth - $width;
                }
        }

        // Define y position
        switch ($yPosLetter) {
            case 'T':
            case 'top':
                $yPos = 0;
                break;
            case 'B':
            case 'bottom':
                $yPos = (int) $newHeight - $height;
                break;
            case 'center':
                $yPos = (int) ($newHeight - $height) / 2;
                break;
            default:
                $factorH = $newHeight / $originalHeight;
                $yPos = $yPosLetter * $factorH;

                // If the desired cropping position goes beyond the height then
                // set the crop to be within the correct bounds.
                if ($yPos + $height > $newHeight) {
                    $yPos = (int) $newHeight - $height;
                }
        }

        // Crop image to reach desired size
        $this->crop($xPos, $yPos, $width, $height);

        return $this;
    }

    /**
     * Stamps another image on this one, scaled to fit a share of this image
     * and placed on a 3x3 grid. Unlike merge(), the size and place are worked
     * out when the operation runs, against the image as it is by then, so a
     * resize or crop queued before it is taken into account.
     *
     * @param Image        $other    the stamp, never enlarged
     * @param string       $position "<top|center|bottom>-<left|center|right>"
     * @param float|string $scale    the share of this image the stamp may fill, 0 to 1
     *
     * @return $this
     */
    public function watermark(Image $other, $position = 'center-center', $scale = 1.0)
    {
        $width = $this->width();
        $height = $this->height();
        $scale = min(1.0, (float) $scale);

        $stamp = clone $other;
        $stamp->init();
        $stamp->applyOperations();
        $ratio = min(1.0, $width * $scale / $stamp->width(), $height * $scale / $stamp->height());
        if ($ratio <= 0) {
            return $this;
        }

        $stampWidth = max(1, (int) round($stamp->width() * $ratio));
        $stampHeight = max(1, (int) round($stamp->height() * $ratio));

        $parts = explode('-', (string) $position, 2);
        $vertical = $parts[0];
        $horizontal = isset($parts[1]) ? $parts[1] : 'center';

        $x = intdiv($width - $stampWidth, 2);
        if ($horizontal === 'left') {
            $x = 0;
        } elseif ($horizontal === 'right') {
            $x = $width - $stampWidth;
        }

        $y = intdiv($height - $stampHeight, 2);
        if ($vertical === 'top') {
            $y = 0;
        } elseif ($vertical === 'bottom') {
            $y = $height - $stampHeight;
        }

        $other = clone $other;
        $other->forceResize($stampWidth, $stampHeight);

        return $this->merge($other, $x, $y);
    }

    /**
     * Resizes the image forcing the destination to have exactly the
     * given width and the height.
     *
     * @param int|null $width  the width
     * @param int|null $height  the height
     * @param string|int $background the background
     */
    public function forceResize($width = null, $height = null, $background = 'transparent')
    {
        return $this->resize($width, $height, $background, true);
    }

    /**
     * {@inheritdoc}
     */
    public function scaleResize($width = null, $height = null, $background = 'transparent', $crop = false)
    {
        return $this->resize($width, $height, $background, false, true, $crop);
    }

    /**
     * {@inheritdoc}
     */
    public function cropResize($width = null, $height = null, $background = 'transparent')
    {
        return $this->resize($width, $height, $background, false, false, true);
    }

    /**
     * Read exif rotation from file and apply it.
     *
     * @return $this
     */
    public function fixOrientation()
    {
        if (!extension_loaded('exif')) {
            throw new \RuntimeException('You need to EXIF PHP Extension to use this function');
        }

        if (!in_array(exif_imagetype($this->source->getInfos()), array(
            IMAGETYPE_JPEG,
            IMAGETYPE_TIFF_II,
            IMAGETYPE_TIFF_MM,
        ))) {
            return $this;
        }

        $exif = @exif_read_data($this->source->getInfos());

        if ($exif === false || !array_key_exists('Orientation', $exif)) {
            return $this;
        }

        return $this->applyExifOrientation($exif['Orientation']);
    }

    /**
     * Apply orientation using Exif orientation value.
     *
     * @param int $exif_orienation
     *
     * @return $this
     */
    public function applyExifOrientation($exif_orientation)
    {
        switch ($exif_orientation) {
            case 1:
                break;

            case 2:
                $this->flip(false, true);
                break;

            case 3: // 180 rotate left
                $this->rotate(180);
                break;

            case 4: // vertical flip
                $this->flip(true, false);
                break;

            case 5: // vertical flip + 90 rotate right
                $this->flip(true, false);
                $this->rotate(-90);
                break;

            case 6: // 90 rotate right
                $this->rotate(-90);
                break;

            case 7: // horizontal flip + 90 rotate right
                $this->flip(false, true);
                $this->rotate(-90);
                break;

            case 8: // 90 rotate left
                $this->rotate(90);
                break;
        }

        return $this;
    }

    /**
     * Opens gif image.
     *
     * @param string
     *
     * @return void
     */
    abstract protected function openGif($file);

    /**
     * Opens jpeg image.
     *
     * @param string
     *
     * @return void
     */
    abstract protected function openJpeg($file);

    /**
     * Opens png image.
     *
     * @param string
     *
     * @return void
     */
    abstract protected function openPng($file);

    /**
     * Opens webp image.
     *
     * @param string
     *
     * @return void
     */
    abstract protected function openWebp($file);

    abstract protected function openAvif($file);

    /**
     * Creates an image.
     *
     * @param int $width
     * @param int $height
     *
     * @return void
     */
    abstract protected function createImage($width, $height);

    /**
     * Creating an image using $data.
     *
     * @param string $data
     *
     * @return void
     */
    abstract protected function createImageFromData($data);

    /**
     * Loading image from $resource.
     *
     * @param resource
     *
     * @return void
     */
    protected function loadResource($resource)
    {
        $this->resource = $resource;
    }

    /**
     * @param string $file
     * @param string $type
     *
     * @return void
     */
    protected function loadFile($file, $type)
    {
        if (!$this->supports($type)) {
            throw new \RuntimeException('Type '.$type.' is not supported by GD');
        }

        if ($type === 'jpeg') {
            $this->openJpeg($file);
        }

        if ($type === 'gif') {
            $this->openGif($file);
        }

        if ($type === 'png') {
            $this->openPng($file);
        }

        if ($type === 'webp') {
            $this->openWebp($file);
        }

        if ($type == 'avif') {
            $this->openAvif($file);
        }

        if (false === $this->resource) {
            throw new \UnexpectedValueException('Unable to open file ('.$file.')');
        }

        $this->convertToTrueColor();
    }

    /**
     * {@inheritdoc}
     */
    public function init()
    {
        $source = $this->source;

        if ($source instanceof File) {
            $this->loadFile($source->getFile(), $source->guessType());
        } elseif ($source instanceof Create) {
            $this->createImage($source->getWidth(), $source->getHeight());
        } elseif ($source instanceof Data) {
            $this->createImageFromData($source->getData());
        } elseif ($source instanceof Resource) {
            $this->loadResource($source->getResource());
        } else {
            throw new \Exception('Unsupported image source type '.get_class($source));
        }

        return $this;
    }

    /**
     * {@inheritdoc}
     */
    public function deinit()
    {
        $this->resource = null;
    }

    /**
     * {@inheritdoc}
     */
    public function resize($width = null, $height = null, $background = 'transparent', $force = false, $rescale = false, $crop = false)
    {
        $current_width = $this->width();
        $current_height = $this->height();
        $new_width = 0;
        $new_height = 0;
        $scale = 1.0;

        if ($height === null && preg_match('#^(.+)%$#mUsi', $width, $matches)) {
            $width = round($current_width * ((float) $matches[1] / 100.0));
            $height = round($current_height * ((float) $matches[1] / 100.0));
        }

        // Don't enlarge on a plain resize. When the requested box is larger
        // than the source in a given axis, the scale logic below never upscales
        // the pixels, yet a canvas is still built at the requested size and the
        // un-enlarged image is padded with the background colour (a white
        // border on JPEGs, which have no transparency). Cap the request to the
        // source so the image is returned at its natural size instead. The
        // explicit-upscale entry points — forceResize() ($force) and
        // scaleResize() ($rescale) — are untouched and still enlarge on demand.
        if (!$force && !$rescale && !$crop) {
            if ($width !== null && $width > $current_width) {
                $width = $current_width;
            }
            if ($height !== null && $height > $current_height) {
                $height = $current_height;
            }
        }

        if (!$rescale && (!$force || $crop)) {
            if ($width !== null && $current_width > $width) {
                $scale = $current_width / $width;
            }

            if ($height !== null && $current_height > $height) {
                if ($current_height / $height > $scale) {
                    $scale = $current_height / $height;
                }
            }
        } else {
            if ($width !== null) {
                $scale = $current_width / $width;
                $new_width = $width;
            }

            if ($height !== null) {
                if ($width !== null && $rescale) {
                    $scale = max($scale, $current_height / $height);
                } else {
                    $scale = $current_height / $height;
                }
                $new_height = $height;
            }
        }

        if (!$force || $width === null || $rescale) {
            $new_width = round($current_width / $scale);
        }

        if (!$force || $height === null || $rescale) {
            $new_height = round($current_height / $scale);
        }

        if ($width === null || $crop) {
            $width = $new_width;
        }

        if ($height === null || $crop) {
            $height = $new_height;
        }

        $this->doResize($background, (int) $width, (int) $height, (int) $new_width, (int) $new_height);
    }

    /**
     * Trim background color around the image.
     *
     * @param string|int $background the background
     *
     * @return void
     */
    protected function _trimColor($background = 'transparent')
    {
        $width = $this->width();
        $height = $this->height();

        $b_top = 0;
        $b_lft = 0;
        $b_btm = $height - 1;
        $b_rt = $width - 1;

        //top
        for (; $b_top < $height; ++$b_top) {
            for ($x = 0; $x < $width; ++$x) {
                if ($this->getColor($x, $b_top) !== $background) {
                    break 2;
                }
            }
        }

        // bottom
        for (; $b_btm >= 0; --$b_btm) {
            for ($x = 0; $x < $width; ++$x) {
                if ($this->getColor($x, $b_btm) !== $background) {
                    break 2;
                }
            }
        }

        // left
        for (; $b_lft < $width; ++$b_lft) {
            for ($y = $b_top; $y <= $b_btm; ++$y) {
                if ($this->getColor($b_lft, $y) !== $background) {
                    break 2;
                }
            }
        }

        // right
        for (; $b_rt >= 0; --$b_rt) {
            for ($y = $b_top; $y <= $b_btm; ++$y) {
                if ($this->getColor($b_rt, $y) !== $background) {
                    break 2;
                }
            }
        }

        ++$b_btm;
        ++$b_rt;

        $this->crop($b_lft, $b_top, $b_rt - $b_lft, $b_btm - $b_top);
    }

    /**
     * Resizes the image to an image having size of $target_width, $target_height, using
     * $new_width and $new_height and padding with $bg color.
     *
     * @param string|int $bg
     * @param int $target_width
     * @param int $target_height
     * @param int $new_width
     * @param int $new_height
     *
     * @return $this
     */
    abstract protected function doResize($bg, int $target_width, int $target_height, int $new_width, int $new_height);

    /**
     * Gets the color of the $x, $y pixel.
     *
     * @param int $x
     * @param int $y
     *
     * @return int
     */
    abstract protected function getColor($x, $y);

    /**
     * {@inheritdoc}
     */
    public function enableProgressive()
    {
        throw new \Exception('The Adapter '.$this->getName().' does not support Progressive Image loading');
    }

    /**
     * This does nothing, but can be used to tag a ressource for instance (having a final image hash
     * for the cache different depending on the tag)
     *
     * @param mixed $tag
     *
     * @return void
     */
    public function tag($tag)
    {
    }
}
