<?php

declare(strict_types=1);

namespace Laminas\Barcode\Renderer;

use Laminas\Barcode\Barcode;
use Laminas\Barcode\Exception as BarcodeException;
use Laminas\Barcode\Object\ObjectInterface;
use Laminas\Stdlib\ArrayUtils;
use Traversable;

use function floatval;
use function floor;
use function in_array;
use function intval;
use function is_array;
use function is_numeric;
use function method_exists;
use function strlen;
use function strtolower;
use function substr;

/**
 * Class for rendering the barcode
 */
abstract class AbstractRenderer implements RendererInterface
{
    /**
     * Namespace of the renderer for autoloading
     *
     * @var string
     */
    protected $rendererNamespace = 'Laminas\Barcode\Renderer';

    /**
     * Renderer type
     *
     * @var string
     */
    protected $type;

    /**
     * Activate/Deactivate the automatic rendering of exception
     *
     * @var bool
     */
    protected $automaticRenderError = false;

    /**
     * Offset of the barcode from the top of the rendering resource
     *
     * @var int
     */
    protected $topOffset = 0;

    /**
     * Offset of the barcode from the left of the rendering resource
     *
     * @var int
     */
    protected $leftOffset = 0;

    /**
     * Horizontal position of the barcode in the rendering resource
     *
     * @var string
     */
    protected $horizontalPosition = 'left';

    /**
     * Vertical position of the barcode in the rendering resource
     *
     * @var string
     */
    protected $verticalPosition = 'top';

    /**
     * Module size rendering
     *
     * @var float
     */
    protected $moduleSize = 1;

    /**
     * Barcode object
     *
     * @var ObjectInterface
     */
    protected $barcode;

    /**
     * Drawing resource
     *
     * @var AbstractRenderer
     */
    protected $resource;

    /**
     * Show a transparent background
     *
     * @var Boolean
     */
    protected $transparentBackground = false;

    /**
     * Constructor
     *
     * @param array|Traversable $options
     */
    public function __construct($options = null)
    {
        if ($options instanceof Traversable) {
            $options = ArrayUtils::iteratorToArray($options);
        }
        if (is_array($options)) {
            $this->setOptions($options);
        }
        $this->type = strtolower(
            substr(
                static::class,
                strlen($this->rendererNamespace) + 1
            )
        );
    }

    /**
     * Set renderer state from options array
     *
     * @param array $options
     * @return self Provides a fluent interface
     */
    public function setOptions($options)
    {
        foreach ($options as $key => $value) {
            $method = 'set' . $key;
            if (method_exists($this, $method)) {
                $this->$method($value);
            }
        }
        return $this;
    }

    /**
     * Set renderer namespace for autoloading
     *
     * @param string $namespace
     * @return self Provides a fluent interface
     */
    public function setRendererNamespace($namespace)
    {
        $this->rendererNamespace = $namespace;
        return $this;
    }

    /**
     * Retrieve renderer namespace
     *
     * @return string
     */
    public function getRendererNamespace()
    {
        return $this->rendererNamespace;
    }

    /**
     * Set whether background should be transparent
     * Will work for SVG and Image (png and gif only)
     *
     * @param bool $bool
     * @return self Provides a fluent interface
     */
    public function setTransparentBackground($bool)
    {
        $this->transparentBackground = $bool;

        return $this;
    }

    /**
     * @return bool
     */
    public function getTransparentBackground()
    {
        return $this->transparentBackground;
    }

    /**
     * Retrieve renderer type
     *
     * @return string
     */
    public function getType()
    {
        return $this->type;
    }

    /**
     * Manually adjust top position
     *
     * @param int $value
     * @return self Provides a fluent interface
     * @throws Exception\OutOfRangeException
     */
    public function setTopOffset($value)
    {
        if (! is_numeric($value) || intval($value) < 0) {
            throw new Exception\OutOfRangeException(
                'Vertical position must be greater than or equals 0'
            );
        }
        $this->topOffset = intval($value);
        return $this;
    }

    /**
     * Retrieve vertical adjustment
     *
     * @return int
     */
    public function getTopOffset()
    {
        return $this->topOffset;
    }

    /**
     * Manually adjust left position
     *
     * @param int $value
     * @return self Provides a fluent interface
     * @throws Exception\OutOfRangeException
     */
    public function setLeftOffset($value)
    {
        if (! is_numeric($value) || intval($value) < 0) {
            throw new Exception\OutOfRangeException(
                'Horizontal position must be greater than or equals 0'
            );
        }
        $this->leftOffset = intval($value);
        return $this;
    }

    /**
     * Retrieve vertical adjustment
     *
     * @return int
     */
    public function getLeftOffset()
    {
        return $this->leftOffset;
    }

    /**
     * Activate/Deactivate the automatic rendering of exception
     *
     * @param bool $value
     * @return self Provides a fluent interface
     */
    public function setAutomaticRenderError($value)
    {
        $this->automaticRenderError = (bool) $value;
        return $this;
    }

    /**
     * Horizontal position of the barcode in the rendering resource
     *
     * @param string $value
     * @return self Provides a fluent interface
     * @throws Exception\UnexpectedValueException
     */
    public function setHorizontalPosition($value)
    {
        if (! in_array($value, ['left', 'center', 'right'])) {
            throw new Exception\UnexpectedValueException(
                "Invalid barcode position provided must be 'left', 'center' or 'right'"
            );
        }
        $this->horizontalPosition = $value;
        return $this;
    }

    /**
     * Horizontal position of the barcode in the rendering resource
     *
     * @return string
     */
    public function getHorizontalPosition()
    {
        return $this->horizontalPosition;
    }

    /**
     * Vertical position of the barcode in the rendering resource
     *
     * @param string $value
     * @return self Provides a fluent interface
     * @throws Exception\UnexpectedValueException
     */
    public function setVerticalPosition($value)
    {
        if (! in_array($value, ['top', 'middle', 'bottom'])) {
            throw new Exception\UnexpectedValueException(
                "Invalid barcode position provided must be 'top', 'middle' or 'bottom'"
            );
        }
        $this->verticalPosition = $value;
        return $this;
    }

    /**
     * Vertical position of the barcode in the rendering resource
     *
     * @return string
     */
    public function getVerticalPosition()
    {
        return $this->verticalPosition;
    }

    /**
     * Set the size of a module
     *
     * @param float $value
     * @return self Provides a fluent interface
     * @throws Exception\OutOfRangeException
     */
    public function setModuleSize($value)
    {
        if (! is_numeric($value) || floatval($value) <= 0) {
            throw new Exception\OutOfRangeException(
                'Float size must be greater than 0'
            );
        }
        $this->moduleSize = floatval($value);
        return $this;
    }

    /**
     * Set the size of a module
     *
     * @return float
     */
    public function getModuleSize()
    {
        return $this->moduleSize;
    }

    /**
     * Retrieve the automatic rendering of exception
     *
     * @return bool
     */
    public function getAutomaticRenderError()
    {
        return $this->automaticRenderError;
    }

    /**
     * Set the barcode object
     *
     * @return self Provides a fluent interface
     */
    public function setBarcode(ObjectInterface $barcode)
    {
        $this->barcode = $barcode;
        return $this;
    }

    /**
     * Retrieve the barcode object
     *
     * @return ObjectInterface
     */
    public function getBarcode()
    {
        return $this->barcode;
    }

    /**
     * Checking of parameters after all settings
     *
     * @return bool
     */
    public function checkParams()
    {
        $this->checkBarcodeObject();
        $this->checkSpecificParams();
        return true;
    }

    /**
     * Check if a barcode object is correctly provided
     *
     * @return void
     * @throws Exception\RuntimeException
     */
    protected function checkBarcodeObject()
    {
        if ($this->barcode === null) {
            throw new Exception\RuntimeException(
                'No barcode object provided'
            );
        }
    }

    /**
     * Calculate the left and top offset of the barcode in the
     * rendering support
     *
     * @param float $supportHeight
     * @param float $supportWidth
     * @return void
     */
    protected function adjustPosition($supportHeight, $supportWidth)
    {
        $barcodeHeight = $this->barcode->getHeight(true) * $this->moduleSize;
        // phpcs:ignore SlevomatCodingStandard.Operators.DisallowEqualOperators
        if ($barcodeHeight != $supportHeight && $this->topOffset == 0) {
            switch ($this->verticalPosition) {
                case 'middle':
                    $this->topOffset = floor(($supportHeight - $barcodeHeight) / 2);
                    break;
                case 'bottom':
                    $this->topOffset = $supportHeight - $barcodeHeight;
                    break;
                case 'top':
                default:
                    $this->topOffset = 0;
                    break;
            }
        }
        $barcodeWidth = $this->barcode->getWidth(true) * $this->moduleSize;
        // phpcs:ignore SlevomatCodingStandard.Operators.DisallowEqualOperators
        if ($barcodeWidth != $supportWidth && $this->leftOffset == 0) {
            switch ($this->horizontalPosition) {
                case 'center':
                    $this->leftOffset = floor(($supportWidth - $barcodeWidth) / 2);
                    break;
                case 'right':
                    $this->leftOffset = $supportWidth - $barcodeWidth;
                    break;
                case 'left':
                default:
                    $this->leftOffset = 0;
                    break;
            }
        }
    }

    /**
     * Draw the barcode in the rendering resource
     *
     * @return AbstractRenderer
     * @throws BarcodeException\ExceptionInterface
     */
    public function draw()
    {
        try {
            $this->checkParams();
            $this->initRenderer();
            $this->drawInstructionList();
        } catch (BarcodeException\ExceptionInterface $e) {
            if ($this->automaticRenderError && ! $e instanceof BarcodeException\RendererCreationException) {
                $barcode = Barcode::makeBarcode(
                    'error',
                    ['text' => $e->getMessage()]
                );
                $this->setBarcode($barcode);
                $this->resource = null;
                $this->initRenderer();
                $this->drawInstructionList();
            } else {
                throw $e;
            }
        }
        return $this->resource;
    }

    /**
     * Sub process to draw the barcode instructions
     * Needed by the automatic error rendering
     */
    private function drawInstructionList()
    {
        $instructionList = $this->barcode->draw();
        foreach ($instructionList as $instruction) {
            switch ($instruction['type']) {
                case 'polygon':
                    $this->drawPolygon(
                        $instruction['points'],
                        $instruction['color'],
                        $instruction['filled']
                    );
                    break;
                case 'text': //$text, $size, $position, $font, $color, $alignment = 'center', $orientation = 0)
                    $this->drawText(
                        $instruction['text'],
                        $instruction['size'],
                        $instruction['position'],
                        $instruction['font'],
                        $instruction['color'],
                        $instruction['alignment'],
                        $instruction['orientation']
                    );
                    break;
                default:
                    throw new Exception\UnexpectedValueException(
                        'Unknown drawing command'
                    );
            }
        }
    }

    /**
     * Checking of parameters after all settings
     *
     * @return void
     */
    abstract protected function checkSpecificParams();

    /**
     * Initialize the rendering resource
     *
     * @return void
     */
    abstract protected function initRenderer();

    /**
     * Draw a polygon in the rendering resource
     *
     * @param array $points
     * @param int $color
     * @param bool $filled
     */
    abstract protected function drawPolygon($points, $color, $filled = true);

    /**
     * Draw a polygon in the rendering resource
     *
     * @param string $text
     * @param float $size
     * @param array $position
     * @param string $font
     * @param int $color
     * @param string $alignment
     * @param float|int $orientation
     */
    abstract protected function drawText(
        $text,
        $size,
        $position,
        $font,
        $color,
        $alignment = 'center',
        $orientation = 0
    );
}
